Background

This tutorial shows in a few examples how to use the GtkImageViewer. All the example codes may be found in the examples directory in the source distribution.

A trivial image viewer

The following is a trivial image viewer using the GtkImageViewer widget. It takes as input a filename and displays it in a window with the size of the image. The widget responds to keyboard and mouse input that has been modeled on Gimp and others.

Example: test_file.c
/*
 * A trivial GtkImageViewer example shows the file given on the
 * command line.
 */
#include <stdlib.h>
#include "gtk-image-viewer.h"

int
main (int argc, char *argv[])
{
  GtkWidget *window, *image_viewer;
  char *filename;

  gtk_init (&argc, &argv);

  if (argc < 2)
    {
      printf("Need name of image!\n");
      exit(0);
    }
  else
    filename = argv[1];

  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_policy(GTK_WINDOW(window), TRUE, TRUE, FALSE);
  gtk_window_set_title (GTK_WINDOW (window), filename);
  g_signal_connect (window, "destroy", G_CALLBACK (gtk_exit), NULL);

  image_viewer = gtk_image_viewer_new_from_file(filename);
  gtk_container_add (GTK_CONTAINER (window), image_viewer);

  gtk_widget_show_all (window);

  gtk_main ();

  return 0;
}

Note that the call to gtk_window_set_policy() is needed in order to allow shrinking as well as expanding the maind window.

User interaction

The following interactions are honered:

ScrollWheel Zoom in and out by 10%
Mouse button 1 Zoom in by a factor of 2 and center coordinate under mouse
Mouse button 2 Pan
Mouse button 3 Zoom out by a factor of 2
F Fill image in window
H Horizontal flip
V Vertical flip
1 or N Show native size
> or + or = Zoom in
< or - Zoom out

Adding scrolling

To add scrolling, just wrap the image viewer inside a scrolled window. Note that you have to manually set the size of the scrolled window as it does not inherit it from the size of the widgets it manages. To set the size of the widget it is more convinient to use the gtk_image_viewer_new() constructor, that takes a pointed to a GdkPixbuf as a parameter.

The example also adds a motion handler that shows the pixel coordinate when the mouse moves over the image. Note that even if you zoom and flip the image the correct pixel coordinate is shown.

Example: test_scroll.c
/*
 * A basic image viewer using the GtkImageViewer widget that shows
 * the pixel coordinate when moving over the image.
 */
#include <stdlib.h>
#include <gtk/gtk.h>
#include <gtk-image-viewer.h>
#include <math.h>

static gint
cb_key_press_event(GtkWidget *widget, GdkEventKey *event)
{
  gint k = event->keyval;

  if (k == 'q')
      exit(0);

  return FALSE;
}

static gint
cb_motion_event(GtkWidget *widget,
                GdkEventButton *event,
                gpointer user_data)
{
    gchar pixel_info[64];
    GtkWidget *label = GTK_WIDGET(user_data);
    double img_x = 0, img_y = 0;

    gtk_image_viewer_canv_coord_to_img_coord(GTK_IMAGE_VIEWER(widget),
                                             event->x, event->y,
                                             &img_x, &img_y);

    g_snprintf(pixel_info, sizeof(pixel_info), "(%d,%d)",
               (int)floor(img_x),
               (int)floor(img_y));
    gtk_label_set_text(GTK_LABEL(label), pixel_info);

    return 0;
}

