diff src/image.c @ 9:d907d608745f

Sync to GQview 1.5.9 release. ######## DO NOT BASE ENHANCEMENTS OR TRANSLATION UPDATES ON CODE IN THIS CVS! This CVS is never up to date with current development and is provided solely for reference purposes, please use the latest official release package when making any changes or translation updates. ########
author gqview
date Sat, 26 Feb 2005 00:13:35 +0000
parents e149abcda4eb
children 147f4c4b9025
line wrap: on
line diff
--- a/src/image.c	Sat Feb 26 00:07:07 2005 +0000
+++ b/src/image.c	Sat Feb 26 00:13:35 2005 +0000
@@ -1,111 +1,834 @@
 /*
- * GQview image viewer
- * (C)2000 John Ellis
+ * GQview
+ * (C) 2004 John Ellis
  *
  * Author: John Ellis
  *
+ * This software is released under the GNU General Public License (GNU GPL).
+ * Please read the included file COPYING for more information.
+ * This software comes with no warranty of any kind, use at your own risk!
  */
 
+
 #include "gqview.h"
 #include "image.h"
-#include "icons/img_unknown.xpm"
-#include <gdk/gdkx.h>
-
-static gchar *zoom_as_text(gint zoom, gfloat scale);
-static void set_zoom_label(GtkWidget *label, gint zoom, gfloat scale);
-static void set_info_label(GtkWidget *label, gint width, gint height, gint size, gint unknown);
-static void set_window_title(ImageWindow *imd, gchar *text);
-
-static gint image_area_size_top_window(ImageWindow *imd, gint w, gint h);
-
-static void image_area_recalc_size(ImageWindow *imd, GtkAllocation *allocation);
-
-static void image_area_redraw(ImageWindow *imd);
-static gint image_area_size_cb(GtkWidget *widget, GtkAllocation *allocation, gpointer data);
-static gint image_area_update_cb(GtkWidget *widget, GdkEventConfigure *event, gpointer data);
-
-static void set_mouse_cursor (GdkWindow *window, gint icon);
-static void image_area_mouse_moved(GtkWidget *widget, GdkEventButton *bevent, gpointer data);
-static void image_area_mouse_pressed(GtkWidget *widget, GdkEventButton *bevent, gpointer data);
-static void image_area_mouse_released(GtkWidget *widget, GdkEventButton *bevent, gpointer data);
-static void image_area_mouse_drag(GtkWidget *widget, GdkDragContext *context, gpointer data);
+
+
+#include "image-load.h"
+#include "collect.h"
+#include "exif.h"
+#include "pixbuf_util.h"
+#include "ui_fileops.h"
+
+#include <math.h>
+
+
+#define IMAGE_TILE_SIZE 128
+#define IMAGE_ZOOM_MIN -32.0
+#define IMAGE_ZOOM_MAX 32.0
+
+/* size of the image loader buffer (512 bytes x defined number) */
+#define IMAGE_LOAD_BUFFER_COUNT 8
+
+/* define this so that more bytes are read per idle loop on larger images (> 1MB) */
+#define IMAGE_THROTTLE_LARGER_IMAGES 1
+
+/* throttle factor to increase read bytes by (2 is double, 3 is triple, etc.) */
+#define IMAGE_THROTTLE_FACTOR 4
+
+/* the file size at which throttling take place */
+#define IMAGE_THROTTLE_THRESHOLD 1048576
+
+/* distance to drag mouse to disable image flip */
+#define IMAGE_DRAG_SCROLL_THRESHHOLD 4
+
+/* alpha channel checkerboard background (same as gimp) */
+#define IMAGE_ALPHA_CHECK1 0x00999999
+#define IMAGE_ALPHA_CHECK2 0x00666666
+#define IMAGE_ALPHA_CHECK_SIZE 16
+
+#define IMAGE_AUTO_REFRESH_TIME 3000
+
+/* when scaling image to below this size, use nearest pixel for scaling
+ * (below about 4, the other scale types become slow generating their conversion tables)
+ */
+#define IMAGE_MIN_SCALE_SIZE 8
+
+
+typedef enum {
+	TILE_RENDER_NONE = 0,	/* do nothing */
+	TILE_RENDER_AREA,	/* render an area of the tile */
+	TILE_RENDER_ALL		/* render the whole tile */
+} TileRenderType;
+
+typedef struct _ImageTile ImageTile;
+struct _ImageTile
+{
+	GdkPixmap *pixmap;	/* off screen buffer */
+	GdkPixbuf *pixbuf;	/* pixbuf area for zooming */
+	gint x;			/* x offset into image */
+	gint y;			/* y offset into image */
+	gint w;			/* width that is visible (may be less if at edge of image) */
+	gint h;			/* height '' */
+
+	gint blank;
+
+/* render_todo: (explanation)
+	NONE	do nothing
+	AREA	render area of tile, usually only used when loading an image
+		note: will jump to an ALL if render_done is not ALL.
+	ALL	render entire tile, if never done before w/ ALL, for expose events *only*
+*/
+
+	TileRenderType render_todo;	/* what to do (see above) */
+	TileRenderType render_done;	/* highest that has been done before on tile */
+};
+
+typedef struct _QueueData QueueData;
+struct _QueueData
+{
+	ImageTile *it;
+	gint x;
+	gint y;
+	gint w;
+	gint h;
+	gint new_data;
+};
+
+typedef struct _CacheData CacheData;
+struct _CacheData
+{
+	GdkPixmap *pixmap;
+	GdkPixbuf *pixbuf;
+	ImageTile *it;
+	guint size;
+};
+
+typedef struct _OverlayData OverlayData;
+struct _OverlayData
+{
+	gint id;
+
+	GdkPixbuf *pixbuf;
+
+	gint x;
+	gint y;
+	gint relative;	/* x,y coordinates are relative, negative values start bottom right */
+
+	gint visible;
+	gint always;	/* hide temporarily when scrolling */
+};
+
+
+static void image_queue_clear(ImageWindow *imd);
+
+static void image_update_title(ImageWindow *imd);
+static void image_update_util(ImageWindow *imd);
+static void image_complete_util(ImageWindow *imd, gint preload);
+
+static void image_button_do(ImageWindow *imd, GdkEventButton *bevent);
+
+static void image_overlay_draw(ImageWindow *imd, gint x, gint y, gint w, gint h);
+static void image_overlay_queue_all(ImageWindow *imd);
+
+static void image_scroller_timer_set(ImageWindow *imd, gint start);
+
+
+static gint util_clip_region(gint x, gint y, gint w, gint h,
+			     gint clip_x, gint clip_y, gint clip_w, gint clip_h,
+			     gint *rx, gint *ry, gint *rw, gint *rh)
+{
+	if (clip_x + clip_w <= x ||
+	    clip_x >= x + w ||
+	    clip_y + clip_h <= y ||
+	    clip_y >= y + h)
+		{
+		return FALSE;
+		}
+
+	*rx = MAX(x, clip_x);
+	*rw = MIN((x + w), (clip_x + clip_w)) - *rx;
+
+	*ry = MAX(y, clip_y);
+	*rh = MIN((y + h), (clip_y + clip_h)) - *ry;
+
+	return TRUE;
+}
+
 
 /*
- *-----------------------------------------------------------------------------
- * image status widget update routines (private)
- *-----------------------------------------------------------------------------
+ *-------------------------------------------------------------------
+ * tile cache
+ *-------------------------------------------------------------------
  */
 
-static gchar *zoom_as_text(gint zoom, gfloat scale)
+static gint pixmap_calc_size(GdkPixmap *pixmap)
+{
+	gint w, h, d;
+
+	d = gdk_drawable_get_depth(pixmap);
+	gdk_drawable_get_size(pixmap, &w, &h);
+	return w * h * (d / 8);
+}
+
+static void image_tile_cache_remove(ImageWindow *imd, ImageTile *it)
+{
+	GList *work;
+
+	work = imd->tile_cache;
+	while(work)
+		{
+		CacheData *cd = work->data;
+		work = work->next;
+
+		if (cd->it == it)
+			{
+			imd->tile_cache = g_list_remove(imd->tile_cache, cd);
+			imd->tile_cache_size -= cd->size;
+			g_free(cd);
+			}
+		}
+}
+
+static void image_tile_cache_free(ImageWindow *imd, CacheData *cd)
 {
-	gint l = 1;
-	gint r = 1;
-	gchar *approx = " ";
-	if (zoom > 1) l = zoom;
-	if (zoom < -1) r = -zoom;
-	if (zoom == 0 && scale != 0)
+	imd->tile_cache = g_list_remove(imd->tile_cache, cd);
+	if (cd->pixmap)
+		{
+		g_object_unref(cd->it->pixmap);
+		cd->it->pixmap = NULL;
+		cd->it->render_done = TILE_RENDER_NONE;
+		}
+	if (cd->pixbuf)
+		{
+		gdk_pixbuf_unref(cd->it->pixbuf);
+		cd->it->pixbuf = NULL;
+		}
+	imd->tile_cache_size -= cd->size;
+	g_free(cd);
+}
+
+static void image_tile_cache_free_space(ImageWindow *imd, gint space, ImageTile *it)
+{
+	GList *work = g_list_last(imd->tile_cache);
+
+	while (work && imd->tile_cache_size > 0 && imd->tile_cache_size + space > tile_cache_max * 1048576)
 		{
-		if (scale < 1) r = 1 / scale + 0.5;
-		approx = " ~";
+		CacheData *cd = work->data;
+		work = work->prev;
+		if (cd->it != it) image_tile_cache_free(imd, cd);
 		}
-	return g_strdup_printf("%d :%s%d", l, approx, r);
 }
 
-static void set_zoom_label(GtkWidget *label, gint zoom, gfloat scale)
+static void image_tile_cache_add(ImageWindow *imd, ImageTile *it,
+				 GdkPixmap *pixmap, GdkPixbuf *pixbuf, guint size)
+{
+	CacheData *cd;
+
+	cd = g_new(CacheData, 1);
+	cd->pixmap = pixmap;
+	cd->pixbuf = pixbuf;
+	cd->it = it;
+	cd->size = size;
+
+	imd->tile_cache = g_list_prepend(imd->tile_cache, cd);
+
+	imd->tile_cache_size += cd->size;
+}
+
+static void image_tile_prepare(ImageWindow *imd, ImageTile *it)
 {
-	gchar *buf;
-	buf = zoom_as_text(zoom, scale);
-	gtk_label_set(GTK_LABEL(label), buf);
-	g_free(buf);
+	if (!it->pixmap)
+		{
+		GdkPixmap *pixmap;
+		guint size;
+
+		pixmap = gdk_pixmap_new(imd->image->window, imd->tile_width, imd->tile_height, -1);
+
+		size = pixmap_calc_size(pixmap);
+		image_tile_cache_free_space(imd, size, it);
+
+		it->pixmap = pixmap;
+		image_tile_cache_add(imd, it, pixmap, NULL, size);
+		}
+	
+	if ((imd->zoom != 1.0 || gdk_pixbuf_get_has_alpha(imd->pixbuf)) &&
+	    !it->pixbuf)
+		{
+		GdkPixbuf *pixbuf;
+		guint size;
+
+		pixbuf = gdk_pixbuf_new(gdk_pixbuf_get_colorspace(imd->pixbuf),
+					gdk_pixbuf_get_has_alpha(imd->pixbuf),
+					gdk_pixbuf_get_bits_per_sample(imd->pixbuf),
+					imd->tile_width, imd->tile_height);
+
+		size = gdk_pixbuf_get_rowstride(pixbuf) * imd->tile_height;
+		image_tile_cache_free_space(imd, size, it);
+
+		it->pixbuf = pixbuf;
+		image_tile_cache_add(imd, it, NULL, pixbuf, size);
+		}
 }
 
-static void set_info_label(GtkWidget *label, gint width, gint height, gint size, gint unknown)
+/*
+ *-------------------------------------------------------------------
+ * tiles
+ *-------------------------------------------------------------------
+ */
+
+static ImageTile *image_tile_new(gint w, gint h)
 {
-	gchar buf[64];
-	if (unknown)
-		sprintf(buf, _("( ? x ? ) %d bytes"), size);
-	else
-		sprintf(buf, _("( %d x %d ) %d bytes"), width, height, size);
-	gtk_label_set(GTK_LABEL(label), buf);
+	ImageTile *it;
+
+	it = g_new0(ImageTile, 1);
+	it->w = w;
+	it->h = h;
+	it->pixmap = NULL;
+	it->pixbuf = NULL;
+	it->blank = TRUE;
+	it->render_todo = TILE_RENDER_NONE;
+	it->render_done = TILE_RENDER_NONE;
+
+	return it;
 }
 
