summaryrefslogtreecommitdiff
path: root/src/page-view.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/page-view.c')
-rw-r--r--src/page-view.c1170
1 files changed, 1170 insertions, 0 deletions
diff --git a/src/page-view.c b/src/page-view.c
new file mode 100644
index 0000000..2c37fc2
--- /dev/null
+++ b/src/page-view.c
@@ -0,0 +1,1170 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#include <math.h>
+
+#include "page-view.h"
+
+enum {
+ CHANGED,
+ SIZE_CHANGED,
+ LAST_SIGNAL
+};
+static guint signals[LAST_SIGNAL] = { 0, };
+
+enum {
+ PROP_0,
+ PROP_PAGE
+};
+
+typedef enum
+{
+ CROP_NONE = 0,
+ CROP_MIDDLE,
+ CROP_TOP,
+ CROP_BOTTOM,
+ CROP_LEFT,
+ CROP_RIGHT,
+ CROP_TOP_LEFT,
+ CROP_TOP_RIGHT,
+ CROP_BOTTOM_LEFT,
+ CROP_BOTTOM_RIGHT
+} CropLocation;
+
+struct PageViewPrivate
+{
+ /* Page being rendered */
+ Page *page;
+
+ /* Image to render at current resolution */
+ GdkPixbuf *image;
+
+ /* Border around image */
+ gboolean selected;
+ gint border_width;
+
+ /* True if image needs to be regenerated */
+ gboolean update_image;
+
+ /* Next scan line to render */
+ gint scan_line;
+
+ /* Dimensions of image to generate */
+ gint width, height;
+
+ /* Location to place this page */
+ gint x, y;
+
+ CropLocation crop_location;
+ gdouble selected_crop_px, selected_crop_py;
+ gint selected_crop_x, selected_crop_y;
+ gint selected_crop_w, selected_crop_h;
+
+ /* Cursor over this page */
+ gint cursor;
+
+ gint animate_n_segments, animate_segment;
+ guint animate_timeout;
+};
+
+G_DEFINE_TYPE (PageView, page_view, G_TYPE_OBJECT);
+
+
+PageView *
+page_view_new (Page *page)
+{
+ return g_object_new (PAGE_VIEW_TYPE, "page", page, NULL);
+}
+
+
+Page *
+page_view_get_page (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, NULL);
+ return view->priv->page;
+}
+
+
+void
+page_view_set_selected (PageView *view, gboolean selected)
+{
+ g_return_if_fail (view != NULL);
+ if ((view->priv->selected && selected) || (!view->priv->selected && !selected))
+ return;
+ view->priv->selected = selected;
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+gboolean page_view_get_selected (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, FALSE);
+ return view->priv->selected;
+}
+
+
+void
+page_view_set_x_offset (PageView *view, gint offset)
+{
+ g_return_if_fail (view != NULL);
+ view->priv->x = offset;
+}
+
+
+void
+page_view_set_y_offset (PageView *view, gint offset)
+{
+ g_return_if_fail (view != NULL);
+ view->priv->y = offset;
+}
+
+
+gint
+page_view_get_x_offset (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, 0);
+ return view->priv->x;
+}
+
+
+gint
+page_view_get_y_offset (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, 0);
+ return view->priv->y;
+}
+
+
+static guchar *
+get_pixel (guchar *input, gint rowstride, gint n_channels, gint x, gint y)
+{
+ return input + rowstride * y + x * n_channels;
+}
+
+
+static void
+set_pixel (guchar *input, gint rowstride, gint n_channels,
+ double l, double r, double t, double b, guchar *pixel)
+{
+ gint x, y;
+ gint L, R, T, B;
+ double scale, red, green, blue;
+
+ /* Decimation:
+ *
+ * Target pixel is defined by (t,l)-(b,r)
+ * It touches 16 pixels in original image
+ * It completely covers 4 pixels in original image (T,L)-(B,R)
+ * Add covered pixels and add weighted partially covered pixels.
+ * Divide by total area.
+ *
+ * l L R r
+ * +-----+-----+-----+-----+
+ * | | | | |
+ * t | +--+-----+-----+---+ |
+ * T +--+--+-----+-----+---+-+
+ * | | | | | | |
+ * | | | | | | |
+ * +--+--+-----+-----+---+-+
+ * | | | | | | |
+ * | | | | | | |
+ * B +--+--+-----+-----+---+-+
+ * | | | | | | |
+ * b | +--+-----+-----+---+ |
+ * +-----+-----+-----+-----+
+ *
+ *
+ * Interpolation:
+ *
+ * l r
+ * +-----+-----+-----+-----+
+ * | | | | |
+ * | | | | |
+ * +-----+-----+-----+-----+
+ * t | | +-+--+ | |
+ * | | | | | | |
+ * +-----+---+-+--+--+-----+
+ * b | | +-+--+ | |
+ * | | | | |
+ * +-----+-----+-----+-----+
+ * | | | | |
+ * | | | | |
+ * +-----+-----+-----+-----+
+ *
+ * Same again, just no completely covered pixels.
+ */
+
+ L = l;
+ if (L != l)
+ L++;
+ R = r;
+ T = t;
+ if (T != t)
+ T++;
+ B = b;
+
+ red = green = blue = 0.0;
+
+ /* Target can fit inside one source pixel
+ * +-----+
+ * | |
+ * | +--+| +-----+-----+ +-----+ +-----+ +-----+
+ * +-+--++ or | +-++ | or | +-+ | or | +--+| or | +--+
+ * | +--+| | +-++ | | +-+ | | | || | | |
+ * | | +-----+-----+ +-----+ +-+--++ +--+--+
+ * +-----+
+ */
+ if ((r - l <= 1.0 && (gint)r == (gint)l) || (b - t <= 1.0 && (gint)b == (gint)t)) {
+ /* Inside */
+ if ((gint)l == (gint)r || (gint)t == (gint)b) {
+ guchar *p = get_pixel (input, rowstride, n_channels, (gint)l, (gint)t);
+ pixel[0] = p[0];
+ pixel[1] = p[1];
+ pixel[2] = p[2];
+ return;
+ }
+
+ /* Stradling horizontal edge */
+ if (L > R) {
+ guchar *p = get_pixel (input, rowstride, n_channels, R, T-1);
+ red += p[0] * (r-l)*(T-t);
+ green += p[1] * (r-l)*(T-t);
+ blue += p[2] * (r-l)*(T-t);
+ for (y = T; y < B; y++) {
+ guchar *p = get_pixel (input, rowstride, n_channels, R, y);
+ red += p[0] * (r-l);
+ green += p[1] * (r-l);
+ blue += p[2] * (r-l);
+ }
+ p = get_pixel (input, rowstride, n_channels, R, B);
+ red += p[0] * (r-l)*(b-B);
+ green += p[1] * (r-l)*(b-B);
+ blue += p[2] * (r-l)*(b-B);
+ }
+ /* Stradling vertical edge */
+ else {
+ guchar *p = get_pixel (input, rowstride, n_channels, L - 1, B);
+ red += p[0] * (b-t)*(L-l);
+ green += p[1] * (b-t)*(L-l);
+ blue += p[2] * (b-t)*(L-l);
+ for (x = L; x < R; x++) {
+ guchar *p = get_pixel (input, rowstride, n_channels, x, B);
+ red += p[0] * (b-t);
+ green += p[1] * (b-t);
+ blue += p[2] * (b-t);
+ }
+ p = get_pixel (input, rowstride, n_channels, R, B);
+ red += p[0] * (b-t)*(r-R);
+ green += p[1] * (b-t)*(r-R);
+ blue += p[2] * (b-t)*(r-R);
+ }
+
+ scale = 1.0 / ((r - l) * (b - t));
+ pixel[0] = (guchar)(red * scale + 0.5);
+ pixel[1] = (guchar)(green * scale + 0.5);
+ pixel[2] = (guchar)(blue * scale + 0.5);
+ return;
+ }
+
+ /* Add the middle pixels */
+ for (x = L; x < R; x++) {
+ for (y = T; y < B; y++) {
+ guchar *p = get_pixel (input, rowstride, n_channels, x, y);
+ red += p[0];
+ green += p[1];
+ blue += p[2];
+ }
+ }
+
+ /* Add the weighted top and bottom pixels */
+ for (x = L; x < R; x++) {
+ if (t != T) {
+ guchar *p = get_pixel (input, rowstride, n_channels, x, T - 1);
+ red += p[0] * (T - t);
+ green += p[1] * (T - t);
+ blue += p[2] * (T - t);
+ }
+
+ if (b != B) {
+ guchar *p = get_pixel (input, rowstride, n_channels, x, B);
+ red += p[0] * (b - B);
+ green += p[1] * (b - B);
+ blue += p[2] * (b - B);
+ }
+ }
+
+ /* Add the left and right pixels */
+ for (y = T; y < B; y++) {
+ if (l != L) {
+ guchar *p = get_pixel (input, rowstride, n_channels, L - 1, y);
+ red += p[0] * (L - l);
+ green += p[1] * (L - l);
+ blue += p[2] * (L - l);
+ }
+
+ if (r != R) {
+ guchar *p = get_pixel (input, rowstride, n_channels, R, y);
+ red += p[0] * (r - R);
+ green += p[1] * (r - R);
+ blue += p[2] * (r - R);
+ }
+ }
+
+ /* Add the corner pixels */
+ if (l != L && t != T) {
+ guchar *p = get_pixel (input, rowstride, n_channels, L - 1, T - 1);
+ red += p[0] * (L - l)*(T - t);
+ green += p[1] * (L - l)*(T - t);
+ blue += p[2] * (L - l)*(T - t);
+ }
+ if (r != R && t != T) {
+ guchar *p = get_pixel (input, rowstride, n_channels, R, T - 1);
+ red += p[0] * (r - R)*(T - t);
+ green += p[1] * (r - R)*(T - t);
+ blue += p[2] * (r - R)*(T - t);
+ }
+ if (r != R && b != B) {
+ guchar *p = get_pixel (input, rowstride, n_channels, R, B);
+ red += p[0] * (r - R)*(b - B);
+ green += p[1] * (r - R)*(b - B);
+ blue += p[2] * (r - R)*(b - B);
+ }
+ if (l != L && b != B) {
+ guchar *p = get_pixel (input, rowstride, n_channels, L - 1, B);
+ red += p[0] * (L - l)*(b - B);
+ green += p[1] * (L - l)*(b - B);
+ blue += p[2] * (L - l)*(b - B);
+ }
+
+ /* Scale pixel values and clamp in range [0, 255] */
+ scale = 1.0 / ((r - l) * (b - t));
+ pixel[0] = (guchar)(red * scale + 0.5);
+ pixel[1] = (guchar)(green * scale + 0.5);
+ pixel[2] = (guchar)(blue * scale + 0.5);
+}
+
+
+static void
+update_preview (GdkPixbuf *image,
+ GdkPixbuf **output_image, gint output_width, gint output_height,
+ Orientation orientation, gint old_scan_line, gint scan_line)
+{
+ guchar *input, *output;
+ gint input_width, input_height;
+ gint input_rowstride, input_n_channels;
+ gint output_rowstride, output_n_channels;
+ gint x, y;
+ gint L, R, T, B;
+
+ input = gdk_pixbuf_get_pixels (image);
+ input_width = gdk_pixbuf_get_width (image);
+ input_height = gdk_pixbuf_get_height (image);
+ input_rowstride = gdk_pixbuf_get_rowstride (image);
+ input_n_channels = gdk_pixbuf_get_n_channels (image);
+
+ /* Create new image if one does not exist or has changed size */
+ if (!*output_image ||
+ gdk_pixbuf_get_width (*output_image) != output_width ||
+ gdk_pixbuf_get_height (*output_image) != output_height) {
+ if (*output_image)
+ g_object_unref (*output_image);
+ *output_image = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
+ FALSE,
+ 8,
+ output_width,
+ output_height);
+
+ /* Update entire image */
+ L = 0;
+ R = output_width - 1;
+ T = 0;
+ B = output_height - 1;
+ }
+ /* Otherwise only update changed area */
+ else {
+ switch (orientation) {
+ case TOP_TO_BOTTOM:
+ L = 0;
+ R = output_width - 1;
+ T = (gint)((double)old_scan_line * output_height / input_height);
+ B = (gint)((double)scan_line * output_height / input_height + 0.5);
+ break;
+ case LEFT_TO_RIGHT:
+ L = (gint)((double)old_scan_line * output_width / input_width);
+ R = (gint)((double)scan_line * output_width / input_width + 0.5);
+ T = 0;
+ B = output_height - 1;
+ break;
+ case BOTTOM_TO_TOP:
+ L = 0;
+ R = output_width - 1;
+ T = (gint)((double)(input_height - scan_line) * output_height / input_height);
+ B = (gint)((double)(input_height - old_scan_line) * output_height / input_height + 0.5);
+ break;
+ case RIGHT_TO_LEFT:
+ L = (gint)((double)(input_width - scan_line) * output_width / input_width);
+ R = (gint)((double)(input_width - old_scan_line) * output_width / input_width + 0.5);
+ T = 0;
+ B = output_height - 1;
+ break;
+ default:
+ L = R = B = T = 0;
+ break;
+ }
+ }
+
+ /* FIXME: There's an off by one error in there somewhere... */
+ if (R >= output_width)
+ R = output_width - 1;
+ if (B >= output_height)
+ B = output_height - 1;
+
+ g_return_if_fail (L >= 0);
+ g_return_if_fail (R < output_width);
+ g_return_if_fail (T >= 0);
+ g_return_if_fail (B < output_height);
+ g_return_if_fail (*output_image != NULL);
+
+ output = gdk_pixbuf_get_pixels (*output_image);
+ output_rowstride = gdk_pixbuf_get_rowstride (*output_image);
+ output_n_channels = gdk_pixbuf_get_n_channels (*output_image);
+
+ /* Update changed area */
+ for (x = L; x <= R; x++) {
+ double l, r;
+
+ l = (double)x * input_width / output_width;
+ r = (double)(x + 1) * input_width / output_width;
+
+ for (y = T; y <= B; y++) {
+ double t, b;
+
+ t = (double)y * input_height / output_height;
+ b = (double)(y + 1) * input_height / output_height;
+
+ set_pixel (input, input_rowstride, input_n_channels,
+ l, r, t, b,
+ get_pixel (output, output_rowstride, output_n_channels, x, y));
+ }
+ }
+}
+
+
+static gint
+get_preview_width (PageView *view)
+{
+ return view->priv->width - view->priv->border_width * 2;
+}
+
+
+static gint
+get_preview_height (PageView *view)
+{
+ return view->priv->height - view->priv->border_width * 2;
+}
+
+
+static void
+update_page_view (PageView *view)
+{
+ GdkPixbuf *image;
+ gint old_scan_line, scan_line;
+
+ if (!view->priv->update_image)
+ return;
+
+ image = page_get_image (view->priv->page);
+ old_scan_line = view->priv->scan_line;
+ scan_line = page_get_scan_line (view->priv->page);
+
+ update_preview (image,
+ &view->priv->image,
+ get_preview_width (view),
+ get_preview_height (view),
+ page_get_orientation (view->priv->page), old_scan_line, scan_line);
+ g_object_unref (image);
+
+ view->priv->update_image = FALSE;
+ view->priv->scan_line = scan_line;
+}
+
+
+static gint
+page_to_screen_x (PageView *view, gint x)
+{
+ return (double) x * get_preview_width (view) / page_get_width (view->priv->page) + 0.5;
+}
+
+
+static gint
+page_to_screen_y (PageView *view, gint y)
+{
+ return (double) y * get_preview_height (view) / page_get_height (view->priv->page) + 0.5;
+}
+
+
+static gint
+screen_to_page_x (PageView *view, gint x)
+{
+ return (double) x * page_get_width (view->priv->page) / get_preview_width (view) + 0.5;
+}
+
+
+static gint
+screen_to_page_y (PageView *view, gint y)
+{
+ return (double) y * page_get_height (view->priv->page) / get_preview_height (view) + 0.5;
+}
+
+
+static CropLocation
+get_crop_location (PageView *view, gint x, gint y)
+{
+ gint cx, cy, cw, ch;
+ gint dx, dy, dw, dh;
+ gint ix, iy;
+ gint crop_border = 20;
+ gchar *name;
+
+ if (!page_has_crop (view->priv->page))
+ return 0;
+
+ page_get_crop (view->priv->page, &cx, &cy, &cw, &ch);
+ dx = page_to_screen_x (view, cx);
+ dy = page_to_screen_y (view, cy);
+ dw = page_to_screen_x (view, cw);
+ dh = page_to_screen_y (view, ch);
+ ix = x - dx;
+ iy = y - dy;
+
+ if (ix < 0 || ix > dw || iy < 0 || iy > dh)
+ return CROP_NONE;
+
+ /* Can't resize named crops */
+ name = page_get_named_crop (view->priv->page);
+ if (name != NULL) {
+ g_free (name);
+ return CROP_MIDDLE;
+ }
+
+ /* Adjust borders so can select */
+ if (dw < crop_border * 3)
+ crop_border = dw / 3;
+ if (dh < crop_border * 3)
+ crop_border = dh / 3;
+
+ /* Top left */
+ if (ix < crop_border && iy < crop_border)
+ return CROP_TOP_LEFT;
+ /* Top right */
+ if (ix > dw - crop_border && iy < crop_border)
+ return CROP_TOP_RIGHT;
+ /* Bottom left */
+ if (ix < crop_border && iy > dh - crop_border)
+ return CROP_BOTTOM_LEFT;
+ /* Bottom right */
+ if (ix > dw - crop_border && iy > dh - crop_border)
+ return CROP_BOTTOM_RIGHT;
+
+ /* Left */
+ if (ix < crop_border)
+ return CROP_LEFT;
+ /* Right */
+ if (ix > dw - crop_border)
+ return CROP_RIGHT;
+ /* Top */
+ if (iy < crop_border)
+ return CROP_TOP;
+ /* Bottom */
+ if (iy > dh - crop_border)
+ return CROP_BOTTOM;
+
+ /* In the middle */
+ return CROP_MIDDLE;
+}
+
+
+void
+page_view_button_press (PageView *view, gint x, gint y)
+{
+ CropLocation location;
+
+ g_return_if_fail (view != NULL);
+
+ /* See if selecting crop */
+ location = get_crop_location (view, x, y);;
+ if (location != CROP_NONE) {
+ view->priv->crop_location = location;
+ view->priv->selected_crop_px = x;
+ view->priv->selected_crop_py = y;
+ page_get_crop (view->priv->page,
+ &view->priv->selected_crop_x,
+ &view->priv->selected_crop_y,
+ &view->priv->selected_crop_w,
+ &view->priv->selected_crop_h);
+ }
+}
+
+
+void
+page_view_motion (PageView *view, gint x, gint y)
+{
+ gint pw, ph;
+ gint cx, cy, cw, ch, dx, dy;
+ gint new_x, new_y, new_w, new_h;
+ CropLocation location;
+ gint cursor;
+ gint min_size;
+
+ min_size = screen_to_page_x (view, 15);
+
+ g_return_if_fail (view != NULL);
+
+ location = get_crop_location (view, x, y);
+ switch (location) {
+ case CROP_MIDDLE:
+ cursor = GDK_HAND1;
+ break;
+ case CROP_TOP:
+ cursor = GDK_TOP_SIDE;
+ break;
+ case CROP_BOTTOM:
+ cursor = GDK_BOTTOM_SIDE;
+ break;
+ case CROP_LEFT:
+ cursor = GDK_LEFT_SIDE;
+ break;
+ case CROP_RIGHT:
+ cursor = GDK_RIGHT_SIDE;
+ break;
+ case CROP_TOP_LEFT:
+ cursor = GDK_TOP_LEFT_CORNER;
+ break;
+ case CROP_TOP_RIGHT:
+ cursor = GDK_TOP_RIGHT_CORNER;
+ break;
+ case CROP_BOTTOM_LEFT:
+ cursor = GDK_BOTTOM_LEFT_CORNER;
+ break;
+ case CROP_BOTTOM_RIGHT:
+ cursor = GDK_BOTTOM_RIGHT_CORNER;
+ break;
+ default:
+ cursor = GDK_ARROW;
+ break;
+ }
+
+ if (view->priv->crop_location == CROP_NONE) {
+ view->priv->cursor = cursor;
+ return;
+ }
+
+ /* Move the crop */
+ pw = page_get_width (view->priv->page);
+ ph = page_get_height (view->priv->page);
+ page_get_crop (view->priv->page, &cx, &cy, &cw, &ch);
+
+ dx = screen_to_page_x (view, x - view->priv->selected_crop_px);
+ dy = screen_to_page_y (view, y - view->priv->selected_crop_py);
+
+ new_x = view->priv->selected_crop_x;
+ new_y = view->priv->selected_crop_y;
+ new_w = view->priv->selected_crop_w;
+ new_h = view->priv->selected_crop_h;
+
+ /* Limit motion to remain within page and minimum crop size */
+ if (view->priv->crop_location == CROP_TOP_LEFT ||
+ view->priv->crop_location == CROP_LEFT ||
+ view->priv->crop_location == CROP_BOTTOM_LEFT) {
+ if (dx > new_w - min_size)
+ dx = new_w - min_size;
+ if (new_x + dx < 0)
+ dx = -new_x;
+ }
+ if (view->priv->crop_location == CROP_TOP_LEFT ||
+ view->priv->crop_location == CROP_TOP ||
+ view->priv->crop_location == CROP_TOP_RIGHT) {
+ if (dy > new_h - min_size)
+ dy = new_h - min_size;
+ if (new_y + dy < 0)
+ dy = -new_y;
+ }
+
+ if (view->priv->crop_location == CROP_TOP_RIGHT ||
+ view->priv->crop_location == CROP_RIGHT ||
+ view->priv->crop_location == CROP_BOTTOM_RIGHT) {
+ if (dx < min_size - new_w)
+ dx = min_size - new_w;
+ if (new_x + new_w + dx > pw)
+ dx = pw - new_x - new_w;
+ }
+ if (view->priv->crop_location == CROP_BOTTOM_LEFT ||
+ view->priv->crop_location == CROP_BOTTOM ||
+ view->priv->crop_location == CROP_BOTTOM_RIGHT) {
+ if (dy < min_size - new_h)
+ dy = min_size - new_h;
+ if (new_y + new_h + dy > ph)
+ dy = ph - new_y - new_h;
+ }
+ if (view->priv->crop_location == CROP_MIDDLE) {
+ if (new_x + dx + new_w > pw)
+ dx = pw - new_x - new_w;
+ if (new_x + dx < 0)
+ dx = -new_x;
+ if (new_y + dy + new_h > ph)
+ dy = ph - new_y - new_h;
+ if (new_y + dy < 0)
+ dy = -new_y;
+ }
+
+ /* Move crop */
+ if (view->priv->crop_location == CROP_MIDDLE) {
+ new_x += dx;
+ new_y += dy;
+ }
+ if (view->priv->crop_location == CROP_TOP_LEFT ||
+ view->priv->crop_location == CROP_LEFT ||
+ view->priv->crop_location == CROP_BOTTOM_LEFT)
+ {
+ new_x += dx;
+ new_w -= dx;
+ }
+ if (view->priv->crop_location == CROP_TOP_LEFT ||
+ view->priv->crop_location == CROP_TOP ||
+ view->priv->crop_location == CROP_TOP_RIGHT) {
+ new_y += dy;
+ new_h -= dy;
+ }
+
+ if (view->priv->crop_location == CROP_TOP_RIGHT ||
+ view->priv->crop_location == CROP_RIGHT ||
+ view->priv->crop_location == CROP_BOTTOM_RIGHT) {
+ new_w += dx;
+ }
+ if (view->priv->crop_location == CROP_BOTTOM_LEFT ||
+ view->priv->crop_location == CROP_BOTTOM ||
+ view->priv->crop_location == CROP_BOTTOM_RIGHT) {
+ new_h += dy;
+ }
+
+ page_move_crop (view->priv->page, new_x, new_y);
+
+ /* If reshaped crop, must be a custom crop */
+ if (new_w != cw || new_h != ch)
+ page_set_custom_crop (view->priv->page, new_w, new_h);
+}
+
+
+void
+page_view_button_release (PageView *view, gint x, gint y)
+{
+ g_return_if_fail (view != NULL);
+
+ /* Complete crop */
+ view->priv->crop_location = CROP_NONE;
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+gint
+page_view_get_cursor (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, 0);
+ return view->priv->cursor;
+}
+
+
+static gboolean
+animation_cb (PageView *view)
+{
+ view->priv->animate_segment = (view->priv->animate_segment + 1) % view->priv->animate_n_segments;
+ g_signal_emit (view, signals[CHANGED], 0);
+ return TRUE;
+}
+
+
+static void
+update_animation (PageView *view)
+{
+ gboolean animate, is_animating;
+
+ animate = page_is_scanning (view->priv->page) && !page_has_data (view->priv->page);
+ is_animating = view->priv->animate_timeout != 0;
+ if (animate == is_animating)
+ return;
+
+ if (animate) {
+ view->priv->animate_segment = 0;
+ if (view->priv->animate_timeout == 0)
+ view->priv->animate_timeout = g_timeout_add (150, (GSourceFunc) animation_cb, view);
+ }
+ else
+ {
+ if (view->priv->animate_timeout != 0)
+ g_source_remove (view->priv->animate_timeout);
+ view->priv->animate_timeout = 0;
+ }
+}
+
+
+void
+page_view_render (PageView *view, cairo_t *context)
+{
+ gint width, height;
+
+ g_return_if_fail (view != NULL);
+ g_return_if_fail (context != NULL);
+
+ update_animation (view);
+ update_page_view (view);
+
+ width = get_preview_width (view);
+ height = get_preview_height (view);
+
+ cairo_set_line_width (context, 1);
+ cairo_translate (context, view->priv->x, view->priv->y);
+
+ /* Draw page border */
+ cairo_set_source_rgb (context, 0, 0, 0);
+ cairo_set_line_width (context, view->priv->border_width);
+ cairo_rectangle (context,
+ (double)view->priv->border_width / 2,
+ (double)view->priv->border_width / 2,
+ view->priv->width - view->priv->border_width,
+ view->priv->height - view->priv->border_width);
+ cairo_stroke (context);
+
+ /* Draw image */
+ cairo_translate (context, view->priv->border_width, view->priv->border_width);
+ if (view->priv->image) {
+ gdk_cairo_set_source_pixbuf (context, view->priv->image, 0, 0);
+ cairo_paint (context);
+ }
+ else {
+ cairo_scale (context,
+ (double) get_preview_width (view) / page_get_width (view->priv->page),
+ (double) get_preview_height (view) / page_get_height (view->priv->page));
+ gdk_cairo_set_source_pixbuf (context, page_get_image (view->priv->page), 0, 0);
+
+ cairo_paint (context);
+ }
+
+ /* Draw throbber */
+ if (page_is_scanning (view->priv->page) && !page_has_data (view->priv->page)) {
+ gdouble inner_radius, outer_radius, x, y, arc, offset = 0.0;
+ gint i;
+
+ if (width > height)
+ outer_radius = 0.15 * width;
+ else
+ outer_radius = 0.15 * height;
+ arc = M_PI / view->priv->animate_n_segments;
+
+ /* Space circles */
+ x = outer_radius * sin (arc);
+ y = outer_radius * (cos (arc) - 1.0);
+ inner_radius = 0.6 * sqrt (x*x + y*y);
+
+ for (i = 0; i < view->priv->animate_n_segments; i++, offset += arc * 2) {
+ x = width / 2 + outer_radius * sin (offset);
+ y = height / 2 - outer_radius * cos (offset);
+ cairo_arc (context, x, y, inner_radius, 0, 2 * M_PI);
+
+ if (i == view->priv->animate_segment) {
+ cairo_set_source_rgb (context, 0.75, 0.75, 0.75);
+ cairo_fill_preserve (context);
+ }
+
+ cairo_set_source_rgb (context, 0.5, 0.5, 0.5);
+ cairo_stroke (context);
+ }
+ }
+
+ /* Draw scan line */
+ if (page_is_scanning (view->priv->page) && page_get_scan_line (view->priv->page) > 0) {
+ gint scan_line;
+ double s;
+ double x1, y1, x2, y2;
+
+ scan_line = page_get_scan_line (view->priv->page);
+
+ switch (page_get_orientation (view->priv->page)) {
+ case TOP_TO_BOTTOM:
+ s = page_to_screen_y (view, scan_line);
+ x1 = 0; y1 = s + 0.5;
+ x2 = width; y2 = s + 0.5;
+ break;
+ case BOTTOM_TO_TOP:
+ s = page_to_screen_y (view, scan_line);
+ x1 = 0; y1 = height - s + 0.5;
+ x2 = width; y2 = height - s + 0.5;
+ break;
+ case LEFT_TO_RIGHT:
+ s = page_to_screen_x (view, scan_line);
+ x1 = s + 0.5; y1 = 0;
+ x2 = s + 0.5; y2 = height;
+ break;
+ case RIGHT_TO_LEFT:
+ s = page_to_screen_x (view, scan_line);
+ x1 = width - s + 0.5; y1 = 0;
+ x2 = width - s + 0.5; y2 = height;
+ break;
+ default:
+ x1 = y1 = x2 = y2 = 0;
+ break;
+ }
+
+ cairo_move_to (context, x1, y1);
+ cairo_line_to (context, x2, y2);
+ cairo_set_source_rgb (context, 1.0, 0.0, 0.0);
+ cairo_stroke (context);
+ }
+
+ /* Draw crop */
+ if (page_has_crop (view->priv->page)) {
+ gint x, y, crop_width, crop_height;
+ gdouble dx, dy, dw, dh;
+
+ page_get_crop (view->priv->page, &x, &y, &crop_width, &crop_height);
+
+ dx = page_to_screen_x (view, x);
+ dy = page_to_screen_y (view, y);
+ dw = page_to_screen_x (view, crop_width);
+ dh = page_to_screen_y (view, crop_height);
+
+ /* Shade out cropped area */
+ cairo_rectangle (context,
+ 0, 0,
+ width, height);
+ cairo_new_sub_path (context);
+ cairo_rectangle (context, dx, dy, dw, dh);
+ cairo_set_fill_rule (context, CAIRO_FILL_RULE_EVEN_ODD);
+ cairo_set_source_rgba (context, 0.25, 0.25, 0.25, 0.2);
+ cairo_fill (context);
+
+ /* Show new edge */
+ cairo_rectangle (context, dx - 1.5, dy - 1.5, dw + 3, dh + 3);
+ cairo_set_source_rgb (context, 1.0, 1.0, 1.0);
+ cairo_stroke (context);
+ cairo_rectangle (context, dx - 0.5, dy - 0.5, dw + 1, dh + 1);
+ cairo_set_source_rgb (context, 0.0, 0.0, 0.0);
+ cairo_stroke (context);
+ }
+}
+
+
+void
+page_view_set_width (PageView *view, gint width)
+{
+ gint height;
+
+ g_return_if_fail (view != NULL);
+
+ // FIXME: Automatically update when get updated image
+ height = (double)width * page_get_height (view->priv->page) / page_get_width (view->priv->page);
+ if (view->priv->width == width && view->priv->height == height)
+ return;
+
+ view->priv->width = width;
+ view->priv->height = height;
+
+ /* Regenerate image */
+ view->priv->update_image = TRUE;
+
+ g_signal_emit (view, signals[SIZE_CHANGED], 0);
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+void
+page_view_set_height (PageView *view, gint height)
+{
+ gint width;
+
+ g_return_if_fail (view != NULL);
+
+ // FIXME: Automatically update when get updated image
+ width = (double)height * page_get_width (view->priv->page) / page_get_height (view->priv->page);
+ if (view->priv->width == width && view->priv->height == height)
+ return;
+
+ view->priv->width = width;
+ view->priv->height = height;
+
+ /* Regenerate image */
+ view->priv->update_image = TRUE;
+
+ g_signal_emit (view, signals[SIZE_CHANGED], 0);
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+gint
+page_view_get_width (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, 0);
+ return view->priv->width;
+}
+
+
+gint
+page_view_get_height (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, 0);
+ return view->priv->height;
+}
+
+
+static void
+page_image_changed_cb (Page *p, PageView *view)
+{
+ /* Regenerate image */
+ view->priv->update_image = TRUE;
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+static void
+page_size_changed_cb (Page *p, PageView *view)
+{
+ /* Regenerate image */
+ view->priv->update_image = TRUE;
+ g_signal_emit (view, signals[SIZE_CHANGED], 0);
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+static void
+page_overlay_changed_cb (Page *p, PageView *view)
+{
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+static void
+page_view_set_page (PageView *view, Page *page)
+{
+ g_return_if_fail (view != NULL);
+ g_return_if_fail (view->priv->page == NULL);
+
+ view->priv->page = g_object_ref (page);
+ g_signal_connect (view->priv->page, "image-changed", G_CALLBACK (page_image_changed_cb), view);
+ g_signal_connect (view->priv->page, "size-changed", G_CALLBACK (page_size_changed_cb), view);
+ g_signal_connect (view->priv->page, "crop-changed", G_CALLBACK (page_overlay_changed_cb), view);
+ g_signal_connect (view->priv->page, "scan-line-changed", G_CALLBACK (page_overlay_changed_cb), view);
+}
+
+
+static void
+page_view_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ PageView *self;
+
+ self = PAGE_VIEW (object);
+
+ switch (prop_id) {
+ case PROP_PAGE:
+ page_view_set_page (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+
+static void
+page_view_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ PageView *self;
+
+ self = PAGE_VIEW (object);
+
+ switch (prop_id) {
+ case PROP_PAGE:
+ g_value_set_object (value, self->priv->page);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+
+static void
+page_view_finalize (GObject *object)
+{
+ PageView *view = PAGE_VIEW (object);
+ g_object_unref (view->priv->page);
+ view->priv->page = NULL;
+ if (view->priv->image)
+ g_object_unref (view->priv->image);
+ view->priv->image = NULL;
+ if (view->priv->animate_timeout != 0)
+ g_source_remove (view->priv->animate_timeout);
+ view->priv->animate_timeout = 0;
+ G_OBJECT_CLASS (page_view_parent_class)->finalize (object);
+}
+
+
+static void
+page_view_class_init (PageViewClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = page_view_get_property;
+ object_class->set_property = page_view_set_property;
+ object_class->finalize = page_view_finalize;
+
+ signals[CHANGED] =
+ g_signal_new ("changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (PageViewClass, changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[SIZE_CHANGED] =
+ g_signal_new ("size-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (PageViewClass, size_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ g_type_class_add_private (klass, sizeof (PageViewPrivate));
+
+ g_object_class_install_property (object_class,
+ PROP_PAGE,
+ g_param_spec_object ("page",
+ "page",
+ "Page being rendered",
+ PAGE_TYPE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+}
+
+
+static void
+page_view_init (PageView *view)
+{
+ view->priv = G_TYPE_INSTANCE_GET_PRIVATE (view, PAGE_VIEW_TYPE, PageViewPrivate);
+ view->priv->update_image = TRUE;
+ view->priv->cursor = GDK_ARROW;
+ view->priv->border_width = 1;
+ view->priv->animate_n_segments = 7;
+}