int
main (int argc, char *argv[])
{
  GtkWidget *window, *vbox, *image_viewer, *scrolled_win, *label;
  GdkPixbuf *img;
  int width, height;

  gtk_init (&argc, &argv);

  if (argc < 2)
    {
      printf("Need name of image!\n");
      exit(0);
    }
  else
    {
      GError *error = NULL;
      img = gdk_pixbuf_new_from_file (argv[1], &error);
    }

  width = gdk_pixbuf_get_width (img);
  height = gdk_pixbuf_get_height (img);

  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_policy(GTK_WINDOW(window), TRUE, TRUE, FALSE);
  gtk_window_set_title (GTK_WINDOW (window), argv[1]);
  g_signal_connect (window, "destroy", G_CALLBACK (gtk_exit), NULL);

  vbox = gtk_vbox_new(0,0);
  gtk_container_add (GTK_CONTAINER (window), vbox);

  scrolled_win = gtk_scrolled_window_new(NULL,NULL);
  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_win),
                                 GTK_POLICY_AUTOMATIC,
                                 GTK_POLICY_AUTOMATIC);

  gtk_box_pack_start (GTK_CONTAINER (vbox), scrolled_win,
                      TRUE, TRUE, 0);
  gtk_widget_show(scrolled_win);

  image_viewer = gtk_image_viewer_new(img);
  gtk_widget_set_size_request (image_viewer, width<500?width:500,
                               height<500?height:500);
  g_signal_connect (window, "key_press_event",
                    G_CALLBACK(cb_key_press_event), NULL);
  gtk_container_add (GTK_CONTAINER (scrolled_win), image_viewer);
  gtk_widget_show (image_viewer);

  label = gtk_label_new("");
  gtk_box_pack_start (GTK_CONTAINER (vbox),
                      label, TRUE, TRUE, 0);
  gtk_widget_show(label);

  g_signal_connect (image_viewer, "motion_notify_event",
                    G_CALLBACK(cb_motion_event), label);

  gtk_widget_show_all (window);

  gtk_main ();

  return 0;
}

Drawing in the image annotate callback

The following example does not provide an image to the gtk image viewer, but instead uses the image annotate callback to generate image data dynamically. The image annotate callback receives a GdkPixbuf from the image being displayed and allows the user to annotate it before it is being shipped to the display. This allows for flicker free drawing of image overlays. In case no image has been provided to the GtkImageViewer then image annotate is simply provided with a white image.

Example: circle.c
//======================================================================
//  circle.c - A trivial example of how to generate pixel data on
//             demand.
//
//  Dov Grobgeld <dov.grobgeld@gmail.com>
//  Fri Oct  3 15:51:01 2008
//----------------------------------------------------------------------
#include <math.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include "gtk-image-viewer.h"


static void
cb_image_annotate(GtkImageViewer *imgv,
                  GdkPixbuf *pixbuf,
                  gint shift_x,
                  gint shift_y,
                  gdouble scale_x,
                  gdouble scale_y,
                  gpointer user_data
                  )
{
  int img_width = gdk_pixbuf_get_width(pixbuf);
  int img_height = gdk_pixbuf_get_height(pixbuf);
  int row_stride = gdk_pixbuf_get_rowstride(pixbuf);
  int pix_stride = 4;
  guint8 *buf = gdk_pixbuf_get_pixels(pixbuf);
  int col_idx, row_idx;

  for (row_idx=0; row_idx<img_height; row_idx++)
    {
      guint8 *row = buf + row_idx * row_stride;
      for (col_idx=0; col_idx<img_width; col_idx++)
        {
          gint gl = 255;
          double x=(col_idx+shift_x) / scale_x;
          double y=(row_idx+shift_y) / scale_y;

          // Draw a circle of size 4
          if (fabs(x*x + y*y) < 16)
            gl = 0;
          *(row+col_idx*pix_stride) = gl;   // r
          *(row+col_idx*pix_stride+1) = gl; // g

          // Play inner part of circle blue
          if (fabs(x*x + y*y) < 3.5*3.5)
            gl = 255;

          *(row+col_idx*pix_stride+2) = gl; // b
        }
    }
}

static gint
cb_key_press_event(GtkWidget *widget, GdkEventKey *event)
{
  gint k = event->keyval;

  if (k == 'q')
    exit(0);

  return FALSE;
}