-static void set_window_title(ImageWindow *imd, gchar *text)
+static void image_tile_free(ImageTile *it)
+{
+	if (!it) return;
+
+	if (it->pixbuf) gdk_pixbuf_unref(it->pixbuf);
+	if (it->pixmap) g_object_unref(it->pixmap);
+
+	g_free(it);
+}
+
+static void image_tile_sync_count(ImageWindow *imd, gint n)
 {
-	gchar *title = NULL;
-	if (!imd->top_window) return;
-
-	if (imd->title)
+	gint l;
+
+	l = g_list_length(imd->tiles);
+
+	if (l == n) return;
+
+	if (l < n)
 		{
-		title = g_strconcat(imd->title, imd->image_name, text, NULL);
+		while (l < n)
+			{
+			imd->tiles = g_list_prepend(imd->tiles, image_tile_new(imd->tile_width, imd->tile_height));
+			l++;
+			}
 		}
 	else
 		{
-		title = g_strconcat(imd->image_name, text, NULL);
+		/* This should remove from the tail of the GList, but with large images there are many tiles,
+		 * making this significantly faster for those cases.
+		 */
+		while (l > n && imd->tiles)
+			{
+			ImageTile *it = imd->tiles->data;
+			imd->tiles = g_list_remove(imd->tiles, it);
+			image_tile_cache_remove(imd, it);
+			image_tile_free(it);
+			l--;
+			}
+		}
+}
+
+static void image_tile_sync(ImageWindow *imd, gint width, gint height, gint blank)
+{
+	gint rows;
+	gint x, y;
+	GList *work;
+
+	imd->tile_cols = (width + imd->tile_width - 1) / imd->tile_width;
+
+	rows = (height + imd->tile_height - 1) / imd->tile_height;
+
+	image_tile_sync_count(imd, imd->tile_cols * rows);
+
+	x = y = 0;
+	work = imd->tiles;
+	while(work)
+		{
+		ImageTile *it = work->data;
+		work = work->next;
+
+		it->x = x;
+		it->y = y;
+		if (x + imd->tile_width > width)
+			{
+			it->w = width - x;
+			}	
+		else
+			{
+			it->w = imd->tile_width;
+			}
+		if (y + imd->tile_height > height)
+			{
+			it->h = height - y;
+			}
+		else
+			{
+			it->h = imd->tile_height;
+			}
+
+		it->blank = blank;
+		it->render_todo = TILE_RENDER_NONE;
+		it->render_done = TILE_RENDER_NONE;
+
+		x += imd->tile_width;
+		if (x >= width)
+			{
+			x = 0;
+			y += imd->tile_height;
+			}
+		}
+
+	/* all it's are now useless in queue */
+	image_queue_clear(imd);
+}
+
+static void image_tile_render(ImageWindow *imd, ImageTile *it,
+			      gint x, gint y, gint w, gint h,
+			      gint new_data, gint fast)
+{
+	gint has_alpha;
+	gint draw = FALSE;
+
+	if (it->render_todo == TILE_RENDER_NONE && it->pixmap) return;
+
+	if (it->render_done != TILE_RENDER_ALL)
+		{
+		x = 0;
+		y = 0;
+		w = it->w;
+		h = it->h;
+		if (!fast) it->render_done = TILE_RENDER_ALL;
+		}
+	else if (it->render_todo != TILE_RENDER_AREA)
+		{
+		if (!fast) it->render_todo = TILE_RENDER_NONE;
+		return;
 		}
 
-	gtk_window_set_title(GTK_WINDOW(imd->top_window), title);
-	g_free(title);
+	if (!fast) it->render_todo = TILE_RENDER_NONE;
+
+	if (new_data) it->blank = FALSE;
+
+	image_tile_prepare(imd, it);
+	has_alpha = gdk_pixbuf_get_has_alpha(imd->pixbuf);
+
+	/* FIXME checker colors for alpha should be configurable,
+	 * also should be drawn for blank = TRUE
+	 */
+
+	if (it->blank)
+		{
+		/* no data, do fast rect fill */
+		gdk_draw_rectangle(it->pixmap, imd->image->style->black_gc, TRUE,
+				   0, 0, it->w, it->h);
+		}
+	else if (imd->zoom == 1.0 || imd->scale == 1.0)
+		{
+		if (has_alpha)
+			{
+			gdk_pixbuf_composite_color(imd->pixbuf, it->pixbuf, x, y, w, h,
+					 (double) 0.0 - it->x,
+					 (double) 0.0 - it->y,
+					 1.0, 1.0, GDK_INTERP_NEAREST,
+					 255, it->x + x, it->y + y,
+					 IMAGE_ALPHA_CHECK_SIZE, IMAGE_ALPHA_CHECK1, IMAGE_ALPHA_CHECK2);
+			draw = TRUE;
+			}
+		else
+			{
+			/* faster, simple */
+			gdk_draw_pixbuf(it->pixmap,
+					imd->image->style->fg_gc[GTK_WIDGET_STATE(imd->image)],
+					imd->pixbuf,
+					it->x + x, it->y + y,
+					x, y,
+					w, h,
+					(GdkRgbDither)dither_quality, it->x + x, it->y + y);
+			}
+		}
+	else
+		{
+		double scale_x, scale_y;
+
+		if (imd->image_width == 0 || imd->image_height == 0) return;
+		scale_x = (double)imd->width / imd->image_width;
+		scale_y = (double)imd->height / imd->image_height;
+
+		/* HACK: The pixbuf scalers get kinda buggy(crash) with extremely
+		 * small sizes for anything but GDK_INTERP_NEAREST
+		 */
+		if (imd->width < IMAGE_MIN_SCALE_SIZE || imd->height < IMAGE_MIN_SCALE_SIZE) fast = TRUE;
+
+		if (!has_alpha)
+			{
+			gdk_pixbuf_scale(imd->pixbuf, it->pixbuf, x, y, w, h,
+					 (double) 0.0 - it->x,
+					 (double) 0.0 - it->y,
+					 scale_x, scale_y,
+					 (fast) ? GDK_INTERP_NEAREST : (GdkInterpType)zoom_quality);
+			}
+		else
+			{
+			gdk_pixbuf_composite_color(imd->pixbuf, it->pixbuf, x, y, w, h,
+					 (double) 0.0 - it->x,
+					 (double) 0.0 - it->y,
+					 scale_x, scale_y,
+					 (fast) ? GDK_INTERP_NEAREST : (GdkInterpType)zoom_quality,
+					 255, it->x + x, it->y + y,
+					 IMAGE_ALPHA_CHECK_SIZE, IMAGE_ALPHA_CHECK1, IMAGE_ALPHA_CHECK2);
+			}
+		draw = TRUE;
+		}
+
+	if (draw && it->pixbuf && !it->blank)
+		{
+		gdk_draw_pixbuf(it->pixmap,
+				imd->image->style->fg_gc[GTK_WIDGET_STATE(imd->image)],
+				it->pixbuf,
+				x, y,
+				x, y,
+				w, h,
+				(GdkRgbDither)dither_quality, it->x + x, it->y + y);
+		}
+}
+
+static void image_tile_expose(ImageWindow *imd, ImageTile *it,
+			      gint x, gint y, gint w, gint h,
+			      gint new_data, gint fast)
+{
+	image_tile_render(imd, it, x, y, w, h, new_data, fast);
+
+	gdk_draw_drawable(imd->image->window, imd->image->style->fg_gc[GTK_WIDGET_STATE(imd->image)],
+			  it->pixmap, x, y,
+			  imd->x_offset + (it->x - imd->x_scroll) + x, imd->y_offset + (it->y - imd->y_scroll) + y, w, h);
+
+	if (imd->overlay_list)
+		{
+		image_overlay_draw(imd, imd->x_offset + (it->x - imd->x_scroll) + x,
+					imd->y_offset + (it->y - imd->y_scroll) + y,
+					w, h);
+		}
+}
+
+static gint image_tile_is_visible(ImageWindow *imd, ImageTile *it)
+{
+	return (it->x + it->w >= imd->x_scroll && it->x <= imd->x_scroll + imd->window_width - imd->x_offset * 2 &&
+		it->y + it->h >= imd->y_scroll && it->y <= imd->y_scroll + imd->window_height - imd->y_offset * 2);
 }
 
 /*
- *-----------------------------------------------------------------------------
- * fit window to image utility (private)
- *-----------------------------------------------------------------------------
- */ 
-
-static gint image_area_size_top_window(ImageWindow *imd, gint w, gint h)
+ *-------------------------------------------------------------------
+ * render queue
+ *-------------------------------------------------------------------
+ */
+
+
+static gint image_queue_draw_idle_cb(gpointer data)
+{
+	ImageWindow *imd = data;
+	QueueData *qd;
+	gint fast;
+
+	if (!imd->pixbuf || (!imd->draw_queue && !imd->draw_queue_2pass) || imd->draw_idle_id == -1)
+		{
+		if (!imd->completed) image_complete_util(imd, FALSE);
+
+		imd->draw_idle_id = -1;
+		return FALSE;
+		}
+
+	if (imd->draw_queue)
+		{
+		qd = imd->draw_queue->data;
+		fast = (two_pass_zoom && (GdkInterpType)zoom_quality != GDK_INTERP_NEAREST && imd->scale != 1.0);
+		}
+	else
+		{
+		if (imd->il)
+			{
+			/* still loading, wait till done (also drops the higher priority) */
+
+			imd->draw_idle_id = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
+							    image_queue_draw_idle_cb, imd, NULL);
+			imd->draw_idle_high = FALSE;
+			return FALSE;
+			}
+		qd = imd->draw_queue_2pass->data;
+		fast = FALSE;
+		}
+
+	if (GTK_WIDGET_REALIZED(imd->image))
+		{
+		if (image_tile_is_visible(imd, qd->it))
+			{
+			image_tile_expose(imd, qd->it, qd->x, qd->y, qd->w, qd->h, qd->new_data, fast);
+			}
+		else if (qd->new_data)
+			{
+			/* if new pixel data, and we already have a pixmap, update the tile */
+			qd->it->blank = FALSE;
+			if (qd->it->pixmap && qd->it->render_done == TILE_RENDER_ALL)
+				{
+				image_tile_render(imd, qd->it, qd->x, qd->y, qd->w, qd->h, qd->new_data, fast);
+				}
+			}
+		}
+
+	if (imd->draw_queue)
+		{
+		imd->draw_queue = g_list_remove(imd->draw_queue, qd);
+		if (fast)
+			{
+			imd->draw_queue_2pass = g_list_append(imd->draw_queue_2pass, qd);
+			}
+		else
+			{
+			g_free(qd);
+			}
+		}
+	else
+		{
+		imd->draw_queue_2pass = g_list_remove(imd->draw_queue_2pass, qd);
+		g_free(qd);
+		}
+
+	if (!imd->draw_queue && !imd->draw_queue_2pass)
+		{
+		if (!imd->completed) image_complete_util(imd, FALSE);
+
+		imd->draw_idle_id = -1;
+		return FALSE;
+		}
+
+	return TRUE;
+}
+
+static QueueData *image_queue_combine(ImageWindow *imd, QueueData *qd)
+{
+	QueueData *found = NULL;
+	GList *work;
+
+	work = imd->draw_queue;
+	while (work && !found)
+		{
+		found = work->data;
+		work = work->next;
+
+		if (found->it != qd->it) found = NULL;
+		}
+
+	if (found)
+		{
+		if (found->x + found->w < qd->x + qd->w) found->w += (qd->x + qd->w) - (found->x + found->w);
+		if (found->x > qd->x)
+			{
+			found->w += found->x - qd->x;
+			found->x = qd->x;
+			}
+
+		if (found->y + found->h < qd->y + qd->h) found->h += (qd->y + qd->h) - (found->y + found->h);
+		if (found->y > qd->y)
+			{
+			found->h += found->y - qd->y;
+			found->y = qd->y;
+			}
+		found->new_data |= qd->new_data;
+		}
+
+	return found;
+}
+
+static gint image_clamp_to_visible(ImageWindow *imd, gint *x, gint *y, gint *w, gint *h)
+{
+	gint nx, ny;
+	gint nw, nh;
+	gint vx, vy;
+	gint vw, vh;
+
+	vw = imd->vis_width;
+	vh = imd->vis_height;
+
+	vx = imd->x_scroll;
+	vy = imd->y_scroll;
+
+	if (*x + *w < vx || *x > vx + vw || *y + *h < vy || *y > vy + vh) return FALSE;
+
+	/* now clamp it */
+	nx = CLAMP(*x, vx, vx + vw);
+	nw = CLAMP(*w - (nx - *x), 1, vw);
+
+	ny = CLAMP(*y, vy, vy + vh);
+	nh = CLAMP(*h - (ny - *y), 1, vh);
+
+	*x = nx;
+	*y = ny;
+	*w = nw;
+	*h = nh;
+
+	return TRUE;
+}
+
+static gint image_queue_to_tiles(ImageWindow *imd, gint x, gint y, gint w, gint h,
+				 gint clamp, TileRenderType render, gint new_data)
+{
+	gint i, j;
+	gint x1, x2;
+	gint y1, y2;
+	GList *work;
+
+	if (clamp && !image_clamp_to_visible(imd, &x, &y, &w, &h)) return FALSE;
+
+	x1 = (gint)floor(x / imd->tile_width) * imd->tile_width;
+	x2 = (gint)ceil((x + w) / imd->tile_width) * imd->tile_width;
+
+	y1 = (gint)floor(y / imd->tile_height) * imd->tile_height;
+	y2 = (gint)ceil((y + h) / imd->tile_height) * imd->tile_height;
+
+	work = g_list_nth(imd->tiles, y1 / imd->tile_height * imd->tile_cols + (x1 / imd->tile_width));
+	for (j = y1; j <= y2; j += imd->tile_height)
+		{
+		GList *tmp;
+		tmp = work;
+		for (i = x1; i <= x2; i += imd->tile_width)
+			{
+			if (tmp)
+				{
+				ImageTile *it = tmp->data;
+				QueueData *qd;
+
+				if ((render == TILE_RENDER_ALL && it->render_done != TILE_RENDER_ALL) ||
+				    (render == TILE_RENDER_AREA && it->render_todo != TILE_RENDER_ALL))
+					{
+					it->render_todo = render;
+					}
+
+				qd = g_new(QueueData, 1);
+				qd->it = it;
+				qd->new_data = new_data;
+
+				if (i < x)
+					{
+					qd->x = x - i;
+					}
+				else
+					{
+					qd->x = 0;
+					}
+				qd->w = x + w - i - qd->x;
+				if (qd->x + qd->w > imd->tile_width) qd->w = imd->tile_width - qd->x;
+
+
+				if (j < y)
+					{
+					qd->y = y - j;
+					}
+				else
+					{
+					qd->y = 0;
+					}
+				qd->h = y + h - j - qd->y;
+				if (qd->y + qd->h > imd->tile_height) qd->h = imd->tile_height - qd->y;
+
+				if (qd->w < 1 || qd->h < 1 || /* <--- sanity checks, rare cases cause this */
+				    image_queue_combine(imd, qd))
+					{
+					g_free(qd);
+					}
+				else
+					{
+					imd->draw_queue = g_list_append(imd->draw_queue, qd);
+					}
+
+				tmp = tmp->next;
+				}
+			}
+		work = g_list_nth(work, imd->tile_cols);	/* step 1 row */
+		}
+
+	return TRUE;
+}
+
+static void image_queue(ImageWindow *imd, gint x, gint y, gint w, gint h,
+			gint clamp, TileRenderType render, gint new_data)
+{
+	gint nx, ny;
+
+	nx = CLAMP(x, 0, imd->width - 1);
+	ny = CLAMP(y, 0, imd->height - 1);
+	w -= (nx - x);
+	h -= (ny - y);
+	w = CLAMP(w, 0, imd->width - nx);
+	h = CLAMP(h, 0, imd->height - ny);
+	if (w < 1 || h < 1) return;
+
+	if (image_queue_to_tiles(imd, nx, ny, w, h, clamp, render, new_data) &&
+	    ((!imd->draw_queue && !imd->draw_queue_2pass) || imd->draw_idle_id == -1 || !imd->draw_idle_high))
+		{
+		if (imd->draw_idle_id != -1) g_source_remove(imd->draw_idle_id);
+		imd->draw_idle_id = g_idle_add_full(GDK_PRIORITY_REDRAW,
+						    image_queue_draw_idle_cb, imd, NULL);
+		imd->draw_idle_high = TRUE;
+		}
+}
+
+static void image_queue_list_free(GList *list)
+{
+	GList *work;
+
+	work = list;
+	while (work)
+		{
+		QueueData *qd;
+
+		qd = work->data;
+		work = work->next;
+		g_free(qd);
+		}
+
+	g_list_free(list);
+}
+
+static void image_queue_clear(ImageWindow *imd)
+{
+	image_queue_list_free(imd->draw_queue);
+	imd->draw_queue = NULL;
+
+	image_queue_list_free(imd->draw_queue_2pass);
+	imd->draw_queue_2pass = NULL;
+
+	if (imd->draw_idle_id != -1) g_source_remove(imd->draw_idle_id);
+	imd->draw_idle_id = -1;
+}
+
+/*
+ *-------------------------------------------------------------------
+ * core calculations
+ *-------------------------------------------------------------------
+ */
+
+static gint image_top_window_sizable(ImageWindow *imd)
 {
 	if (!imd->top_window) return FALSE;
-	if (imd == full_screen_image) return FALSE;
-	if (imd == normal_image && !toolwindow) return FALSE;
 	if (!fit_window) return FALSE;
-
-	if (imd == normal_image)
-		{
-		/* account for border frame */
-		w += 4;
-		h += 4;
-		}
+	if (!imd->top_window_sync) return FALSE;
+	if (!imd->image->window) return FALSE;
+	if (window_maximized(imd->top_window)) return FALSE;
+
+	return TRUE;
+}
+
+static gint image_size_top_window(ImageWindow *imd, gint w, gint h)
+{
+	gint ww, wh;
+
+	if (!image_top_window_sizable(imd)) return FALSE;
 
 	if (limit_window_size)
 		{
@@ -116,407 +839,480 @@
 		if (h > sh) h = sh;
 		}
 
-	/* to cheat on a prob a little, don't resize if within 1 either way...
-	   ...dumb off by 1 errors! ;) */
-
-/*	if (w >= (imd->top_window)->allocation.width - 1 &&
-	    w <= (imd->top_window)->allocation.width + 1 &&
-	    h >= (imd->top_window)->allocation.height - 1 &&
-	    h <= (imd->top_window)->allocation.height + 1)
-		return FALSE;
-*/
-	if (debug) printf("auto sized to %d x %d\n", w, h);
+	w += (imd->top_window->allocation.width - imd->image->allocation.width);
+	h += (imd->top_window->allocation.height - imd->image->allocation.height);
+
+	gdk_drawable_get_size(imd->top_window->window, &ww, &wh);
+	if (w == ww && h == wh) return FALSE;
 
 	gdk_window_resize(imd->top_window->window, w, h);
-	gtk_widget_set_usize(imd->top_window, w, h);
 
 	return TRUE;
 }
 
-/*
- *-----------------------------------------------------------------------------
- * image widget zoom/recalc routines
- *-----------------------------------------------------------------------------
- */ 
-
-void image_area_scroll(ImageWindow *imd, gint x, gint y)
+static void image_redraw(ImageWindow *imd, gint new_data)
+{
+	image_queue_clear(imd);
+	image_queue(imd, 0, 0, imd->width, imd->height, TRUE, TILE_RENDER_ALL, new_data);
+}
+
+static void image_border_draw(ImageWindow *imd, gint x, gint y, gint w, gint h)
 {
-	if (x != 0)
+	gint rx, ry, rw, rh;
+
+	if (!imd->image->window) return;
+
+	if (!imd->pixbuf)
+		{
+		if (util_clip_region(x, y, w, h,
+			0, 0,
+			imd->window_width, imd->window_height,
+			&rx, &ry, &rw, &rh))
+			{
+			gdk_window_clear_area(imd->image->window, rx, ry, rw, rh);
+			image_overlay_draw(imd, rx, ry, rw, rh);
+			}
+		return;
+		}
+
+	if (imd->vis_width < imd->window_width)
 		{
-		GtkAdjustment *h = gtk_viewport_get_hadjustment(GTK_VIEWPORT(imd->viewport));
-		gfloat val = h->value + x;
-		if (val < h->lower) val = h->lower;
-		if (val > h->upper - h->page_size) val = h->upper - h->page_size;
-		gtk_adjustment_set_value(GTK_ADJUSTMENT(h), val);
+		if (imd->x_offset > 0 &&
+		    util_clip_region(x, y, w, h,
+			0, 0,
+			imd->x_offset, imd->window_height,
+			&rx, &ry, &rw, &rh))
+			{
+			gdk_window_clear_area(imd->image->window, rx, ry, rw, rh);
+			image_overlay_draw(imd, rx, ry, rw, rh);
+			}
+		if (imd->window_width - imd->vis_width - imd->x_offset > 0 &&
+		    util_clip_region(x, y, w, h,
+			imd->x_offset + imd->vis_width, 0,
+			imd->window_width - imd->vis_width - imd->x_offset, imd->window_height,
+			&rx, &ry, &rw, &rh))
+			{
+			gdk_window_clear_area(imd->image->window, rx, ry, rw, rh);
+			image_overlay_draw(imd, rx, ry, rw, rh);
+			}
 		}
-
-	if (y != 0)
+	if (imd->vis_height < imd->window_height)
 		{
-		GtkAdjustment *v = gtk_viewport_get_vadjustment(GTK_VIEWPORT(imd->viewport));
-		gfloat val = v->value + y;
-		if (val < v->lower) val = v->lower;
-		if (val > v->upper - v->page_size) val = v->upper - v->page_size;
-		gtk_adjustment_set_value(GTK_ADJUSTMENT(v), val);
+		if (imd->y_offset > 0 &&
+		    util_clip_region(x, y, w, h,
+			imd->x_offset, 0,
+			imd->vis_width, imd->y_offset,
+			&rx, &ry, &rw, &rh))
+			{
+			gdk_window_clear_area(imd->image->window, rx, ry, rw, rh);
+			image_overlay_draw(imd, rx, ry, rw, rh);
+			}
+		if (imd->window_height - imd->vis_height - imd->y_offset > 0 &&
+		    util_clip_region(x, y, w, h,
+			imd->x_offset, imd->y_offset + imd->vis_height,
+			imd->vis_width, imd->window_height - imd->vis_height - imd->y_offset,
+			&rx, &ry, &rw, &rh))
+			{
+			gdk_window_clear_area(imd->image->window, rx, ry, rw, rh);
+			image_overlay_draw(imd, rx, ry, rw, rh);
+			}
 		}
 }
 
-gint image_area_get_zoom(ImageWindow *imd)
+static void image_border_clear(ImageWindow *imd)
 {
-	return imd->zoom;
+	image_border_draw(imd, 0, 0, imd->window_width, imd->window_height);
 }
 
-void image_area_adjust_zoom(ImageWindow *imd, gint increment)
+static gint image_scroll_clamp(ImageWindow *imd)
 {
-	gint zoom = imd->zoom;
-	if (increment < 0)
+	gint old_xs;
+	gint old_ys;
+
+	if (imd->zoom == 0.0)
 		{
-		while (increment < 0)
-			{
-			zoom--;
-			if (zoom == 0 || zoom == -1) zoom = -2;
-			increment++;
-			}
-		if (zoom < -8) zoom = -8;
+		imd->x_scroll = 0;
+		imd->y_scroll = 0;
+		return FALSE;
+		}
+
+	old_xs = imd->x_scroll;
+	old_ys = imd->y_scroll;
+
+	if (imd->x_offset > 0)
+		{
+		imd->x_scroll = 0;
 		}
 	else
 		{
-		while (increment > 0)
-			{
-			zoom++;
-			if (zoom == -1) zoom = 1;
-			increment--;
-			}
-		if (zoom > 3) zoom = 3;
+		imd->x_scroll = CLAMP(imd->x_scroll, 0, imd->width - imd->vis_width);
 		}
-	if (zoom != imd->zoom)
-		image_area_set_zoom(imd, zoom);
-}
-
-void image_area_set_zoom(ImageWindow *imd, gint zoom)
-{
-	if (zoom == imd->zoom && imd->width > 0 && imd->height > 0) return;
-
-	imd->zoom = zoom;
-	image_area_recalc_size(imd, NULL);
-
-	gtk_widget_set_usize (imd->table, imd->width, imd->height);
-	gtk_drawing_area_size(GTK_DRAWING_AREA(imd->image), imd->width, imd->height);
-}
-
-static void image_area_recalc_size(ImageWindow *imd, GtkAllocation *allocation)
-{
-	gint w, h, ww, wh;
-	gfloat scale_factor = 1;
-
-	w = imd->image_data->rgb_width;
-	h = imd->image_data->rgb_height;
-	if (allocation)
+
+	if (imd->y_offset > 0)
 		{
-		ww = allocation->width;
-        	wh = allocation->height;
+		imd->y_scroll = 0;
 		}
 	else
 		{
-		ww = (imd->eventbox)->allocation.width;
-        	wh = (imd->eventbox)->allocation.height;
+		imd->y_scroll = CLAMP(imd->y_scroll, 0, imd->height - imd->vis_height);
 		}
 
-	if (imd == normal_image)
+	return (old_xs != imd->x_scroll || old_ys != imd->y_scroll);
+}
+
+static gint image_zoom_clamp(ImageWindow *imd, gdouble zoom, gint force, gint new)
+{
+	gint w, h;
+	gdouble scale;
+
+	zoom = CLAMP(zoom, IMAGE_ZOOM_MIN, IMAGE_ZOOM_MAX);
+
+	if (imd->zoom == zoom && !force) return FALSE;
+
+	w = imd->image_width;
+	h = imd->image_height;
+
+	if (zoom == 0.0 && !imd->pixbuf)
 		{
-		/* account for frame */
-		ww -= 4;
-		wh -= 4;
+		scale = 1.0;
 		}
-
-	if (imd->zoom == 0) /* zoom to fit */
+	else if (zoom == 0.0)
 		{
-		if (imd == normal_image && imd->width == 0 && imd->height == 0 &&
-		    fit_window && toolwindow)
+		gint max_w;
+		gint max_h;
+		gint sizeable;
+
+		sizeable = (new && image_top_window_sizable(imd));
+
+		if (sizeable)
 			{
+			max_w = gdk_screen_width();
+			max_h = gdk_screen_height();
+
 			if (limit_window_size)
 				{
-				ww = (gdk_screen_width() * max_window_size / 100) - 4;
-				wh = (gdk_screen_height() * max_window_size / 100) - 4;
+				max_w = max_w * max_window_size / 100;
+				max_h = max_h * max_window_size / 100;
+				}
+			}
+		else
+			{
+			max_w = imd->window_width;
+			max_h = imd->window_height;
+			}
+
+		if ((zoom_to_fit_expands && !sizeable) || w > max_w || h > max_h)
+			{
+			if ((gdouble)max_w / w > (gdouble)max_h / h)
+				{
+				scale = (gdouble)max_h / h;
+				h = max_h;
+				w = w * scale + 0.5;
+				if (w > max_w) w = max_w;
 				}
 			else
 				{
-				ww = w;
-				wh = h;
-				}
-			}
-		if (w > ww || h > wh)
-			{
-			if ((gfloat)ww / w > (gfloat)wh / h)
-				{
-				scale_factor = (gfloat) wh / h;
-				h = wh;
-				w = w * scale_factor + 0.5;
-				if (w > ww) w = ww;
-				}
-			else
-				{
-				scale_factor = (gfloat)ww / w;
-				w = ww;
-				h = h * scale_factor + 0.5;
-				if (h > wh) h = wh;
+				scale = (gdouble)max_w / w;
+				w = max_w;
+				h = h * scale + 0.5;
+				if (h > max_h) h = max_h;
 				}
 			if (w < 1) w = 1;
 			if (h < 1) h = 1;
 			}
-		}
-	else if (imd->zoom > 0) /* zoom orig, in */
-		{
-		scale_factor = imd->zoom;
-		w = w * scale_factor;
-		h = h * scale_factor;
+		else
+			{
+			scale = 1.0;
+			}
 		}
-	else if (imd->zoom < -1) /* zoom out */
+	else if (zoom > 0.0) /* zoom orig, in */
 		{
-		scale_factor = (- imd->zoom);
-		w = w / scale_factor;
-		h = h / scale_factor;
+		scale = zoom;
+		w = w * scale;
+		h = h * scale;
 		}
-
+	else /* zoom out */
+		{
+		scale = 1.0 / (0.0 - zoom);
+		w = w * scale;
+		h = h * scale;
+		}
+
+	imd->zoom = zoom;
 	imd->width = w;
 	imd->height = h;
-
-	if (debug) printf("recalc %d x %d @ %f\n", w, h, scale_factor);
-
-	if (imd->zoom_label)
-		{
-		set_zoom_label(imd->zoom_label, imd->zoom, scale_factor);
-		}
-
-/* this is causing problems with resizing
-	if (imd->top_window && imd->show_title_zoom)
-		{
-		gchar *buf = zoom_as_text(imd->zoom, scale_factor);
-		gchar *zbuf = g_strconcat(" [ ", buf, "]", NULL);
-		g_free(buf);
-		set_window_title(imd, zbuf);
-		g_free(zbuf);
-		}
-*/
-
-	if (image_area_size_top_window(imd, w, h))
-		{
-		/* this is hacky */
-		imd->artificial_size = TRUE;
-		gtk_grab_add (info_zoom);
-		while(gtk_events_pending()) gtk_main_iteration();
-		gtk_grab_remove(info_zoom);
-		imd->artificial_size = FALSE;
-		}
+	imd->scale = scale;
+
+	return TRUE;
 }
 
-/*
- *-----------------------------------------------------------------------------
- * image widget set/get image information
- *-----------------------------------------------------------------------------
- */ 
-
-void image_area_set_path(ImageWindow *imd, gchar *newpath)
+static gint image_size_clamp(ImageWindow *imd)
 {
-	if (!imd->image_path || !newpath) return;
-
-	g_free(imd->image_path);
-	imd->image_path = g_strdup(newpath);
-	imd->image_name = filename_from_path(imd->image_path);
-
-	if (imd->top_window)
+	gint old_vw, old_vh;
+
+	old_vw = imd->vis_width;
+	old_vh = imd->vis_height;
+
+	if (imd->width < imd->window_width)
 		{
-		set_window_title(imd, NULL);
-		}
-}
-
-gchar *image_area_get_path(ImageWindow *imd)
-{
-	return imd->image_path;
-}
-
-gchar *image_area_get_name(ImageWindow *imd)
-{
-	return imd->image_name;
-}
-
-void image_area_set_image(ImageWindow *imd, gchar *path, gint zoom)
-{
-	if (path && imd->image_path && !strcmp(path, imd->image_path)) return;
-
-	g_free(imd->image_path);
-	if (path)
-		{
-		imd->image_path = g_strdup(path);
-		imd->image_name = filename_from_path(imd->image_path);
+		imd->vis_width = imd->width;
+		imd->x_offset = (imd->window_width - imd->width) / 2;
 		}
 	else
 		{
-		imd->image_path = NULL;
-		imd->image_name = " ";
-		zoom = 1;
+		imd->vis_width = imd->window_width;
+		imd->x_offset = 0;
 		}
 
-	if (imd->image_data) gdk_imlib_destroy_image(imd->image_data);
-	if (path && isfile(path))
+	if (imd->height < imd->window_height)
 		{
-		imd->image_data = gdk_imlib_load_image(path);
-		if (!imd->image_data)
-			{
-			imd->image_data = gdk_imlib_create_image_from_xpm_data((gchar **)img_unknown_xpm);
-			imd->unknown = TRUE;
-			}
-		else
-			{
-			imd->unknown = FALSE;
-			}
-		imd->size = filesize(path);
+		imd->vis_height = imd->height;
+		imd->y_offset = (imd->window_height - imd->height) / 2;
 		}
 	else
 		{
-		if (path)
-			imd->image_data = gdk_imlib_create_image_from_xpm_data((gchar **)img_unknown_xpm);
-		else
-			imd->image_data = gdk_imlib_create_image_from_data((char *)logo, NULL, logo_width, logo_height);
-		imd->unknown = TRUE;
-		imd->size = 0;
+		imd->vis_height = imd->window_height;
+		imd->y_offset = 0;
 		}
 
-	imd->width = imd->old_width = 0;
-	imd->height = imd->old_height = 0;
-
-	if (imd->top_window)
+	return (old_vw != imd->vis_width || old_vh != imd->vis_height);
+}
+
+static void image_size_sync(ImageWindow *imd, gint new_width, gint new_height)
+{
+	if (imd->window_width == new_width && imd->window_height == new_height) return;
+
+	imd->window_width = new_width;
+	imd->window_height = new_height;
+
+	if (imd->zoom == 0.0) image_zoom_clamp(imd, 0.0, TRUE, FALSE);
+
+	image_size_clamp(imd);
+	image_scroll_clamp(imd);
+
+	gtk_widget_set_size_request(imd->image, imd->window_width, imd->window_height);
+
+	/* ensure scroller remains visible */
+	if (imd->scroller_overlay != -1)
 		{
-		set_window_title(imd, NULL);
+		gint update = FALSE;
+
+		if (imd->scroller_x > new_width)
+			{
+			imd->scroller_x = new_width;
+			imd->scroller_xpos = new_width;
+			update = TRUE;
+			}
+		if (imd->scroller_y > new_height)
+			{
+			imd->scroller_y = new_height;
+			imd->scroller_ypos = new_height;
+			update = TRUE;
+			}
+
+		if (update)
+			{
+			GdkPixbuf *pixbuf;
+
+			if (image_overlay_get(imd, imd->scroller_overlay, &pixbuf, NULL, NULL))
+				{
+				gint w, h;
+
+				w = gdk_pixbuf_get_width(pixbuf);
+				h = gdk_pixbuf_get_height(pixbuf);
+				image_overlay_set(imd, imd->scroller_overlay, pixbuf,
+						  imd->scroller_x - w / 2, imd->scroller_y - h / 2);
+				}
+			}
 		}
-	if (imd->info_label)
-		{
-		set_info_label(imd->info_label, imd->image_data->rgb_width, imd->image_data->rgb_height, imd->size, imd->unknown);
-		}
-
-	/* do info area updates here */
-
-	imd->new_img = TRUE;
-	image_area_set_zoom(imd, zoom);
+
+	/* clear any borders */
+	image_border_clear(imd);
+	
+	image_tile_sync(imd, imd->width, imd->height, FALSE);
+#if 0
+	/* no longer needed? (expose event should be doing this for us) */
+	image_redraw(imd, FALSE);
+#endif
+
+	if (imd->title_show_zoom) image_update_title(imd);
+	image_update_util(imd);
 }
 
 /*
- *-----------------------------------------------------------------------------
- * image widget redraw/callbacks (private)
- *-----------------------------------------------------------------------------
- */ 
-
-static void image_area_redraw(ImageWindow *imd)
+ *-------------------------------------------------------------------
+ * misc
+ *-------------------------------------------------------------------
+ */
+
+static void image_update_title(ImageWindow *imd)
 {
-	GdkBitmap *mask = NULL;
-
-	if (debug) printf("redrawn %d x %d\n", imd->width, imd->height);
-
-	if (!imd->image_data) return;
-
-	if (imd->width == imd->old_width && imd->height == imd->old_height)
+	gchar *title = NULL;
+	gchar *zoom = NULL;
+	gchar *collection = NULL;
+
+	if (!imd->top_window) return;
+
+	if (imd->collection && collection_to_number(imd->collection) >= 0)
+		{
+		const gchar *name;
+		name = imd->collection->name;
+		if (!name) name = _("Untitled");
+		collection = g_strdup_printf(" (Collection %s)", name);
+		}
+
+	if (imd->title_show_zoom)
 		{
-		if (debug) printf("redraw cancelled\n");
-		return;
+		gchar *buf = image_zoom_get_as_text(imd);
+		zoom = g_strconcat(" [", buf, "]", NULL);
+		g_free(buf);
 		}
 
-	if (imd->image_pixmap) gdk_imlib_free_pixmap(imd->image_pixmap);
-	imd->image_pixmap = NULL;
-
-	gdk_imlib_render(imd->image_data, imd->width, imd->height);
-	imd->image_pixmap = gdk_imlib_move_image(imd->image_data);
-	mask = gdk_imlib_move_mask(imd->image_data);
-
-	gdk_window_set_back_pixmap(imd->image->window, imd->image_pixmap, FALSE);
-	gdk_window_shape_combine_mask (imd->image->window, mask, 0, 0);
-	gdk_window_clear(imd->image->window);
-	gdk_flush();
-
-	imd->old_width = imd->width;
-	imd->old_height = imd->height;
+	title = g_strdup_printf("%s%s%s%s%s%s",
+		imd->title ? imd->title : "",
+		imd->image_name ? imd->image_name : "",
+		zoom ? zoom : "",
+		collection ? collection : "",
+		imd->image_name ? " - " : "",
+		imd->title_right ? imd->title_right : "");
+
+	gtk_window_set_title(GTK_WINDOW(imd->top_window), title);
+
+	g_free(title);
+	g_free(zoom);
+	g_free(collection);
+}
+
+static void image_update_util(ImageWindow *imd)
+{
+	if (imd->func_update) imd->func_update(imd, imd->data_update);
 }
 