int main(int argc, char **argv)
{
  GtkWidget *window, *image_viewer, *scrolled_win;
  GdkPixbuf *img;
  int width, height;

  gtk_init (&argc, &argv);

  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_policy(GTK_WINDOW(window), TRUE, TRUE, FALSE);

  gtk_window_set_title (GTK_WINDOW (window), "A circle");
  g_signal_connect (window, "destroy", G_CALLBACK (gtk_exit), NULL);

  scrolled_win = gtk_scrolled_window_new(NULL,NULL);
  gtk_container_add (GTK_CONTAINER (window), scrolled_win);
  gtk_widget_show(scrolled_win);

  image_viewer = gtk_image_viewer_new(NULL);
  g_signal_connect(image_viewer,
                   "image-annotate",
                   G_CALLBACK(cb_image_annotate), NULL);

  gtk_widget_set_size_request (window, 500,500);

  g_signal_connect (window, "key_press_event",
                    GTK_SIGNAL_FUNC(cb_key_press_event), NULL);

  gtk_container_add (GTK_CONTAINER (scrolled_win), image_viewer);

  gtk_widget_show (image_viewer);
  gtk_widget_show (window);

  // Set the scroll region and zoom range
  gtk_image_viewer_set_scroll_region(GTK_IMAGE_VIEWER(image_viewer),
                                     -5,-5,5,5);
  gtk_image_viewer_set_zoom_range(GTK_IMAGE_VIEWER(image_viewer),
                                  -HUGE, HUGE);

  // Need to do a manual zoom fit at creation because a bug when
  // not using an image.
  gtk_image_viewer_zoom_fit(GTK_IMAGE_VIEWER(image_viewer));
  gtk_main ();

  exit(0);
  return(0);
}

Complex dynamic image generation

Here is a more interesting example using the same concept to generate a mandelbrot image. It shows the power of the image generation on demand, even during panning. When panning, the image viewer generates image annotate events only or the newly exposed rectangles, and the rest of the image are just copied.

The same idea could be used for extracting data from a database, e.g. in a GIS system.

Example: mandelbrot.c
//======================================================================
//  mandelbrot.c - An example of how to use the gtk image viewer
//  infinite scrolling with image generation on the fly.
//
//  Dov Grobgeld <dov.grobgeld@gmail.com>
//  Fri Oct  3 15:51:01 2008
//----------------------------------------------------------------------
#include <math.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include "gtk-image-viewer.h"


#define MAX_IT 200
static int color_table[MAX_IT][3];

// Build a nice color table
static void build_color_table()
{
  int i;
  for (i=0; i<MAX_IT; i++)
    {
      // Three arbitrary functions that start at 0 and end at 1
      double r = pow(1.0*i/(MAX_IT-1), 0.1);
      double g = pow(1.0*i/(MAX_IT-1), 0.5);
      double b = pow(1.0*i/(MAX_IT-1), 1);
      color_table[i][0] = (int)(255*r);
      color_table[i][1] = (int)(255*g);
      color_table[i][2] = (int)(255*b);
    }
}

// Callback for exposure
static void
cb_image_annotate(GtkImageViewer *imgv,
                  GdkPixbuf *pixbuf,
                  gint shift_x,
                  gint shift_y,
                  gdouble scale_x,
                  gdouble scale_y,
                  gpointer user_data
                  )
{
  int img_width = gdk_pixbuf_get_width(pixbuf);
  int img_height = gdk_pixbuf_get_height(pixbuf);
  int row_stride = gdk_pixbuf_get_rowstride(pixbuf);
  int pix_stride = 4; // Fixed for GdkPixbuf's in cb_image_annotate
  guint8 *buf = gdk_pixbuf_get_pixels(pixbuf);
  int col_idx, row_idx;

  for (row_idx=0; row_idx<img_height; row_idx++) {
    guint8 *row = buf + row_idx * row_stride;
    for (col_idx=0; col_idx<img_width; col_idx++) {
      gint gl = 255;
      double x=(col_idx+shift_x) / scale_x;
      double y=(row_idx+shift_y) / scale_y;
      double x0 = x;
      double y0 = y;
      int iteration = 0;

      // Run mandelbrot test for (x,y) with code from wikipedia
      while(x*x + y*y < (2*2) && iteration < MAX_IT) {
        double xtemp = x*x - y*y + x0;
        y = 2 * x * y + y0;
        x = xtemp;
        iteration++;
      }

      *(row+col_idx*pix_stride) = color_table[iteration][0];
      *(row+col_idx*pix_stride+1) = color_table[iteration][1];
      *(row+col_idx*pix_stride+2) = color_table[iteration][2];
    }
  }
}