-static gint image_area_size_cb(GtkWidget *widget, GtkAllocation *allocation, gpointer data)
+static void image_complete_util(ImageWindow *imd, gint preload)
+{
+	if (imd->il && imd->pixbuf != image_loader_get_pixbuf(imd->il)) return;
+
+	if (debug) printf("image load completed \"%s\" (%s)\n",
+			  (preload) ? imd->read_ahead_path : imd->image_path,
+			  (preload) ? "preload" : "current");
+
+	if (!preload) imd->completed = TRUE;
+	if (imd->func_complete) imd->func_complete(imd, preload, imd->data_complete);
+}
+
+static void image_new_util(ImageWindow *imd)
+{
+	if (imd->func_new) imd->func_new(imd, imd->data_new);
+}
+
+static void image_scroll_real(ImageWindow *imd, gint x, gint y)
 {
-	ImageWindow *imd = data;
-	gint old_w, old_h;
-	GtkAdjustment *h;
-	GtkAdjustment *v;
-	gfloat h_pos;
-	gfloat v_pos;
-	gfloat h_max;
-	gfloat v_max;
-
-	if (imd->artificial_size) return FALSE;
-
-	h = gtk_viewport_get_hadjustment(GTK_VIEWPORT(imd->viewport));
-	v = gtk_viewport_get_vadjustment(GTK_VIEWPORT(imd->viewport));
-
-	h_pos = h->value;
-	h_max = allocation->width;
-	v_pos = v->value;
-	v_max = allocation->height;
-
-	if (imd == normal_image)
+	gint old_x, old_y;
+	gint x_off, y_off;
+	gint w, h;
+
+	if (!imd->pixbuf) return;
+
+	old_x = imd->x_scroll;
+	old_y = imd->y_scroll;
+
+	imd->x_scroll += x;
+	imd->y_scroll += y;
+
+	image_scroll_clamp(imd);
+	if (imd->x_scroll == old_x && imd->y_scroll == old_y) return;
+
+	if (imd->overlay_list)
 		{
-		h_max -= 4.0;
-		v_max -= 4.0;
+		gint new_x, new_y;
+
+		new_x = imd->x_scroll;
+		new_y = imd->y_scroll;
+		imd->x_scroll = old_x;
+		imd->y_scroll = old_y;
+		image_overlay_queue_all(imd);
+		imd->x_scroll = new_x;
+		imd->y_scroll = new_y;
 		}
 
-	if (h_pos > h->upper - h_max) h_pos = h->upper - h_max;
-	if (v_pos > v->upper - v_max) v_pos = v->upper - v_max;
-
-	if (imd->new_img)
+	x_off = imd->x_scroll - old_x;
+	y_off = imd->y_scroll - old_y;
+
+	w = imd->vis_width - abs(x_off);
+	h = imd->vis_height - abs(y_off);
+
+	if (w < 1 || h < 1)
 		{
-		imd->new_img = FALSE;
-		gtk_adjustment_clamp_page(h, 0.0, h_max);
-	        gtk_adjustment_clamp_page(v, 0.0, v_max);
+		/* scrolled completely to new material */
+		image_queue(imd, 0, 0, imd->width, imd->height, TRUE, TILE_RENDER_ALL, FALSE);
+		return;
 		}
 	else
 		{
-		gtk_adjustment_clamp_page(h, h_pos, h_max);
-	        gtk_adjustment_clamp_page(v, v_pos, v_max);
-		}
-
-        gtk_adjustment_changed(h);
-        gtk_adjustment_changed(v);
-
-	if (!imd->image_data || imd->zoom != 0) return FALSE;
-
-	old_w = imd->width;
-	old_h = imd->height;
-	image_area_recalc_size(imd, allocation);
-	if (old_w != imd->width || old_h != imd->height)
-		{
-		gtk_widget_set_usize (imd->table, imd->width, imd->height);
-		gtk_drawing_area_size(GTK_DRAWING_AREA(imd->image), imd->width, imd->height);
+		gint x1, y1;
+		gint x2, y2;
+		GdkGC *gc;
+
+		if (x_off < 0)
+			{
+			x1 = abs(x_off);
+			x2 = 0;
+			}
+		else
+			{
+			x1 = 0;
+			x2 = abs(x_off);
+			}
+
+		if (y_off < 0)
+			{
+			y1 = abs(y_off);
+			y2 = 0;
+			}
+		else
+			{
+			y1 = 0;
+			y2 = abs(y_off);
+			}
+
+		gc = gdk_gc_new(imd->image->window);
+		gdk_gc_set_exposures(gc, TRUE);
+		gdk_draw_drawable(imd->image->window, gc,
+				  imd->image->window,
+				  x2 + imd->x_offset, y2 + imd->y_offset,
+				  x1 + imd->x_offset, y1 + imd->y_offset, w, h);
+		g_object_unref(gc);
+
+		if (imd->overlay_list)
+			{
+			image_overlay_queue_all(imd);
+			}
+
+		w = imd->vis_width - w;
+		h = imd->vis_height - h;
+
+		if (w > 0)
+			{
+			image_queue(imd,
+				    x_off > 0 ? imd->x_scroll + (imd->vis_width - w) : imd->x_scroll, imd->y_scroll,
+				    w, imd->vis_height, TRUE, TILE_RENDER_ALL, FALSE);
+			}
+		if (h > 0)
+			{
+			/* FIXME, to optimize this, remove overlap */
+			image_queue(imd,
+				    imd->x_scroll, y_off > 0 ? imd->y_scroll + (imd->vis_height - h) : imd->y_scroll,
+				    imd->vis_width, h, TRUE, TILE_RENDER_ALL, FALSE);
+			}
 		}
-
-	if (debug) printf("sized %d x %d (%d x %d)\n", allocation->width, allocation->height, imd->width, imd->height);
-
-	return FALSE;
 }
 
-static gint image_area_update_cb(GtkWidget *widget, GdkEventConfigure *event, gpointer data)
-{
-	ImageWindow *imd = data;
-
-	if (imd->artificial_size) return FALSE;
-
-	image_area_redraw(imd);
-
-	return FALSE;
-}
-
-/*
- *-----------------------------------------------------------------------------
- * image widget mouse routines (private)
- *-----------------------------------------------------------------------------
- */ 
-
-static void set_mouse_cursor (GdkWindow *window, gint icon)
+static void widget_set_cursor(GtkWidget *widget, gint icon)
 {
 	GdkCursor *cursor;
 
+	if (!widget->window) return;
+
 	if (icon == -1)
 		{
 		cursor = NULL;
@@ -526,66 +1322,1208 @@
 		cursor = gdk_cursor_new (icon);
 		}
 
-	gdk_window_set_cursor (window, cursor);
-
-	if (cursor) gdk_cursor_destroy (cursor);
+	gdk_window_set_cursor(widget->window, cursor);
+
+	if (cursor) gdk_cursor_unref(cursor);
+}
+
+/*
+ *-------------------------------------------------------------------
+ * image pixbuf handling
+ *-------------------------------------------------------------------
+ */
+
+static void image_zoom_sync(ImageWindow *imd, gdouble zoom,
+			    gint force, gint blank, gint new,
+			    gint center_point, gint px, gint py)
+{
+	gdouble old_scale;
+	gint old_cx, old_cy;
+	gint clamped;
+	gint sized;
+
+	old_scale = imd->scale;
+	if (center_point)
+		{
+		px = CLAMP(px, 0, imd->width);
+		py = CLAMP(py, 0, imd->height);
+		old_cx = imd->x_scroll + (px - imd->x_offset);
+		old_cy = imd->y_scroll + (py - imd->y_offset);
+		}
+	else
+		{
+		px = py = 0;
+		old_cx = imd->x_scroll + imd->vis_width / 2;
+		old_cy = imd->y_scroll + imd->vis_height / 2;
+		}
+
+	if (!image_zoom_clamp(imd, zoom, force, new)) return;
+
+	clamped = image_size_clamp(imd);
+	sized = image_size_top_window(imd, imd->width, imd->height);
+
+	if (force)
+		{
+		/* force means new image, so update scroll offset per options */
+		switch (scroll_reset_method)
+			{
+			case SCROLL_RESET_NOCHANGE:
+				/* maintain old scroll position, do nothing */
+				break;
+			case SCROLL_RESET_CENTER:
+				/* center new image */
+				imd->x_scroll = ((double)imd->image_width / 2.0 * imd->scale) - imd->vis_width / 2;
+				imd->y_scroll = ((double)imd->image_height / 2.0 * imd->scale) - imd->vis_height / 2;
+				break;
+			case SCROLL_RESET_TOPLEFT:
+			default:
+				/* reset to upper left */
+				imd->x_scroll = 0;
+				imd->y_scroll = 0;
+				break;
+			}
+		}
+	else
+		{
+		/* user zoom does not force, so keep visible center point */
+		if (center_point)
+			{
+			imd->x_scroll = old_cx / old_scale * imd->scale - (px - imd->x_offset);
+			imd->y_scroll = old_cy / old_scale * imd->scale - (py - imd->y_offset);
+			}
+		else
+			{
+			imd->x_scroll = old_cx / old_scale * imd->scale - (imd->vis_width / 2);
+			imd->y_scroll = old_cy / old_scale * imd->scale - (imd->vis_height / 2);
+			}
+		}
+	image_scroll_clamp(imd);
+
+	image_tile_sync(imd, imd->width, imd->height, blank);
+
+	/* If the window was not sized, redraw the image - we know there will be no size/expose signal.
+	 * But even if a size is claimed, there is no guarantee that the window manager will allow it,
+	 * so redraw the window anyway :/
+	 */
+	if (sized || clamped) image_border_clear(imd);
+	image_redraw(imd, FALSE);
+
+	if (imd->title_show_zoom) image_update_title(imd);
+	image_update_util(imd);
+}
+
+static void image_pixbuf_sync(ImageWindow *imd, gdouble zoom, gint blank, gint new)
+{
+	if (!imd->pixbuf)
+		{
+		/* no pixbuf so just clear the window */
+		imd->image_width = 0;
+		imd->image_height = 0;
+		imd->scale = 1.0;
+
+		if (imd->image->window)
+			{
+			gdk_window_clear(imd->image->window);
+			image_overlay_draw(imd, 0, 0, imd->window_width, imd->window_height);
+			}
+
+		image_update_util(imd);
+		
+		return;
+		}
+
+	imd->image_width = gdk_pixbuf_get_width(imd->pixbuf);
+	imd->image_height = gdk_pixbuf_get_height(imd->pixbuf);
+
+#if 0
+	/* reset scrolling */
+	imd->x_scroll = 0;
+	imd->y_scroll = 0;
+#endif
+
+	image_zoom_sync(imd, zoom, TRUE, blank, new, FALSE, 0, 0);
+}
+
+static void image_set_pixbuf(ImageWindow *imd, GdkPixbuf *pixbuf, gdouble zoom, gint new)
+{
+	if (pixbuf) g_object_ref(pixbuf);
+	if (imd->pixbuf) g_object_unref(imd->pixbuf);
+	imd->pixbuf = pixbuf;
+
+	image_pixbuf_sync(imd, zoom, FALSE, new);
+}
+
+static void image_alter_real(ImageWindow *imd, AlterType type, gint clamp)
+{
+	GdkPixbuf *new = NULL;
+	gint x, y;
+	gint t;
+
+	imd->delay_alter_type = ALTER_NONE;
+
+	if (!imd->pixbuf) return;
+
+	x = imd->x_scroll + (imd->vis_width / 2);
+	y = imd->y_scroll + (imd->vis_height / 2);
+
+	switch (type)
+		{
+		case ALTER_ROTATE_90:
+			new = pixbuf_copy_rotate_90(imd->pixbuf, FALSE);
+			t = x;
+			x = imd->height - y;
+			y = t;
+			break;
+		case ALTER_ROTATE_90_CC:
+			new = pixbuf_copy_rotate_90(imd->pixbuf, TRUE);
+			t = x;
+			x = y;
+			y = imd->width - t;
+			break;
+		case ALTER_ROTATE_180:
+			new = pixbuf_copy_mirror(imd->pixbuf, TRUE, TRUE);
+			x = imd->width - x;
+			y = imd->height - y;
+			break;
+		case ALTER_MIRROR:
+			new = pixbuf_copy_mirror(imd->pixbuf, TRUE, FALSE);
+			x = imd->width - x;
+			break;
+		case ALTER_FLIP:
+			new = pixbuf_copy_mirror(imd->pixbuf, FALSE, TRUE);
+			y = imd->height - y;
+			break;
+		case ALTER_NONE:
+		default:
+			return;
+			break;
+		}
+
+	if (!new) return;
+
+	if (clamp)
+		{
+		image_set_pixbuf(imd, new, imd->zoom, TRUE);
+		g_object_unref(new);
+
+		if (imd->zoom != 0.0)
+			{
+			image_scroll(imd, x - (imd->vis_width / 2), y - (imd->vis_height / 2));
+			}
+		}
+	else
+		{
+		g_object_unref(imd->pixbuf);
+		imd->pixbuf = new;
+		}
 }
 