// main excluded as it is identical to the previous example

Result before and after zoom-in:

Output of the mandelbrot program

Drawing with cairo

The following example uses cairo to draw in the image annotate callback. Note that because of incompatibilities between the GdkPixbuf and Cairo, it is necessary to swap the red and the blue color in the cairo context.

Example: cairo-circles.c
//======================================================================
//  cairo-circles.c - An example of how to use cairo to draw on demand.
//
//  This program is released in the public domain.
//
//  Dov Grobgeld <dov.grobgeld@gmail.com>
//  Fri Oct  3 15:51:01 2008
//----------------------------------------------------------------------
#include <math.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <cairo.h>
#include "gtk-image-viewer.h"


static void
cb_image_annotate(GtkImageViewer *imgv,
                  GdkPixbuf *pixbuf,
                  gint shift_x,
                  gint shift_y,
                  gdouble scale_x,
                  gdouble scale_y,
                  gpointer user_data
                  )
{
  int img_width = gdk_pixbuf_get_width(pixbuf);
  int img_height = gdk_pixbuf_get_height(pixbuf);

  cairo_surface_t *surface
    = cairo_image_surface_create_for_data(gdk_pixbuf_get_pixels(pixbuf),
                                          CAIRO_FORMAT_RGB24,
                                          img_width,
                                          img_height,
                                          gdk_pixbuf_get_rowstride(pixbuf));
  cairo_t *cr = cairo_create (surface);
  cairo_translate(cr, -shift_x, -shift_y);
  cairo_scale(cr, scale_x, scale_y);

  // Now do any cairo commands you want, but you have to swap
  // R and B in set_source_rgba() commands because of cairo and
  // pixbuf incompabilitities.
  cairo_set_source_rgba (cr, 0,0,1.0,0.5);
  cairo_arc(cr,
            -1, 0,
            3, 0.0, 2*G_PI);
  cairo_fill(cr);

  cairo_set_source_rgba (cr, 1.0,0,0,0.5);
  cairo_arc(cr,
            1, 0,
            3, 0.0, 2*G_PI);
  cairo_fill(cr);

  cairo_set_source_rgba (cr, 0,0.9,0,0.9);
  cairo_arc(cr,
            0, 0,
            1, 0.0, 2*G_PI);
  cairo_fill(cr);

  // Display some tiny text
  cairo_set_font_size(cr, 0.08);
  cairo_set_source_rgba (cr, 0,0,0,1);
  cairo_move_to(cr, -0.2, 0);
  cairo_show_text(cr, "Cairo Rules!!!");

  // cleanup
  cairo_surface_destroy(surface);
  cairo_destroy(cr);
}

// main excluded as it is identical to the previous example

Result before and after zoom-in:

Output of the cairo-circles program

Drawing with agg

Anti-Grain graphics may be faster than cairo in certain situation, and it may also be used for drawing on the GdkPixbuf.

Example: agg-circles.c
//======================================================================
//  agg-example.c - Drawing with agg on the gtk image viewer.
//
//  This program is in the public domain
//  Dov Grobgeld <dov.grobgeld@gmail.com>
//  Sun Oct  5 21:34:03 2008
//----------------------------------------------------------------------

#include <math.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include "agg2/agg_rasterizer_scanline_aa.h"
#include "agg2/agg_ellipse.h"
#include "agg2/agg_scanline_p.h"
#include "agg2/agg_renderer_scanline.h"
#include "agg2/agg_path_storage.h"
#include "agg2/agg_pixfmt_rgba.h"
#include "gtk-image-viewer.h"

static void
cb_image_annotate(GtkImageViewer *imgv,
                  GdkPixbuf *pixbuf,
                  gint shift_x,
                  gint shift_y,
                  gdouble scale_x,
                  gdouble scale_y,
                  gpointer user_data
                  )
{
  int width = gdk_pixbuf_get_width(pixbuf);
  int height = gdk_pixbuf_get_height(pixbuf);

  // initialize agg
  agg::rendering_buffer rbuf(gdk_pixbuf_get_pixels(pixbuf),
                             width,
                             height,
                             gdk_pixbuf_get_rowstride(pixbuf));
  agg::pixfmt_rgba32 pixf(rbuf);
  agg::renderer_base<agg::pixfmt_rgba32> rbase(pixf);
  agg::rasterizer_scanline_aa<> pf;
  agg::scanline_p8 sl;
  agg::ellipse e1;

  // Setup the affine transform
  agg::trans_affine mtx;
  mtx *= agg::trans_affine_scaling(scale_x, scale_y);
  mtx *= agg::trans_affine_translation(-shift_x, -shift_y);

  // Apply it for each element being drawn.
  agg::conv_transform<agg::ellipse, agg::trans_affine> trans(e1, mtx);

  e1.init(1,0,3,3,128);
  pf.add_path(trans);
  agg::render_scanlines_aa_solid(pf, sl, rbase,
                                 agg::rgba(1,0,0,0.5));

  e1.init(-1,0,3,3,128);
  pf.add_path(trans);
  agg::render_scanlines_aa_solid(pf, sl, rbase,
                                 agg::rgba(0,0,1,0.5));

  e1.init(0,0,1,1,128);
  pf.add_path(trans);
  agg::render_scanlines_aa_solid(pf, sl, rbase,
                                 agg::rgba(0,0.9,0,0.9));

}

// main excluded as it is identical to the previous example

The result is the same as with cairo:

Circles drawn with agg

Drawing on top of images

Drawing on top of images really is really more of the same. The example draws two circles around Lena's eyes. Notice the nice cairo blending effect.

Example: lena-circles.c
static void
cb_image_annotate(GtkImageViewer *imgv,
                  GdkPixbuf *pixbuf,
                  gint shift_x,
                  gint shift_y,
                  gdouble scale_x,
                  gdouble scale_y,
                  gpointer user_data
                  )
{
  int img_width = gdk_pixbuf_get_width(pixbuf);
  int img_height = gdk_pixbuf_get_height(pixbuf);

  cairo_surface_t *surface
    = cairo_image_surface_create_for_data(gdk_pixbuf_get_pixels(pixbuf),
                                          CAIRO_FORMAT_RGB24,
                                          img_width,
                                          img_height,
                                          gdk_pixbuf_get_rowstride(pixbuf));
  cairo_t *cr = cairo_create (surface);
  cairo_translate(cr, -shift_x, -shift_y);
  cairo_scale(cr, scale_x, scale_y);

  // Now do any cairo commands you want, but you have to swap
  // R and B in set_source_rgba() commands because of cairo and
  // pixbuf incompabilitities.
  cairo_set_source_rgba (cr, 0,0,1.0,0.6);
  cairo_set_line_width(cr, 3);
  cairo_arc(cr,
            266.0, 267.0,
            12, 0.0, 2*G_PI);
  cairo_stroke(cr);

  cairo_set_source_rgba (cr, 1.0,0,0.0,0.6);
  cairo_arc(cr,
            327.0, 267.0,
            12, 0.0, 2*G_PI);
  cairo_stroke(cr);


  // cleanup
  cairo_surface_destroy(surface);
  cairo_destroy(cr);
}

// in main
  :
  image_viewer = gtk_image_viewer_new_from_file("lena.pgm");
  g_signal_connect(image_viewer,
                   "image-annotate",
                   G_CALLBACK(cb_image_annotate), NULL);
  :

The result is:

Circles drawn with cairo on top of an image