-static void image_area_mouse_moved(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
+static void image_post_process(ImageWindow *imd, gint clamp)
+{
+	if (exif_rotate_enable && imd->pixbuf)
+		{
+		ExifData *ed;
+		gint orientation;
+
+		ed = exif_read(imd->image_path);
+		if (ed && exif_get_integer(ed, "Orientation", &orientation))
+			{
+			/* see http://jpegclub.org/exif_orientation.html 
+			  1        2       3      4         5            6           7          8
+
+			888888  888888      88  88      8888888888  88                  88  8888888888
+			88          88      88  88      88  88      88  88          88  88      88  88
+			8888      8888    8888  8888    88          8888888888  8888888888          88
+			88          88      88  88
+			88          88  888888  888888
+			*/
+
+			switch (orientation)
+				{
+				case EXIF_ORIENTATION_TOP_LEFT:
+					/* normal -- nothing to do */
+					break;
+				case EXIF_ORIENTATION_TOP_RIGHT:
+					/* mirrored */
+					imd->delay_alter_type = ALTER_MIRROR;
+					break;
+				case EXIF_ORIENTATION_BOTTOM_RIGHT:
+					/* upside down */
+					imd->delay_alter_type = ALTER_ROTATE_180;
+					break;
+				case EXIF_ORIENTATION_BOTTOM_LEFT:
+					/* flipped */
+					imd->delay_alter_type = ALTER_FLIP;
+					break;
+				case EXIF_ORIENTATION_LEFT_TOP:
+					/* not implemented -- too wacky to fix in one step */
+					break;
+				case EXIF_ORIENTATION_RIGHT_TOP:
+					/* rotated -90 (270) */
+					imd->delay_alter_type = ALTER_ROTATE_90;
+					break;
+				case EXIF_ORIENTATION_RIGHT_BOTTOM:
+					/* not implemented -- too wacky to fix in one step */
+					break;
+				case EXIF_ORIENTATION_LEFT_BOTTOM:
+					/* rotated 90 */
+					imd->delay_alter_type = ALTER_ROTATE_90_CC;
+					break;
+				default:
+					/* The other values are out of range */
+					break;
+				}
+			}
+		exif_free(ed);
+		}
+
+	if (imd->delay_alter_type != ALTER_NONE)
+		{
+		image_alter_real(imd, imd->delay_alter_type, clamp);
+		}
+}
+
+/*
+ *-------------------------------------------------------------------
+ * read ahead (prebuffer)
+ *-------------------------------------------------------------------
+ */
+
+static void image_read_ahead_cancel(ImageWindow *imd)
+{
+	if (debug) printf("read ahead cancelled for :%s\n", imd->read_ahead_path);
+
+	image_loader_free(imd->read_ahead_il);
+	imd->read_ahead_il = NULL;
+
+	if (imd->read_ahead_pixbuf) g_object_unref(imd->read_ahead_pixbuf);
+	imd->read_ahead_pixbuf = NULL;
+
+	g_free(imd->read_ahead_path);
+	imd->read_ahead_path = NULL;
+}
+
+static void image_read_ahead_done_cb(ImageLoader *il, gpointer data)
+{
+	ImageWindow *imd = data;
+
+	if (debug) printf("read ahead done for :%s\n", imd->read_ahead_path);
+
+	imd->read_ahead_pixbuf = image_loader_get_pixbuf(imd->read_ahead_il);
+	if (imd->read_ahead_pixbuf)
+		{
+		g_object_ref(imd->read_ahead_pixbuf);
+		}
+	else
+		{
+		imd->read_ahead_pixbuf = pixbuf_inline(PIXBUF_INLINE_BROKEN);
+		}
+	image_loader_free(imd->read_ahead_il);
+	imd->read_ahead_il = NULL;
+
+	image_complete_util(imd, TRUE);
+}
+
+static void image_read_ahead_error_cb(ImageLoader *il, gpointer data)
+{
+	/* we even treat errors as success, maybe at least some of the file was ok */
+	image_read_ahead_done_cb(il, data);
+}
+
+static void image_read_ahead_start(ImageWindow *imd)
+{
+	/* already started ? */
+	if (!imd->read_ahead_path || imd->read_ahead_il || imd->read_ahead_pixbuf) return;
+
+	/* still loading ?, do later */
+	if (imd->il) return;
+
+	if (debug) printf("read ahead started for :%s\n", imd->read_ahead_path);
+
+	imd->read_ahead_il = image_loader_new(imd->read_ahead_path);
+
+	image_loader_set_error_func(imd->read_ahead_il, image_read_ahead_error_cb, imd);
+	if (!image_loader_start(imd->read_ahead_il, image_read_ahead_done_cb, imd))
+		{
+		image_read_ahead_cancel(imd);
+		image_complete_util(imd, TRUE);
+		}
+}
+
+static void image_read_ahead_set(ImageWindow *imd, const gchar *path)
+{
+	if (imd->read_ahead_path && path && strcmp(imd->read_ahead_path, path) == 0) return;
+
+	image_read_ahead_cancel(imd);
+
+	imd->read_ahead_path = g_strdup(path);
+
+	if (debug) printf("read ahead set to :%s\n", imd->read_ahead_path);
+
+	image_read_ahead_start(imd);
+}
+
+/*
+ *-------------------------------------------------------------------
+ * post buffering
+ *-------------------------------------------------------------------
+ */
+
+static void image_post_buffer_set(ImageWindow *imd, const gchar *path, GdkPixbuf *pixbuf)
+{
+	g_free(imd->prev_path);
+	if (imd->prev_pixbuf) g_object_unref(imd->prev_pixbuf);
+
+	if (path && pixbuf)
+		{
+		imd->prev_path = g_strdup(path);
+			
+		g_object_ref(pixbuf);
+		imd->prev_pixbuf = pixbuf;
+		}
+	else
+		{
+		imd->prev_path = NULL;
+		imd->prev_pixbuf = NULL;
+		}
+
+	if (debug) printf("post buffer set: %s\n", path);
+}
+
+static gint image_post_buffer_get(ImageWindow *imd)
+{
+	gint success;
+
+	if (imd->prev_pixbuf &&
+	    imd->image_path && imd->prev_path && strcmp(imd->image_path, imd->prev_path) == 0)
+		{
+		if (imd->pixbuf) g_object_unref(imd->pixbuf);
+		imd->pixbuf = imd->prev_pixbuf;
+		success = TRUE;
+		}
+	else
+		{
+		if (imd->prev_pixbuf) g_object_unref(imd->prev_pixbuf);
+		success = FALSE;
+		}
+
+	imd->prev_pixbuf = NULL;
+
+	g_free(imd->prev_path);
+	imd->prev_path = NULL;
+
+	return success;
+}
+
+/*
+ *-------------------------------------------------------------------
+ * loading
+ *-------------------------------------------------------------------
+ */
+
+static void image_load_pixbuf_ready(ImageWindow *imd)
+{
+	if (imd->pixbuf || !imd->il) return;
+
+	imd->pixbuf = image_loader_get_pixbuf(imd->il);
+
+	if (imd->pixbuf) g_object_ref(imd->pixbuf);
+
+	image_pixbuf_sync(imd, imd->zoom, TRUE, TRUE);
+}
+
+static void image_load_area_cb(ImageLoader *il, guint x, guint y, guint w, guint h, gpointer data)
+{
+	ImageWindow *imd = data;
+
+	if (imd->delay_flip &&
+	    imd->pixbuf != image_loader_get_pixbuf(il))
+		{
+		return;
+		}
+
+	if (!imd->pixbuf) image_load_pixbuf_ready(imd);
+
+	if (imd->scale != 1.0)
+		{
+		x = (guint) floor((double)x * imd->scale);
+		y = (guint) floor((double)y * imd->scale);
+		w = (guint) ceil((double)w * imd->scale);
+		h = (guint) ceil((double)h * imd->scale);
+
+		if (w == 0) w = 1;
+		if (h == 0) h = 1;
+
+		if ((GdkInterpType)zoom_quality != GDK_INTERP_NEAREST)
+			{
+			/* some scaling types use surrounding pixels to smooth the image,
+			 * this will expand the new area to cover up for faint black
+			 * lines caused by previous renders with non-complete image
+			 */
+			y -= 1;
+			h += 2;
+			}
+
+		}
+
+	image_queue(imd, (gint) x, (gint) y, (gint) w, (gint) h, FALSE, TILE_RENDER_AREA, TRUE);
+}
+
+static void image_load_done_cb(ImageLoader *il, gpointer data)
 {
 	ImageWindow *imd = data;
-	GtkAdjustment* h;
-	GtkAdjustment* v;
-	gfloat x, y;
-	gfloat val;
-
-	if (!imd->in_drag || !gdk_pointer_is_grabbed()) return;
-
-	if (imd->drag_moved < 4)
+
+	if (debug) printf ("image done\n");
+
+	if (imd->delay_flip &&
+	    imd->pixbuf != image_loader_get_pixbuf(imd->il))
+		{
+		if (imd->pixbuf) g_object_unref(imd->pixbuf);
+		imd->pixbuf = image_loader_get_pixbuf(imd->il);
+		if (imd->pixbuf) g_object_ref(imd->pixbuf);
+		image_pixbuf_sync(imd, imd->zoom, FALSE, TRUE);
+		}
+
+	image_loader_free(imd->il);
+	imd->il = NULL;
+
+	image_post_process(imd, TRUE);
+
+	image_read_ahead_start(imd);
+}
+
+static void image_load_error_cb(ImageLoader *il, gpointer data)
+{
+	if (debug) printf ("image error\n");
+
+	/* even on error handle it like it was done,
+	 * since we have a pixbuf with _something_ */
+
+	image_load_done_cb(il, data);
+}
+
+#ifdef IMAGE_THROTTLE_LARGER_IMAGES
+static void image_load_buffer_throttle(ImageLoader *il)
+{
+	if (!il || il->bytes_total < IMAGE_THROTTLE_THRESHOLD) return;
+
+	/* Larger image files usually have larger chunks of data per pixel...
+	 * So increase the buffer read size so that the rendering chunks called
+	 * are also larger.
+	 */
+
+	image_loader_set_buffer_size(il, IMAGE_LOAD_BUFFER_COUNT * IMAGE_THROTTLE_FACTOR);
+}
+#endif
+
+/* this read ahead is located here merely for the callbacks, above */
+
+static gint image_read_ahead_check(ImageWindow *imd)
+{
+	if (!imd->read_ahead_path) return FALSE;
+	if (imd->il) return FALSE;
+
+	if (!imd->image_path || strcmp(imd->read_ahead_path, imd->image_path) != 0)
+		{
+		image_read_ahead_cancel(imd);
+		return FALSE;
+		}
+
+	if (imd->read_ahead_il)
+		{
+		imd->il = imd->read_ahead_il;
+		imd->read_ahead_il = NULL;
+
+		/* override the old signals */
+		image_loader_set_area_ready_func(imd->il, image_load_area_cb, imd);
+		image_loader_set_error_func(imd->il, image_load_error_cb, imd);
+		image_loader_set_buffer_size(imd->il, IMAGE_LOAD_BUFFER_COUNT);
+
+#ifdef IMAGE_THROTTLE_LARGER_IMAGES
+		image_load_buffer_throttle(imd->il);
+#endif
+
+		/* do this one directly (probably should add a set func) */
+		imd->il->func_done = image_load_done_cb;
+
+		if (!imd->delay_flip)
+			{
+			if (imd->pixbuf) g_object_unref(imd->pixbuf);
+			imd->pixbuf = image_loader_get_pixbuf(imd->il);
+			if (imd->pixbuf) g_object_ref(imd->pixbuf);
+			}
+
+		image_read_ahead_cancel(imd);
+		return TRUE;
+		}
+	else if (imd->read_ahead_pixbuf)
+		{
+		if (imd->pixbuf) g_object_unref(imd->pixbuf);
+		imd->pixbuf = imd->read_ahead_pixbuf;
+		imd->read_ahead_pixbuf = NULL;
+
+		image_read_ahead_cancel(imd);
+
+		image_post_process(imd, FALSE);
+		return TRUE;
+		}
+
+	image_read_ahead_cancel(imd);
+	return FALSE;
+}
+
+static gint image_load_begin(ImageWindow *imd, const gchar *path)
+{
+	if (debug) printf ("image begin \n");
+
+	if (imd->il) return FALSE;
+
+	imd->completed = FALSE;
+
+	if (image_post_buffer_get(imd))
+		{
+		if (debug) printf("from post buffer: %s\n", imd->image_path);
+
+		image_pixbuf_sync(imd, imd->zoom, FALSE, TRUE);
+		return TRUE;
+		}
+
+	if (image_read_ahead_check(imd))
+		{
+		if (debug) printf("from read ahead buffer: %s\n", imd->image_path);
+
+		if (!imd->delay_flip || !imd->il) image_pixbuf_sync(imd, imd->zoom, FALSE, TRUE);
+		return TRUE;
+		}
+
+	if (!imd->delay_flip && imd->pixbuf)
+		{
+		g_object_unref(imd->pixbuf);
+		imd->pixbuf = NULL;
+		}
+
+	imd->il = image_loader_new(path);
+
+	image_loader_set_area_ready_func(imd->il, image_load_area_cb, imd);
+	image_loader_set_error_func(imd->il, image_load_error_cb, imd);
+	image_loader_set_buffer_size(imd->il, IMAGE_LOAD_BUFFER_COUNT);
+
+	if (!image_loader_start(imd->il, image_load_done_cb, imd))
+		{
+		if (debug) printf("image start error\n");
+
+		image_loader_free(imd->il);
+		imd->il = NULL;
+
+		image_complete_util(imd, FALSE);
+
+		return FALSE;
+		}
+
+#ifdef IMAGE_THROTTLE_LARGER_IMAGES
+	image_load_buffer_throttle(imd->il);
+#endif
+
+	if (!imd->delay_flip && !imd->pixbuf) image_load_pixbuf_ready(imd);
+
+	return TRUE;
+}
+
+static void image_reset(ImageWindow *imd)
+{
+	/* stops anything currently being done */
+
+	if (debug) printf("image reset\n");
+
+	image_loader_free(imd->il);
+	imd->il = NULL;
+
+	image_queue_clear(imd);
+	imd->delay_alter_type = ALTER_NONE;
+}
+
+/*
+ *-------------------------------------------------------------------
+ * image changer
+ *-------------------------------------------------------------------
+ */
+
+static void image_change_complete(ImageWindow *imd, gdouble zoom, gint new)
+{
+	gint sync = TRUE;
+
+	imd->zoom = zoom;	/* store the zoom, needed by the loader */
+
+	image_reset(imd);
+
+	if (imd->image_path && isfile(imd->image_path))
+		{
+		if (image_load_begin(imd, imd->image_path))
+			{
+			imd->unknown = FALSE;
+			sync = FALSE;
+			}
+		else
+			{
+			if (imd->pixbuf) g_object_unref(imd->pixbuf);
+			imd->pixbuf = pixbuf_inline(PIXBUF_INLINE_BROKEN);
+			imd->unknown = TRUE;
+			}
+		imd->size = filesize(imd->image_path);
+		imd->mtime = filetime(imd->image_path);
+		}
+	else
+		{
+		if (imd->pixbuf) g_object_unref(imd->pixbuf);
+		imd->pixbuf = NULL;
+
+		if (imd->image_path)
+			{
+			imd->pixbuf = pixbuf_inline(PIXBUF_INLINE_BROKEN);
+			imd->mtime = filetime(imd->image_path);
+			}
+		else
+			{
+			imd->pixbuf = NULL;
+			imd->mtime = 0;
+			}
+		imd->unknown = TRUE;
+		imd->size = 0;
+		}
+
+	if (sync)
+		{
+		image_pixbuf_sync(imd, zoom, FALSE, new);
+		}
+	else
+		{
+		image_update_util(imd);
+		}
+}
+
+static void image_change_real(ImageWindow *imd, const gchar *path,
+			      CollectionData *cd, CollectInfo *info, gdouble zoom)
+{
+	GdkPixbuf *prev_pixbuf = NULL;
+	gchar *prev_path = NULL;
+	gint prev_clear = FALSE;
+
+	imd->collection = cd;
+	imd->collection_info = info;
+
+	if (enable_read_ahead && imd->image_path && imd->pixbuf)
+		{
+		if (imd->il)
+			{
+			/* current image is not finished */
+			prev_clear = TRUE;
+			}
+		else
+			{
+			prev_path = g_strdup(imd->image_path);
+			prev_pixbuf = imd->pixbuf;
+			g_object_ref(prev_pixbuf);
+			}
+		}
+
+	g_free(imd->image_path);
+	imd->image_path = g_strdup(path);
+	imd->image_name = filename_from_path(imd->image_path);
+
+	image_change_complete(imd, zoom, TRUE);
+
+	if (prev_pixbuf)
+		{
+		image_post_buffer_set(imd, prev_path, prev_pixbuf);
+		g_free(prev_path);
+		g_object_unref(prev_pixbuf);
+		}
+	else if (prev_clear)
+		{
+		image_post_buffer_set(imd, NULL, NULL);
+		}
+
+	image_update_title(imd);
+	image_new_util(imd);
+}
+
+/*
+ *-------------------------------------------------------------------
+ * callbacks
+ *-------------------------------------------------------------------
+ */
+
+static gint image_expose_cb(GtkWidget *widget, GdkEventExpose *event, gpointer data)
+{
+	gint x, y;
+
+	ImageWindow *imd = data;
+
+	image_border_draw(imd, event->area.x, event->area.y,
+			  event->area.width, event->area.height);
+
+	/* image */
+	x = MAX(0, (gint)event->area.x - imd->x_offset + imd->x_scroll);
+	y = MAX(0, (gint)event->area.y - imd->y_offset + imd->y_scroll);
+
+	image_queue(imd, x, y,
+		    MIN((gint)event->area.width, imd->width - x),
+		    MIN((gint)event->area.height, imd->height - y),
+		    FALSE, TILE_RENDER_ALL, FALSE);
+
+	return TRUE;
+}
+
+static void image_size_cb(GtkWidget *widget, GtkAllocation *allocation, gpointer data)
+{
+	ImageWindow *imd = data;
+
+	image_size_sync(imd, allocation->width, allocation->height);
+}
+
+/*
+ *-------------------------------------------------------------------
+ * focus stuff
+ *-------------------------------------------------------------------
+ */
+
+static void image_focus_paint(ImageWindow *imd, gint has_focus, GdkRectangle *area)
+{
+	GtkWidget *widget;
+
+	widget = imd->widget;
+	if (!widget->window) return;
+
+	if (has_focus)
+		{
+		gtk_paint_focus (widget->style, widget->window, GTK_STATE_ACTIVE,
+				 area, widget, "image_window",
+				 widget->allocation.x, widget->allocation.y,
+				 widget->allocation.width - 1, widget->allocation.height - 1);	
+		}
+	else
+		{
+		gtk_paint_shadow (widget->style, widget->window, GTK_STATE_NORMAL, GTK_SHADOW_IN,
+				  area, widget, "image_window",
+				  widget->allocation.x, widget->allocation.y,
+				  widget->allocation.width - 1, widget->allocation.height - 1);
+		}
+}
+
+static gint image_focus_expose(GtkWidget *widget, GdkEventExpose *event, gpointer data)
+{
+	ImageWindow *imd = data;
+
+	image_focus_paint(imd, GTK_WIDGET_HAS_FOCUS(widget), &event->area);
+	return TRUE;
+}
+
+static gint image_focus_in_cb(GtkWidget *widget, GdkEventFocus *event, gpointer data)
+{
+	ImageWindow *imd = data;
+
+	GTK_WIDGET_SET_FLAGS(imd->widget, GTK_HAS_FOCUS);
+	image_focus_paint(imd, TRUE, NULL);
+
+	return TRUE;
+}
+
+static gint image_focus_out_cb(GtkWidget *widget, GdkEventFocus *event, gpointer data)
+{
+	ImageWindow *imd = data;
+
+	GTK_WIDGET_UNSET_FLAGS(imd->widget, GTK_HAS_FOCUS);
+	image_focus_paint(imd, FALSE, NULL);
+
+	return TRUE;
+}
+
+
+/*
+ *-------------------------------------------------------------------
+ * overlays
+ *-------------------------------------------------------------------
+ */
+
+static void image_overlay_draw(ImageWindow *imd, gint x, gint y, gint w, gint h)
+{
+	GList *work;
+
+	work = imd->overlay_list;
+	while (work)
+		{
+		OverlayData *od;
+		gint px, py, pw, ph;
+		gint rx, ry, rw, rh;
+
+		od = work->data;
+		work = work->next;
+
+		if (!od->visible) continue;
+
+		pw = gdk_pixbuf_get_width(od->pixbuf);
+		ph = gdk_pixbuf_get_height(od->pixbuf);
+		px = od->x;
+		py = od->y;
+
+	        if (od->relative)
+			{
+			if (px < 0) px = imd->window_width - pw + px;
+			if (py < 0) py = imd->window_height - ph + py;
+			}
+
+		if (util_clip_region(x, y, w, h, px, py, pw, ph, &rx, &ry, &rw, &rh))
+			{
+			gdk_draw_pixbuf(imd->image->window,
+					imd->image->style->fg_gc[GTK_WIDGET_STATE(imd->image)],
+					od->pixbuf,
+					rx - px, ry - py,
+					rx, ry, rw, rh,
+					(GdkRgbDither)dither_quality, rx, ry);
+			}
+		}
+}
+
+static void image_overlay_queue_draw(ImageWindow *imd, OverlayData *od, gint hidden)
+{
+	gint x, y, w, h;
+	gint old_vis;
+
+	w = gdk_pixbuf_get_width(od->pixbuf);
+	h = gdk_pixbuf_get_height(od->pixbuf);
+	x = od->x;
+	y = od->y;
+
+	if (od->relative)
+		{
+		if (x < 0) x = imd->window_width - w + x;
+		if (y < 0) y = imd->window_height - h + y;
+		}
+
+	image_queue(imd, imd->x_scroll - imd->x_offset + x,
+			 imd->y_scroll - imd->y_offset + y,
+			 w, h,
+			 FALSE, TILE_RENDER_ALL, FALSE);
+
+	old_vis = od->visible;
+	if (hidden) od->visible = FALSE;
+	image_border_draw(imd, x, y, w, h);
+	od->visible = old_vis;
+}
+
+static void image_overlay_queue_all(ImageWindow *imd)
+{
+	GList *work;
+
+	work = imd->overlay_list;
+	while (work)
+		{
+		OverlayData *od = work->data;
+		work = work->next;
+
+		image_overlay_queue_draw(imd, od, FALSE);
+		}
+}
+
+static OverlayData *image_overlay_find(ImageWindow *imd, gint id)
+{
+	GList *work;
+
+	work = imd->overlay_list;
+	while (work)
+		{
+		OverlayData *od = work->data;
+		work = work->next;
+
+		if (od->id == id) return od;
+		}
+
+	return NULL;
+}
+
+gint image_overlay_add(ImageWindow *imd, GdkPixbuf *pixbuf, gint x, gint y,
+		       gint relative, gint always)
+{
+	OverlayData *od;
+	gint id;
+
+	if (!imd || !pixbuf) return -1;
+
+	id = 1;
+	while (image_overlay_find(imd, id)) id++;
+
+	od = g_new0(OverlayData, 1);
+	od->id = id;
+	od->pixbuf = pixbuf;
+	g_object_ref(G_OBJECT(od->pixbuf));
+	od->x = x;
+	od->y = y;
+	od->relative = relative;
+	od->visible = TRUE;
+	od->always = always;
+
+	imd->overlay_list = g_list_append(imd->overlay_list, od);
+
+	image_overlay_queue_draw(imd, od, FALSE);
+
+	return od->id;
+}
+
+static void image_overlay_free(ImageWindow *imd, OverlayData *od)
+{
+	imd->overlay_list = g_list_remove(imd->overlay_list, od);
+
+	if (od->pixbuf) g_object_unref(G_OBJECT(od->pixbuf));
+	g_free(od);
+}
+
+static void image_overlay_list_clear(ImageWindow *imd)
+{
+	while (imd->overlay_list)
+		{
+		OverlayData *od;
+
+		od = imd->overlay_list->data;
+		image_overlay_free(imd, od);
+		}
+}
+
+void image_overlay_set(ImageWindow *imd, gint id, GdkPixbuf *pixbuf, gint x, gint y)
+{
+	OverlayData *od;
+
+	if (!imd) return;
+
+	od = image_overlay_find(imd, id);
+	if (!od) return;
+
+	if (pixbuf)
+		{
+		image_overlay_queue_draw(imd, od, TRUE);
+
+		g_object_ref(G_OBJECT(pixbuf));
+		g_object_unref(G_OBJECT(od->pixbuf));
+		od->pixbuf = pixbuf;
+
+		od->x = x;
+		od->y = y;
+
+		image_overlay_queue_draw(imd, od, FALSE);
+		}
+	else
+		{
+		image_overlay_queue_draw(imd, od, TRUE);
+		image_overlay_free(imd, od);
+		}
+}
+
+gint image_overlay_get(ImageWindow *imd, gint id, GdkPixbuf **pixbuf, gint *x, gint *y)
+{
+	OverlayData *od;
+
+	if (!imd) return FALSE;
+
+	od = image_overlay_find(imd, id);
+	if (!od) return FALSE;
+
+	if (pixbuf) *pixbuf = od->pixbuf;
+	if (x) *x = od->x;
+	if (y) *y = od->y;
+
+	return TRUE;
+}
+
+void image_overlay_remove(ImageWindow *imd, gint id)
+{
+	image_overlay_set(imd, id, NULL, 0, 0);
+}
+
+/*
+ *-------------------------------------------------------------------
+ * scroller
+ *-------------------------------------------------------------------
+ */
+
+#define SCROLLER_UPDATES_PER_SEC 30
+#define SCROLLER_DEAD_ZONE 6
+
+
+static gboolean image_scroller_update_cb(gpointer data)
+{
+	ImageWindow *imd = data;
+	gint x, y;
+	gint xinc, yinc;
+
+	/* this was a simple scroll by difference between scroller and mouse position,
+	 * but all this math results in a smoother result and accounts for a dead zone.
+	 */
+
+	if (abs(imd->scroller_xpos - imd->scroller_x) < SCROLLER_DEAD_ZONE)
+		{
+		x = 0;
+		}
+	else
+		{
+		gint shift = SCROLLER_DEAD_ZONE / 2 * SCROLLER_UPDATES_PER_SEC;
+		x = (imd->scroller_xpos - imd->scroller_x) / 2 * SCROLLER_UPDATES_PER_SEC;
+		x += (x > 0) ? -shift : shift;
+		}
+
+	if (abs(imd->scroller_ypos - imd->scroller_y) < SCROLLER_DEAD_ZONE)
+		{
+		y = 0;
+		}
+	else
+		{
+		gint shift = SCROLLER_DEAD_ZONE / 2 * SCROLLER_UPDATES_PER_SEC;
+		y = (imd->scroller_ypos - imd->scroller_y) / 2 * SCROLLER_UPDATES_PER_SEC;
+		y += (y > 0) ? -shift : shift;
+		}
+
+	if (abs(x) < SCROLLER_DEAD_ZONE * SCROLLER_UPDATES_PER_SEC)
+		{
+		xinc = x;
+		}
+	else
+		{
+		xinc = imd->scroller_xinc;
+
+		if (x >= 0)
+			{
+			if (xinc < 0) xinc = 0;
+			if (x < xinc) xinc = x;
+			if (x > xinc) xinc = MIN(xinc + x / SCROLLER_UPDATES_PER_SEC, x);
+			}
+		else
+			{
+			if (xinc > 0) xinc = 0;
+			if (x > xinc) xinc = x;
+			if (x < xinc) xinc = MAX(xinc + x / SCROLLER_UPDATES_PER_SEC, x);
+			}
+		}
+
+	if (abs(y) < SCROLLER_DEAD_ZONE * SCROLLER_UPDATES_PER_SEC)
+		{
+		yinc = y;
+		}
+	else
+		{
+		yinc = imd->scroller_yinc;
+
+		if (y >= 0)
+			{
+			if (yinc < 0) yinc = 0;
+			if (y < yinc) yinc = y;
+			if (y > yinc) yinc = MIN(yinc + y / SCROLLER_UPDATES_PER_SEC, y);
+			}
+		else
+			{
+			if (yinc > 0) yinc = 0;
+			if (y > yinc) yinc = y;
+			if (y < yinc) yinc = MAX(yinc + y / SCROLLER_UPDATES_PER_SEC, y);
+			}
+		}
+
+	imd->scroller_xinc = xinc;
+	imd->scroller_yinc = yinc;
+
+	xinc = xinc / SCROLLER_UPDATES_PER_SEC;
+	yinc = yinc / SCROLLER_UPDATES_PER_SEC;
+
+	image_scroll(imd, xinc, yinc);
+
+	return TRUE;
+}
+
+static void image_scroller_timer_set(ImageWindow *imd, gint start)
+{
+	if (imd->scroller_id != -1)
+		{
+		g_source_remove(imd->scroller_id);
+		imd->scroller_id = -1;
+		}
+
+	if (start)
+		{
+		imd->scroller_id = g_timeout_add(1000 / SCROLLER_UPDATES_PER_SEC,
+						   image_scroller_update_cb, imd);
+		}
+}
+
+static void image_scroller_start(ImageWindow *imd, gint x, gint y)
+{
+	if (imd->scroller_overlay == -1)
+		{
+		GdkPixbuf *pixbuf;
+		gint w, h;
+
+		pixbuf = pixbuf_inline(PIXBUF_INLINE_SCROLLER);
+		w = gdk_pixbuf_get_width(pixbuf);
+		h = gdk_pixbuf_get_height(pixbuf);
+
+		imd->scroller_overlay = image_overlay_add(imd, pixbuf, x - w / 2, y - h / 2, FALSE, TRUE);
+		}
+
+	imd->scroller_x = x;
+	imd->scroller_y = y;
+	imd->scroller_xpos = x;
+	imd->scroller_ypos = y;
+
+	image_scroller_timer_set(imd, TRUE);
+}
+
+static void image_scroller_stop(ImageWindow *imd)
+{
+	if (imd->scroller_id == -1) return;
+
+	image_overlay_remove(imd, imd->scroller_overlay);
+	imd->scroller_overlay = -1;
+
+	image_scroller_timer_set(imd, FALSE);
+}
+
+/*
+ *-------------------------------------------------------------------
+ * mouse stuff
+ *-------------------------------------------------------------------
+ */
+
+static gint image_mouse_motion_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
+{
+	ImageWindow *imd = data;
+
+	if (imd->scroller_id != -1)
+		{
+		imd->scroller_xpos = bevent->x;
+		imd->scroller_ypos = bevent->y;
+		}
+
+	if (!imd->in_drag || !gdk_pointer_is_grabbed()) return FALSE;
+
+	if (imd->drag_moved < IMAGE_DRAG_SCROLL_THRESHHOLD)
 		{
 		imd->drag_moved++;
 		}
 	else
 		{
-		set_mouse_cursor (imd->eventbox->window, GDK_FLEUR);
+		widget_set_cursor (imd->image, GDK_FLEUR);
 		}
 
-	h = gtk_viewport_get_hadjustment(GTK_VIEWPORT(imd->viewport));
-	v = gtk_viewport_get_vadjustment(GTK_VIEWPORT(imd->viewport));
-
-	x = imd->drag_last_x - bevent->x;
-	y = imd->drag_last_y - bevent->y;
-
-	/* x */
-	if (h->upper - h->page_size > 0)
-		{
-		val = (float)h->value + x;
-		if (val < 0 ) val = 0;
-		if (val > h->upper - h->page_size) val = h->upper - h->page_size;
-		h->value = val;
-		gtk_adjustment_set_value (GTK_ADJUSTMENT(h), val);
-		}
-
-	/* y */
-	if (v->upper - v->page_size > 0)
-		{
-		val = v->value + y;
-		if (val < 0 ) val = 0;
-		if (val > v->upper - v->page_size) val = v->upper - v->page_size;
-		v->value = val;
-		gtk_adjustment_set_value (GTK_ADJUSTMENT(v), val);
-		}
-
-	gtk_adjustment_value_changed(h);
-	gtk_adjustment_value_changed(v);
+	/* do the scroll */
+	image_scroll_real(imd, imd->drag_last_x - bevent->x, imd->drag_last_y - bevent->y);
 
 	imd->drag_last_x = bevent->x;
 	imd->drag_last_y = bevent->y;
+
+	return FALSE;
 }
 
-static void image_area_mouse_pressed(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
+static gint image_mouse_press_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
 {
 	ImageWindow *imd = data;
+
+	if (imd->scroller_id != -1) return TRUE;
+
 	switch (bevent->button)
 		{
 		case 1:
@@ -593,236 +2531,793 @@
 			imd->drag_last_x = bevent->x;
 			imd->drag_last_y = bevent->y;
 			imd->drag_moved = 0;
-			gdk_pointer_grab (imd->eventbox->window, FALSE,
-                                GDK_POINTER_MOTION_MASK |
-                                GDK_BUTTON_RELEASE_MASK,
+			gdk_pointer_grab(imd->image->window, FALSE,
+                                GDK_POINTER_MOTION_MASK | GDK_BUTTON_RELEASE_MASK,
                                 NULL, NULL, bevent->time);
-			gtk_grab_add (imd->eventbox);
+			gtk_grab_add(imd->image);
 			break;
 		case 2:
 			imd->drag_moved = 0;
 			break;
 		case 3:
-			if (imd->func_btn3)
-				imd->func_btn3(imd, bevent, imd->data_btn3);
-			break;
-		case 4:
-			if (imd->func_btn4)
-				imd->func_btn4(imd, bevent, imd->data_btn4);
-			break;
-		case 5:
-			if (imd->func_btn5)
-				imd->func_btn5(imd, bevent, imd->data_btn5);
+			image_button_do(imd, bevent);
 			break;
 		default:
 			break;
 		}
-	gtk_widget_grab_focus(imd->viewport);
+
+	gtk_widget_grab_focus(imd->widget);
+
+	return FALSE;
 }
 
-static void image_area_mouse_released(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
+static gint image_mouse_release_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data)
 {
 	ImageWindow *imd = data;
-	if (gdk_pointer_is_grabbed() && GTK_WIDGET_HAS_GRAB (imd->eventbox))
+
+	if (imd->scroller_id != -1)
 		{
-		gtk_grab_remove (imd->eventbox);
-		gdk_pointer_ungrab (bevent->time);
-		set_mouse_cursor (imd->eventbox->window, -1);
+		image_scroller_stop(imd);
+		return TRUE;
 		}
 
-	if (bevent->button == 1)
+	if (gdk_pointer_is_grabbed() && GTK_WIDGET_HAS_GRAB(imd->image))
 		{
-		if (imd->drag_moved < 4 && imd->func_btn1)
-			imd->func_btn1(imd, bevent, imd->data_btn1);
+		gtk_grab_remove(imd->image);
+		gdk_pointer_ungrab(bevent->time);
+		widget_set_cursor(imd->image, -1);
 		}
 
-	if (bevent->button == 2)
+	if (bevent->button == 1 && (bevent->state & GDK_SHIFT_MASK))
 		{
-		if (imd->drag_moved < 4 && imd->func_btn2)
-			imd->func_btn2(imd, bevent, imd->data_btn2);
+		image_scroller_start(imd, bevent->x, bevent->y);
+		}
+	else if (bevent->button == 1 || bevent->button == 2)
+		{
+		if (imd->drag_moved < IMAGE_DRAG_SCROLL_THRESHHOLD) image_button_do(imd, bevent);
 		}
 
 	imd->in_drag = FALSE;
+
+	return FALSE;
 }
 
-static void image_area_mouse_drag(GtkWidget *widget, GdkDragContext *context, gpointer data)
+static gint image_mouse_leave_cb(GtkWidget *widget, GdkEventCrossing *event, gpointer data)
 {
 	ImageWindow *imd = data;
-	imd->drag_moved = 4;
+
+	if (imd->scroller_id != -1)
+		{
+		imd->scroller_xpos = imd->scroller_x;
+		imd->scroller_ypos = imd->scroller_y;
+		imd->scroller_xinc = 0;
+		imd->scroller_yinc = 0;
+		}
+
+	return FALSE;
+}
+
+static gint image_scroll_cb(GtkWidget *widget, GdkEventScroll *event, gpointer data)
+{
+	ImageWindow *imd = data;
+
+	if (imd->func_scroll &&
+	    event && event->type == GDK_SCROLL)
+	        {
+		imd->func_scroll(imd, event->direction, event->time,
+				 event->x, event->y, event->state, imd->data_scroll);
+		return TRUE;
+		}
+
+	return FALSE;
+}
+
+static void image_mouse_drag_cb(GtkWidget *widget, GdkDragContext *context, gpointer data)
+{
+	ImageWindow *imd = data;
+
+	imd->drag_moved = IMAGE_DRAG_SCROLL_THRESHHOLD;
 }
 
 /*
- *-----------------------------------------------------------------------------
- * image widget setup routines
- *-----------------------------------------------------------------------------
- */ 
-
-void image_area_set_topwindow(ImageWindow *imd, GtkWidget *window, gchar *title, gint show_zoom)
+ *-------------------------------------------------------------------
+ * drag and drop
+ *-------------------------------------------------------------------
+ */
+
+/*
+ *-------------------------------------------------------------------
+ * public interface
+ *-------------------------------------------------------------------
+ */
+
+void image_attach_window(ImageWindow *imd, GtkWidget *window,
+			 const gchar *title, const gchar *title_right, gint show_zoom)
 {
 	imd->top_window = window;
-	imd->show_title_zoom = show_zoom;
-
 	g_free(imd->title);
-	if (title)
-		imd->title = g_strdup(title);
-	else
-		imd->title = NULL;
+	imd->title = g_strdup(title);
+	g_free(imd->title_right);
+	imd->title_right = g_strdup(title_right);
+	imd->title_show_zoom = show_zoom;
+
+	image_update_title(imd);
+}
+
+void image_set_update_func(ImageWindow *imd,
+			   void (*func)(ImageWindow *imd, gpointer data),
+			   gpointer data)
+{
+	imd->func_update = func;
+	imd->data_update = data;
 }
 
-void image_area_set_labels(ImageWindow *imd, GtkWidget *info, GtkWidget *zoom)
+void image_set_complete_func(ImageWindow *imd,
+			     void (*func)(ImageWindow *, gint preload, gpointer),
+			     gpointer data)
 {
-	imd->info_label = info;
-	imd->zoom_label = zoom;
+	imd->func_complete = func;
+	imd->data_complete = data;
 }
 
-void image_area_set_button(ImageWindow *imd, gint button,
-	void (*func)(ImageWindow *, GdkEventButton *, gpointer), gpointer data)
+void image_set_new_func(ImageWindow *imd,
+			void (*func)(ImageWindow *, gpointer),
+			gpointer data)
 {
-	switch (button)
+	imd->func_new = func;
+	imd->data_new = data;
+}
+
+
+static void image_button_do(ImageWindow *imd, GdkEventButton *bevent)
+{
+	if (imd->func_button &&
+	    bevent &&
+	    (bevent->type == GDK_BUTTON_PRESS || bevent->type == GDK_BUTTON_RELEASE))
 		{
-		case 1:
-			imd->func_btn1 = func;
-			imd->data_btn1 = data;
-			break;
-		case 2:
-			imd->func_btn2 = func;
-			imd->data_btn2 = data;
-			break;
-		case 3:
-			imd->func_btn3 = func;
-			imd->data_btn3 = data;
-			break;
-		case 4:
-			imd->func_btn4 = func;
-			imd->data_btn4 = data;
-			break;
-		case 5:
-			imd->func_btn5 = func;
-			imd->data_btn5 = data;
-			break;
+		imd->func_button(imd, bevent->button, bevent->time,
+				 bevent->x, bevent->y, bevent->state, imd->data_button);
 		}
 }
 
-ImageWindow *image_area_new(GtkWidget *top_window)
+void image_set_button_func(ImageWindow *imd,
+			   void (*func)(ImageWindow *, gint button, guint32 time, gdouble x, gdouble y, guint state, gpointer),
+			   gpointer data)
+{
+	imd->func_button = func;
+	imd->data_button = data;
+}
+
+void image_set_scroll_func(ImageWindow *imd,
+			   void (*func)(ImageWindow *, GdkScrollDirection direction, guint32 time, gdouble x, gdouble y, guint state, gpointer),
+			   gpointer data)
 {
-        GtkObject *h_adj;
-        GtkObject *v_adj;
-	ImageWindow *imd;
-
-	imd = g_new0(ImageWindow, 1);
-	imd->zoom = 0;
-
-	imd->top_window = top_window;
-	imd->title = g_strdup("GQview - ");
-	imd->show_title_zoom = FALSE;
-	imd->new_img = FALSE;
-
-	imd->eventbox = gtk_event_box_new();
-
-	gtk_signal_connect(GTK_OBJECT(imd->eventbox),"motion_notify_event",
-			   GTK_SIGNAL_FUNC(image_area_mouse_moved), imd);
-	gtk_signal_connect(GTK_OBJECT(imd->eventbox),"button_press_event",
-			   GTK_SIGNAL_FUNC(image_area_mouse_pressed), imd);
-	gtk_signal_connect(GTK_OBJECT(imd->eventbox),"button_release_event",
-			   GTK_SIGNAL_FUNC(image_area_mouse_released), imd);
-	gtk_widget_set_events(imd->eventbox, GDK_POINTER_MOTION_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK);
-
-	/* viewer */
-	h_adj = gtk_adjustment_new(0.0,0.0,0.0,1.0,1.0,1.0);
-	v_adj = gtk_adjustment_new(0.0,0.0,0.0,1.0,1.0,1.0);
-
-	imd->viewport = gtk_viewport_new (GTK_ADJUSTMENT(h_adj), GTK_ADJUSTMENT(v_adj));
-	gtk_container_add(GTK_CONTAINER(imd->eventbox), imd->viewport);
-
-	/* table for resize */
-	imd->table = gtk_table_new (1,1,TRUE);
-	gtk_container_add(GTK_CONTAINER (imd->viewport), imd->table);
-
-	/* imagewindow */
-	imd->image = gtk_drawing_area_new();
-	gtk_table_attach(GTK_TABLE (imd->table),imd->image,0,1,0,1,GTK_EXPAND,GTK_EXPAND,0,0);
-
-	gtk_signal_connect(GTK_OBJECT(imd->eventbox),"size_allocate",GTK_SIGNAL_FUNC(image_area_size_cb), imd);
-	gtk_signal_connect(GTK_OBJECT(imd->image),"configure_event",GTK_SIGNAL_FUNC(image_area_update_cb), imd);
-
-	gtk_signal_connect(GTK_OBJECT(imd->viewport),"drag_begin",
-			   GTK_SIGNAL_FUNC(image_area_mouse_drag), imd);
-
-	return imd;
+	imd->func_scroll = func;
+	imd->data_scroll = data;
 }
 
-void image_area_free(ImageWindow *imd)
+/* path, name */
+
+const gchar *image_get_path(ImageWindow *imd)
+{
+	return imd->image_path;
+}
+
+const gchar *image_get_name(ImageWindow *imd)
+{
+	return imd->image_name;
+}
+
+/* merely changes path string, does not change the image! */
+void image_set_path(ImageWindow *imd, const gchar *newpath)
 {
 	g_free(imd->image_path);
-	g_free(imd->title);
-
-	if (imd->image_pixmap) gdk_imlib_free_pixmap(imd->image_pixmap);
-	if (imd->image_data) gdk_imlib_destroy_image(imd->image_data);
-
-	g_free(imd);
+	imd->image_path = g_strdup(newpath);
+	imd->image_name = filename_from_path(imd->image_path);
+
+	image_update_title(imd);
+	image_new_util(imd);
+}
+
+/* load a new image */
+
+void image_change_path(ImageWindow *imd, const gchar *path, gdouble zoom)
+{
+	if (imd->image_path == path ||
+	    (path && imd->image_path && !strcmp(path, imd->image_path)) ) return;
+
+	image_change_real(imd, path, NULL, NULL, zoom);
+}
+
+void image_change_pixbuf(ImageWindow *imd, GdkPixbuf *pixbuf, gdouble zoom)
+{
+	image_set_pixbuf(imd, pixbuf, zoom, TRUE);
+	image_new_util(imd);
+}
+
+void image_change_from_collection(ImageWindow *imd, CollectionData *cd, CollectInfo *info, gdouble zoom)
+{
+	if (!cd || !info || !g_list_find(cd->list, info)) return;
+
+	image_change_real(imd, info->path, cd, info, zoom);
+}
+
+CollectionData *image_get_collection(ImageWindow *imd, CollectInfo **info)
+{
+	if (collection_to_number(imd->collection) >= 0)
+		{
+		if (g_list_find(imd->collection->list, imd->collection_info) != NULL)
+			{
+			if (info) *info = imd->collection_info;
+			}
+		else
+			{
+			if (info) *info = NULL;
+			}
+		return imd->collection;
+		}
+
+	if (info) *info = NULL;
+	return NULL;
+}
+
+static void image_loader_sync_data(ImageLoader *il, gpointer data)
+{
+	/* change data for the callbacks directly */
+
+	il->data_area_ready = data;
+	il->data_error = data;
+	il->data_done = data;
+	il->data_percent = data;
 }
 
-gint get_default_zoom(ImageWindow *imd)
+/* this is more like a move function
+ * it moves most data from source to imd, source does keep a ref on the pixbuf
+ */
+void image_change_from_image(ImageWindow *imd, ImageWindow *source)
+{
+	if (imd == source) return;
+
+	imd->unknown = source->unknown;
+
+	image_set_pixbuf(imd, source->pixbuf, image_zoom_get(source), TRUE);
+
+	imd->collection = source->collection;
+	imd->collection_info = source->collection_info;
+	imd->size = source->size;
+	imd->mtime = source->mtime;
+
+	image_set_path(imd, image_get_path(source));
+
+	image_loader_free(imd->il);
+	imd->il = NULL;
+
+	if (imd->pixbuf && source->il)
+		{
+		imd->il = source->il;
+		source->il = NULL;
+
+		image_loader_sync_data(imd->il, imd);
+
+		imd->delay_alter_type = source->delay_alter_type;
+		source->delay_alter_type = ALTER_NONE;
+		}
+
+	image_loader_free(imd->read_ahead_il);
+	imd->read_ahead_il = source->read_ahead_il;
+	source->read_ahead_il = NULL;
+	if (imd->read_ahead_il) image_loader_sync_data(imd->read_ahead_il, imd);
+
+	if (imd->read_ahead_pixbuf) g_object_unref(imd->read_ahead_pixbuf);
+	imd->read_ahead_pixbuf = source->read_ahead_pixbuf;
+	source->read_ahead_pixbuf = NULL;
+
+	g_free(imd->read_ahead_path);
+	imd->read_ahead_path = source->read_ahead_path;
+	source->read_ahead_path = NULL;
+
+	if (imd->prev_pixbuf) g_object_unref(imd->prev_pixbuf);
+	imd->prev_pixbuf = source->prev_pixbuf;
+	source->prev_pixbuf = NULL;
+
+	g_free(imd->prev_path);
+	imd->prev_path = source->prev_path;
+	source->prev_path = NULL;
+
+	imd->completed = source->completed;
+
+	imd->x_scroll = source->x_scroll;
+	imd->y_scroll = source->y_scroll;
+
+	image_scroll_clamp(imd);
+}
+
+/* manipulation */
+
+void image_area_changed(ImageWindow *imd, gint x, gint y, gint width, gint height)
+{
+	gint sx, sy, sw, sh;
+
+	sx = (gint)floor((double)x * imd->scale);
+	sy = (gint)floor((double)y * imd->scale);
+	sw = (gint)ceil((double)width * imd->scale);
+	sh = (gint)ceil((double)height * imd->scale);
+
+	image_queue(imd, sx, sy, sw, sh, FALSE, TILE_RENDER_AREA, TRUE);
+}
+
+void image_reload(ImageWindow *imd)
+{
+	image_change_complete(imd, imd->zoom, FALSE);
+}
+
+void image_scroll(ImageWindow *imd, gint x, gint y)
+{
+	image_scroll_real(imd, x, y);
+}
+
+void image_alter(ImageWindow *imd, AlterType type)
 {
-	gint zoom;
-
-	if (zoom_mode == ZOOM_RESET_ORIGINAL)
+	if (imd->il)
+		{
+		/* still loading, wait till done */
+		imd->delay_alter_type = type;
+		return;
+		}
+
+	image_alter_real(imd, type, TRUE);
+}
+
+/* zoom */
+
+static void image_zoom_adjust_real(ImageWindow *imd, gdouble increment,
+				   gint center_point, gint x, gint y)
+{
+	gdouble zoom = imd->zoom;
+
+	if (increment == 0.0) return; /* avoid possible div by zero, a no-op anyway... */
+
+	if (zoom == 0.0)
+		{
+		if (imd->scale < 1.0)
+			{
+			zoom = 0.0 - 1.0 / imd->scale;
+			}
+		else
+			{
+			zoom = imd->scale;
+			}
+		}
+
+	if (increment < 0.0)
+		{
+		if (zoom >= 1.0 && zoom + increment < 1.0)
+			{
+			zoom = zoom + increment - 2.0;
+			}
+		else
+			{
+			zoom = zoom + increment;
+			}
+		}
+	else
 		{
-		zoom = 1;
+		if (zoom <= -1.0 && zoom + increment > -1.0)
+			{
+			zoom = zoom + increment + 2.0;
+			}
+		else
+			{
+			zoom = zoom + increment;
+			}
+		}
+
+	image_zoom_sync(imd, zoom, FALSE, FALSE, FALSE, center_point, x, y);
+}
+
+void image_zoom_adjust(ImageWindow *imd, gdouble increment)
+{
+	image_zoom_adjust_real(imd, increment, FALSE, 0, 0);
+}
+
+void image_zoom_adjust_at_point(ImageWindow *imd, gdouble increment, gint x, gint y)
+{
+	image_zoom_adjust_real(imd, increment, TRUE, x, y);
+}
+
+void image_zoom_set(ImageWindow *imd, gdouble zoom)
+{
+	image_zoom_sync(imd, zoom, FALSE, FALSE, FALSE, FALSE, 0, 0);
+}
+
+void image_zoom_set_fill_geometry(ImageWindow *imd, gint vertical)
+{
+	gdouble zoom;
+
+	if (!imd->pixbuf || imd->image_width < 1 || imd->image_height < 1) return;
+
+	if (vertical)
+		{
+		zoom = (gdouble)imd->window_height / imd->image_height;
+		}
+	else
+		{
+		zoom = (gdouble)imd->window_width / imd->image_width;
+		}
+
+	if (zoom < 1.0)
+		{
+		zoom = 0.0 - 1.0 / zoom;
 		}
-	else if (zoom_mode == ZOOM_RESET_FIT_WINDOW)
+
+	image_zoom_set(imd, zoom);
+}
+
+gdouble image_zoom_get(ImageWindow *imd)
+{
+	return imd->zoom;
+}
+
+gdouble image_zoom_get_real(ImageWindow *imd)
+{
+	return imd->scale;
+}
+
+gchar *image_zoom_get_as_text(ImageWindow *imd)
+{
+	gdouble l = 1.0;
+	gdouble r = 1.0;
+	gint pl = 0;
+	gint pr = 0;
+	gchar *approx = " ";
+
+	if (imd->zoom > 0.0)
+		{
+		l = imd->zoom;
+		}
+	else if (imd->zoom < 0.0)
 		{
-		zoom = 0;
+		r = 0.0 - imd->zoom;
+		}
+	else if (imd->zoom == 0.0 && imd->scale != 0.0)
+		{
+		if (imd->scale >= 1.0)
+			{
+			l = imd->scale;
+			}
+		else
+			{
+			r = 1.0 / imd->scale;
+			}
+		approx = " ~";
+		}
+
+	if (rint(l) != l) pl = 1;
+	if (rint(r) != r) pr = 1;
+
+	return g_strdup_printf("%.*f :%s%.*f", pl, l, approx, pr, r);
+}
+
+gdouble image_zoom_get_default(ImageWindow *imd, gint mode)
+{
+	gdouble zoom;
+
+	if (mode == ZOOM_RESET_ORIGINAL)
+		{
+		zoom = 1.0;
+		}
+	else if (mode == ZOOM_RESET_FIT_WINDOW)
+		{
+		zoom = 0.0;
 		}
 	else
 		{
 		if (imd)
 			{
-			zoom = image_area_get_zoom(imd);
+			zoom = image_zoom_get(imd);
 			}
 		else
 			{
-			zoom = 1;
+			zoom = 1.0;
 			}
 		}
 
 	return zoom;
 }
 
-/*
- *-----------------------------------------------------------------------------
- * image widget misc utils
- *-----------------------------------------------------------------------------
- */ 
-
-void image_area_to_root(ImageWindow *imd, gint scaled)
-{                                                                               
-	GdkVisual *gdkvisual;
+/* read ahead */
+
+void image_prebuffer_set(ImageWindow *imd, const gchar *path)
+{
+	if (path)
+		{
+		image_read_ahead_set(imd, path);
+		}
+	else
+		{
+		image_read_ahead_cancel(imd);
+		}
+}
+
+static gint image_auto_refresh_cb(gpointer data)
+{
+	ImageWindow *imd = data;
+	time_t newtime;
+	
+	if (!imd || !imd->pixbuf ||
+	    imd->il || !imd->image_path ||
+	    !update_on_time_change) return TRUE;
+
+	newtime = filetime(imd->image_path);
+	if (newtime > 0 && newtime != imd->mtime)
+		{
+		imd->mtime = newtime;
+		image_reload(imd);
+		}
+
+	return TRUE;
+}
+
+/* image auto refresh on time stamp change, in 1/1000's second, -1 disables */
+
+void image_auto_refresh(ImageWindow *imd, gint interval)
+{
+	if (!imd) return;
+
+	if (imd->auto_refresh_id > -1)
+		{
+		g_source_remove(imd->auto_refresh_id);
+		imd->auto_refresh_id = -1;
+		imd->auto_refresh_interval = -1;
+		}
+
+	if (interval < 0) return;
+
+	if (interval == 0) interval = IMAGE_AUTO_REFRESH_TIME;
+
+	imd->auto_refresh_id = g_timeout_add((guint32)interval, image_auto_refresh_cb, imd);
+	imd->auto_refresh_interval = interval;
+}
+
+/* allow top window to be resized ? */
+
+void image_top_window_set_sync(ImageWindow *imd, gint allow_sync)
+{
+	imd->top_window_sync = allow_sync;
+}
+
+/* background colors */
+
+void image_background_set_black(ImageWindow *imd, gint black)
+{
+	GtkStyle *style;
+
+	if (!imd) return;
+
+	style = gtk_style_copy(gtk_widget_get_style(imd->widget));
+	g_object_ref(G_OBJECT(style));
+
+	if (black)
+		{
+		style->bg[GTK_STATE_NORMAL] = style->black;
+		}
+
+	gtk_widget_set_style(imd->image, style);
+	g_object_unref(G_OBJECT(style));
+
+	if (GTK_WIDGET_VISIBLE(imd->widget)) image_border_clear(imd);
+}
+
+void image_background_set_color(ImageWindow *imd, GdkColor *color)
+{
+	GtkStyle *style;
+
+	if (!imd) return;
+
+	style = gtk_style_copy(gtk_widget_get_style(imd->widget));
+	g_object_ref(G_OBJECT(style));
+
+	if (color)
+		{
+		GdkColor *slot;
+
+		slot = &style->bg[GTK_STATE_NORMAL];
+
+		slot->red = color->red;
+		slot->green = color->green;
+		slot->blue = color->blue;
+		}
+
+	gtk_widget_set_style(imd->image, style);
+	g_object_unref(G_OBJECT(style));
+
+	if (GTK_WIDGET_VISIBLE(imd->widget)) image_border_clear(imd);
+}
+
+void image_set_delay_flip(ImageWindow *imd, gint delay)
+{
+	if (!imd ||
+	    imd->delay_flip == delay) return;
+
+	imd->delay_flip = delay;
+	if (!imd->delay_flip && imd->il)
+		{
+		if (imd->pixbuf) g_object_unref(imd->pixbuf);
+		imd->pixbuf = NULL;
+		image_load_pixbuf_ready(imd);
+
+		image_queue_clear(imd);
+		image_queue(imd, 0, 0, imd->width, imd->height, FALSE, TILE_RENDER_AREA, TRUE);
+		}
+}
+
+/* wallpaper util */
+
+void image_to_root_window(ImageWindow *imd, gint scaled)
+{
+	GdkScreen *screen;
 	GdkWindow *rootwindow;
 	GdkPixmap *pixmap;
-
-	if (!imd || !imd->image_data) return;
-
-
-	rootwindow = (GdkWindow *) &gdk_root_parent;	/* hmm, don't know, correct? */
-	gdkvisual = gdk_window_get_visual(rootwindow);
-	if (gdkvisual != gdk_imlib_get_visual()) return;
+	GdkPixbuf *pb;
+
+	if (!imd || !imd->pixbuf) return;
+
+
+	screen = gtk_widget_get_screen(imd->image);
+	rootwindow = gdk_screen_get_root_window(screen);
+	if (gdk_drawable_get_visual(rootwindow) != gdk_visual_get_system()) return;
 
 	if (scaled)
 		{
-		gdk_imlib_render(imd->image_data, gdk_screen_width(), gdk_screen_height());
+		pb = gdk_pixbuf_scale_simple(imd->pixbuf, gdk_screen_width(), gdk_screen_height(), (GdkInterpType)zoom_quality);
 		}
 	else
 		{
-		gdk_imlib_render(imd->image_data, imd->width, imd->height);
+		pb = gdk_pixbuf_scale_simple(imd->pixbuf, imd->width, imd->height, (GdkInterpType)zoom_quality);
 		}
 
-	pixmap = gdk_imlib_move_image(imd->image_data);
+	gdk_pixbuf_render_pixmap_and_mask (pb, &pixmap, NULL, 128);
 	gdk_window_set_back_pixmap(rootwindow, pixmap, FALSE);
 	gdk_window_clear(rootwindow);
-	gdk_imlib_free_pixmap(pixmap);
+	g_object_unref(pb);
+	g_object_unref(pixmap);
 
 	gdk_flush();
 }
 
 
+/*
+ *-------------------------------------------------------------------
+ * init / destroy
+ *-------------------------------------------------------------------
+ */
+
+static void image_free(ImageWindow *imd)
+{
+	image_read_ahead_cancel(imd);
+	image_post_buffer_set(imd, NULL, NULL);
+	image_auto_refresh(imd, -1);
+
+	g_free(imd->image_path);
+	g_free(imd->title);
+	g_free(imd->title_right);
+
+	image_reset(imd);
+	image_tile_sync_count(imd, 0);
+	if (imd->pixbuf) g_object_unref(imd->pixbuf);
+
+	image_scroller_timer_set(imd, FALSE);
+
+	image_overlay_list_clear(imd);
+
+	g_free(imd);
+}
+
+static void image_destroy_cb(GtkObject *widget, gpointer data)
+{
+	ImageWindow *imd = data;
+	image_free(imd);
+}
+
+ImageWindow *image_new(gint frame)
+{
+	ImageWindow *imd;
+
+	imd = g_new0(ImageWindow, 1);
+	imd->zoom = 1.0;
+	imd->scale = 1.0;
+
+	imd->draw_idle_id = -1;
+
+	imd->tile_width = IMAGE_TILE_SIZE;
+	imd->tile_height = IMAGE_TILE_SIZE;
+
+	imd->top_window = NULL;
+	imd->title = NULL;
+	imd->title_right = NULL;
+	imd->title_show_zoom = FALSE;
+
+	imd->unknown = TRUE;
+
+	imd->pixbuf = NULL;
+
+	imd->has_frame = frame;
+	imd->top_window_sync = FALSE;
+
+	imd->tile_cache = NULL;
+	imd->tile_cache_size = 0;
+
+	imd->delay_alter_type = ALTER_NONE;
+
+	imd->read_ahead_il = NULL;
+	imd->read_ahead_pixbuf = NULL;
+	imd->read_ahead_path = NULL;
+
+	imd->completed = FALSE;
+
+	imd->auto_refresh_id = -1;
+	imd->auto_refresh_interval = -1;
+
+	imd->delay_flip = FALSE;
+
+	imd->func_update = NULL;
+	imd->func_complete = NULL;
+
+	imd->func_button = NULL;
+	imd->func_scroll = NULL;
+
+	imd->scroller_id = -1;
+	imd->scroller_overlay = -1;
+
+	imd->image = gtk_drawing_area_new();
+	gtk_widget_set_double_buffered(imd->image, FALSE);
+
+	if (imd->has_frame)
+		{
+		imd->widget = gtk_frame_new(NULL);
+		gtk_frame_set_shadow_type(GTK_FRAME(imd->widget), GTK_SHADOW_IN);
+		gtk_container_add(GTK_CONTAINER(imd->widget), imd->image);
+		gtk_widget_show(imd->image);
+
+		GTK_WIDGET_SET_FLAGS(imd->widget, GTK_CAN_FOCUS);
+		g_signal_connect(G_OBJECT(imd->widget), "focus_in_event",
+				 G_CALLBACK(image_focus_in_cb), imd);
+		g_signal_connect(G_OBJECT(imd->widget), "focus_out_event",
+				 G_CALLBACK(image_focus_out_cb), imd);
+
+		g_signal_connect_after(G_OBJECT(imd->widget), "expose_event",
+				       G_CALLBACK(image_focus_expose), imd);
+		}
+	else
+		{
+		imd->widget = imd->image;
+		}
+
+	g_signal_connect(G_OBJECT(imd->image), "motion_notify_event",
+			 G_CALLBACK(image_mouse_motion_cb), imd);
+	g_signal_connect(G_OBJECT(imd->image), "button_press_event",
+			 G_CALLBACK(image_mouse_press_cb), imd);
+	g_signal_connect(G_OBJECT(imd->image), "button_release_event",
+			 G_CALLBACK(image_mouse_release_cb), imd);
+	g_signal_connect(G_OBJECT(imd->image), "leave_notify_event",
+			 G_CALLBACK(image_mouse_leave_cb), imd);
+	gtk_widget_set_events(imd->image, GDK_POINTER_MOTION_MASK |
+					  GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK |
+					  GDK_LEAVE_NOTIFY_MASK);
+
+	g_signal_connect(G_OBJECT(imd->image), "expose_event",
+			 G_CALLBACK(image_expose_cb), imd);
+	g_signal_connect_after(G_OBJECT(imd->image), "size_allocate",
+			 G_CALLBACK(image_size_cb), imd);
+
+	g_signal_connect(G_OBJECT(imd->image), "drag_begin",
+			 G_CALLBACK(image_mouse_drag_cb), imd);
+	g_signal_connect(G_OBJECT(imd->image), "scroll_event",
+			 G_CALLBACK(image_scroll_cb), imd);
+
+	g_signal_connect(G_OBJECT(imd->widget), "destroy",
+			 G_CALLBACK(image_destroy_cb), imd);
+
+	return imd;
+}
+