summaryrefslogtreecommitdiff
path: root/src/page.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/page.vala')
-rw-r--r--src/page.vala711
1 files changed, 711 insertions, 0 deletions
diff --git a/src/page.vala b/src/page.vala
new file mode 100644
index 0000000..b375723
--- /dev/null
+++ b/src/page.vala
@@ -0,0 +1,711 @@
+/*
+ * Copyright (C) 2009-2011 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.
+ */
+
+public enum ScanDirection
+{
+ TOP_TO_BOTTOM,
+ LEFT_TO_RIGHT,
+ BOTTOM_TO_TOP,
+ RIGHT_TO_LEFT
+}
+
+public class Page
+{
+ /* Width of the page in pixels after rotation applied */
+ public int width
+ {
+ get
+ {
+ if (scan_direction == ScanDirection.TOP_TO_BOTTOM || scan_direction == ScanDirection.BOTTOM_TO_TOP)
+ return scan_width;
+ else
+ return scan_height;
+ }
+ }
+
+ /* Height of the page in pixels after rotation applied */
+ public int height
+ {
+ get
+ {
+ if (scan_direction == ScanDirection.TOP_TO_BOTTOM || scan_direction == ScanDirection.BOTTOM_TO_TOP)
+ return scan_height;
+ else
+ return scan_width;
+ }
+ }
+
+ /* true if the page is landscape (wider than the height) */
+ public bool is_landscape { get { return width > height; } }
+
+ /* Resolution of page */
+ public int dpi { get; private set; }
+
+ /* Number of rows in this page or -1 if currently unknown */
+ private int expected_rows;
+
+ /* Bit depth */
+ public int depth { get; private set; }
+
+ /* Color profile */
+ public string? color_profile { get; set; }
+
+ /* Width of raw scan data in pixels */
+ public int scan_width { get; private set; }
+
+ /* Height of raw scan data in pixels */
+ public int scan_height { get; private set; }
+
+ /* Offset between rows in scan data */
+ public int rowstride { get; private set; }
+
+ /* Number of color channels */
+ public int n_channels { get; private set; }
+
+ /* Pixel data */
+ private uchar[] pixels;
+
+ /* Page is getting data */
+ public bool is_scanning { get; private set; }
+
+ /* true if have some page data */
+ public bool has_data { get; private set; }
+
+ /* Expected next scan row */
+ public int scan_line { get; private set; }
+
+ /* true if scan contains color information */
+ public bool is_color { get { return n_channels > 1; } }
+
+ /* Rotation of scanned data */
+ private ScanDirection scan_direction_;
+ public ScanDirection scan_direction
+ {
+ get { return scan_direction_; }
+
+ set
+ {
+ if (scan_direction_ == value)
+ return;
+
+ /* Work out how many times it has been rotated to the left */
+ var size_has_changed = false;
+ var left_steps = (int) (value - scan_direction_);
+ if (left_steps < 0)
+ left_steps += 4;
+ if (left_steps != 2)
+ size_has_changed = true;
+
+ /* Rotate crop */
+ if (has_crop)
+ {
+ switch (left_steps)
+ {
+ /* 90 degrees counter-clockwise */
+ case 1:
+ var t = crop_x;
+ crop_x = crop_y;
+ crop_y = width - (t + crop_width);
+ t = crop_width;
+ crop_width = crop_height;
+ crop_height = t;
+ break;
+ /* 180 degrees */
+ case 2:
+ crop_x = width - (crop_x + crop_width);
+ crop_y = width - (crop_y + crop_height);
+ break;
+ /* 90 degrees clockwise */
+ case 3:
+ var t = crop_y;
+ crop_y = crop_x;
+ crop_x = height - (t + crop_height);
+ t = crop_width;
+ crop_width = crop_height;
+ crop_height = t;
+ break;
+ }
+ }
+
+ scan_direction_ = value;
+ if (size_has_changed)
+ size_changed ();
+ scan_direction_changed ();
+ if (has_crop)
+ crop_changed ();
+ }
+
+ default = ScanDirection.TOP_TO_BOTTOM;
+ }
+
+ /* True if the page has a crop set */
+ public bool has_crop { get; private set; }
+
+ /* Name of the crop if using a named crop */
+ public string? crop_name { get; private set; }
+
+ /* X co-ordinate of top left crop corner */
+ public int crop_x { get; private set; }
+
+ /* Y co-ordinate of top left crop corner */
+ public int crop_y { get; private set; }
+
+ /* Width of crop in pixels */
+ public int crop_width { get; private set; }
+
+ /* Height of crop in pixels*/
+ public int crop_height { get; private set; }
+
+ public signal void pixels_changed ();
+ public signal void size_changed ();
+ public signal void scan_line_changed ();
+ public signal void scan_direction_changed ();
+ public signal void crop_changed ();
+ public signal void scan_finished ();
+
+ public Page (int width, int height, int dpi, ScanDirection scan_direction)
+ {
+ if (scan_direction == ScanDirection.TOP_TO_BOTTOM || scan_direction == ScanDirection.BOTTOM_TO_TOP)
+ {
+ scan_width = width;
+ scan_height = height;
+ }
+ else
+ {
+ scan_width = height;
+ scan_height = width;
+ }
+ this.dpi = dpi;
+ this.scan_direction = scan_direction;
+ }
+
+ public Page.from_data (int scan_width,
+ int scan_height,
+ int rowstride,
+ int n_channels,
+ int depth,
+ int dpi,
+ ScanDirection scan_direction,
+ string? color_profile,
+ uchar[]? pixels,
+ bool has_crop,
+ string? crop_name,
+ int crop_x,
+ int crop_y,
+ int crop_width,
+ int crop_height)
+ {
+ this.scan_width = scan_width;
+ this.scan_height = scan_height;
+ this.expected_rows = scan_height;
+ this.rowstride = rowstride;
+ this.n_channels = n_channels;
+ this.depth = depth;
+ this.dpi = dpi;
+ this.scan_direction = scan_direction;
+ this.color_profile = color_profile;
+ this.pixels = pixels;
+ has_data = pixels != null;
+ this.has_crop = has_crop;
+ this.crop_name = crop_name;
+ this.crop_x = crop_x;
+ this.crop_y = crop_y;
+ this.crop_width = crop_width;
+ this.crop_height = crop_height;
+ }
+
+ public void set_page_info (ScanPageInfo info)
+ {
+ expected_rows = info.height;
+ dpi = (int) info.dpi;
+
+ /* Create a white page */
+ scan_width = info.width;
+ scan_height = info.height;
+ /* Variable height, try 50% of the width for now */
+ if (scan_height < 0)
+ scan_height = scan_width / 2;
+ depth = info.depth;
+ n_channels = info.n_channels;
+ rowstride = (scan_width * depth * n_channels + 7) / 8;
+ pixels.resize (scan_height * rowstride);
+ return_if_fail (pixels != null);
+
+ /* Fill with white */
+ if (depth == 1)
+ Memory.set (pixels, 0x00, scan_height * rowstride);
+ else
+ Memory.set (pixels, 0xFF, scan_height * rowstride);
+
+ size_changed ();
+ pixels_changed ();
+ }
+
+ public void start ()
+ {
+ is_scanning = true;
+ scan_line_changed ();
+ }
+
+ private void parse_line (ScanLine line, int n, out bool size_changed)
+ {
+ var line_number = line.number + n;
+
+ /* Extend image if necessary */
+ size_changed = false;
+ while (line_number >= scan_height)
+ {
+ /* Extend image */
+ var rows = scan_height;
+ scan_height = rows + scan_width / 2;
+ debug ("Extending image from %d lines to %d lines", rows, scan_height);
+ pixels.resize (scan_height * rowstride);
+
+ size_changed = true;
+ }
+
+ /* Copy in new row */
+ var offset = line_number * rowstride;
+ var line_offset = n * line.data_length;
+ for (var i = 0; i < line.data_length; i++)
+ pixels[offset+i] = line.data[line_offset+i];
+
+ scan_line = line_number;
+ }
+
+ public void parse_scan_line (ScanLine line)
+ {
+ bool size_has_changed = false;
+ for (var i = 0; i < line.n_lines; i++)
+ parse_line (line, i, out size_has_changed);
+
+ has_data = true;
+
+ if (size_has_changed)
+ size_changed ();
+ scan_line_changed ();
+ pixels_changed ();
+ }
+
+ public void finish ()
+ {
+ bool size_has_changed = false;
+
+ /* Trim page */
+ if (expected_rows < 0 &&
+ scan_line != scan_height)
+ {
+ var rows = scan_height;
+ scan_height = scan_line;
+ pixels.resize (scan_height * rowstride);
+ debug ("Trimming page from %d lines to %d lines", rows, scan_height);
+
+ size_has_changed = true;
+ }
+ is_scanning = false;
+
+ if (size_has_changed)
+ size_changed ();
+ scan_line_changed ();
+ scan_finished ();
+ }
+
+ public void rotate_left ()
+ {
+ switch (scan_direction)
+ {
+ case ScanDirection.TOP_TO_BOTTOM:
+ scan_direction = ScanDirection.LEFT_TO_RIGHT;
+ break;
+ case ScanDirection.LEFT_TO_RIGHT:
+ scan_direction = ScanDirection.BOTTOM_TO_TOP;
+ break;
+ case ScanDirection.BOTTOM_TO_TOP:
+ scan_direction = ScanDirection.RIGHT_TO_LEFT;
+ break;
+ case ScanDirection.RIGHT_TO_LEFT:
+ scan_direction = ScanDirection.TOP_TO_BOTTOM;
+ break;
+ }
+ }
+
+ public void rotate_right ()
+ {
+ switch (scan_direction)
+ {
+ case ScanDirection.TOP_TO_BOTTOM:
+ scan_direction = ScanDirection.RIGHT_TO_LEFT;
+ break;
+ case ScanDirection.LEFT_TO_RIGHT:
+ scan_direction = ScanDirection.TOP_TO_BOTTOM;
+ break;
+ case ScanDirection.BOTTOM_TO_TOP:
+ scan_direction = ScanDirection.LEFT_TO_RIGHT;
+ break;
+ case ScanDirection.RIGHT_TO_LEFT:
+ scan_direction = ScanDirection.BOTTOM_TO_TOP;
+ break;
+ }
+ }
+
+ public void set_no_crop ()
+ {
+ if (!has_crop)
+ return;
+ has_crop = false;
+ crop_name = null;
+ crop_x = 0;
+ crop_y = 0;
+ crop_width = 0;
+ crop_height = 0;
+ crop_changed ();
+ }
+
+ public void set_custom_crop (int width, int height)
+ {
+ return_if_fail (width >= 1);
+ return_if_fail (height >= 1);
+
+ if (crop_name == null && has_crop && crop_width == width && crop_height == height)
+ return;
+ crop_name = null;
+ has_crop = true;
+
+ crop_width = width;
+ crop_height = height;
+
+ /*var pw = width;
+ var ph = height;
+ if (crop_width < pw)
+ crop_x = (pw - crop_width) / 2;
+ else
+ crop_x = 0;
+ if (crop_height < ph)
+ crop_y = (ph - crop_height) / 2;
+ else
+ crop_y = 0;*/
+
+ crop_changed ();
+ }
+
+ public void set_named_crop (string name)
+ {
+ double w, h;
+ switch (name)
+ {
+ case "A4":
+ w = 8.3;
+ h = 11.7;
+ break;
+ case "A5":
+ w = 5.8;
+ h = 8.3;
+ break;
+ case "A6":
+ w = 4.1;
+ h = 5.8;
+ break;
+ case "letter":
+ w = 8.5;
+ h = 11;
+ break;
+ case "legal":
+ w = 8.5;
+ h = 14;
+ break;
+ case "4x6":
+ w = 4;
+ h = 6;
+ break;
+ default:
+ warning ("Unknown paper size '%s'", name);
+ return;
+ }
+
+ crop_name = name;
+ has_crop = true;
+
+ var pw = width;
+ var ph = height;
+
+ /* Rotate to match original aspect */
+ if (pw > ph)
+ {
+ var t = w;
+ w = h;
+ h = t;
+ }
+
+ /* Custom crop, make slightly smaller than original */
+ crop_width = (int) (w * dpi + 0.5);
+ crop_height = (int) (h * dpi + 0.5);
+
+ if (crop_width < pw)
+ crop_x = (pw - crop_width) / 2;
+ else
+ crop_x = 0;
+ if (crop_height < ph)
+ crop_y = (ph - crop_height) / 2;
+ else
+ crop_y = 0;
+ crop_changed ();
+ }
+
+ public void move_crop (int x, int y)
+ {
+ return_if_fail (x >= 0);
+ return_if_fail (y >= 0);
+ return_if_fail (x < width);
+ return_if_fail (y < height);
+
+ crop_x = x;
+ crop_y = y;
+ crop_changed ();
+ }
+
+ public void rotate_crop ()
+ {
+ if (!has_crop)
+ return;
+
+ var t = crop_width;
+ crop_width = crop_height;
+ crop_height = t;
+
+ /* Clip custom crops */
+ if (crop_name == null)
+ {
+ var w = width;
+ var h = height;
+
+ if (crop_x + crop_width > w)
+ crop_x = w - crop_width;
+ if (crop_x < 0)
+ {
+ crop_x = 0;
+ crop_width = w;
+ }
+ if (crop_y + crop_height > h)
+ crop_y = h - crop_height;
+ if (crop_y < 0)
+ {
+ crop_y = 0;
+ crop_height = h;
+ }
+ }
+
+ crop_changed ();
+ }
+
+ public unowned uchar[] get_pixels ()
+ {
+ return pixels;
+ }
+
+ // FIXME: Copied from page-view, should be shared code
+ private uchar get_sample (uchar[] pixels, int offset, int x, int depth, int n_channels, int channel)
+ {
+ // FIXME
+ return 0xFF;
+ }
+
+ // FIXME: Copied from page-view, should be shared code
+ private void get_pixel (int x, int y, uchar[] pixel, int offset)
+ {
+ switch (scan_direction)
+ {
+ case ScanDirection.TOP_TO_BOTTOM:
+ break;
+ case ScanDirection.BOTTOM_TO_TOP:
+ x = scan_width - x - 1;
+ y = scan_height - y - 1;
+ break;
+ case ScanDirection.LEFT_TO_RIGHT:
+ var t = x;
+ x = scan_width - y - 1;
+ y = t;
+ break;
+ case ScanDirection.RIGHT_TO_LEFT:
+ var t = x;
+ x = y;
+ y = scan_height - t - 1;
+ break;
+ }
+
+ var line_offset = rowstride * y;
+
+ /* Optimise for 8 bit images */
+ if (depth == 8 && n_channels == 3)
+ {
+ var o = line_offset + x * n_channels;
+ pixel[offset+0] = pixels[o];
+ pixel[offset+1] = pixels[o+1];
+ pixel[offset+2] = pixels[o+2];
+ return;
+ }
+ else if (depth == 8 && n_channels == 1)
+ {
+ var p = pixels[line_offset + x];
+ pixel[offset+0] = pixel[offset+1] = pixel[offset+2] = p;
+ return;
+ }
+
+ /* Optimise for bitmaps */
+ else if (depth == 1 && n_channels == 1)
+ {
+ var p = pixels[line_offset + (x / 8)];
+ pixel[offset+0] = pixel[offset+1] = pixel[offset+2] = (p & (0x80 >> (x % 8))) != 0 ? 0x00 : 0xFF;
+ return;
+ }
+
+ /* Optimise for 2 bit images */
+ else if (depth == 2 && n_channels == 1)
+ {
+ int block_shift[4] = { 6, 4, 2, 0 };
+
+ var p = pixels[line_offset + (x / 4)];
+ var sample = (p >> block_shift[x % 4]) & 0x3;
+ sample = sample * 255 / 3;
+
+ pixel[offset+0] = pixel[offset+1] = pixel[offset+2] = (uchar) sample;
+ return;
+ }
+
+ /* Use slow method */
+ pixel[offset+0] = get_sample (pixels, line_offset, x, depth, n_channels, 0);
+ pixel[offset+1] = get_sample (pixels, line_offset, x, depth, n_channels, 1);
+ pixel[offset+2] = get_sample (pixels, line_offset, x, depth, n_channels, 2);
+ }
+
+ public Gdk.Pixbuf get_image (bool apply_crop)
+ {
+ int l, r, t, b;
+ if (apply_crop && has_crop)
+ {
+ l = crop_x;
+ r = l + crop_width;
+ t = crop_y;
+ b = t + crop_height;
+
+ if (l < 0)
+ l = 0;
+ if (r > width)
+ r = width;
+ if (t < 0)
+ t = 0;
+ if (b > height)
+ b = height;
+ }
+ else
+ {
+ l = 0;
+ r = width;
+ t = 0;
+ b = height;
+ }
+
+ var image = new Gdk.Pixbuf (Gdk.Colorspace.RGB, false, 8, r - l, b - t);
+ unowned uint8[] image_pixels = image.get_pixels ();
+ for (var y = t; y < b; y++)
+ {
+ var offset = image.get_rowstride () * (y - t);
+ for (var x = l; x < r; x++)
+ get_pixel (x, y, image_pixels, offset + (x - l) * 3);
+ }
+
+ return image;
+ }
+
+ private string? get_icc_data_encoded (string icc_profile_filename)
+ {
+ /* Get binary data */
+ string contents;
+ try
+ {
+ FileUtils.get_contents (icc_profile_filename, out contents);
+ }
+ catch (Error e)
+ {
+ warning ("failed to get icc profile data: %s", e.message);
+ return null;
+ }
+
+ /* Encode into base64 */
+ return Base64.encode ((uchar[]) contents.to_utf8 ());
+ }
+
+ public void copy_to_clipboard (Gtk.Window window)
+ {
+ var display = window.get_display ();
+ var clipboard = Gtk.Clipboard.get_for_display (display, Gdk.SELECTION_CLIPBOARD);
+ var image = get_image (true);
+ clipboard.set_image (image);
+ }
+
+ public void save (string type, int quality, File file) throws Error
+ {
+ var stream = file.replace (null, false, FileCreateFlags.NONE, null);
+ var writer = new PixbufWriter (stream);
+ var image = get_image (true);
+
+ string? icc_profile_data = null;
+ if (color_profile != null)
+ icc_profile_data = get_icc_data_encoded (color_profile);
+
+ if (strcmp (type, "jpeg") == 0)
+ {
+ string[] keys = { "quality", "density-unit", "x-density", "y-density", "icc-profile", null };
+ string[] values = { "%d".printf (quality), "dots-per-inch", "%d".printf (dpi), "%d".printf (dpi), icc_profile_data, null };
+ if (icc_profile_data == null)
+ keys[4] = null;
+ writer.save (image, "jpeg", keys, values);
+ }
+ else if (strcmp (type, "png") == 0)
+ {
+ string[] keys = { "icc-profile", null };
+ string[] values = { icc_profile_data, null };
+ if (icc_profile_data == null)
+ keys[0] = null;
+ writer.save (image, "png", keys, values);
+ }
+ else if (strcmp (type, "tiff") == 0)
+ {
+ string[] keys = { "compression", "icc-profile", null };
+ string[] values = { "8" /* Deflate compression */, icc_profile_data, null };
+ if (icc_profile_data == null)
+ keys[1] = null;
+ writer.save (image, "tiff", keys, values);
+ }
+ else
+ ; // FIXME: Throw Error
+ }
+}
+
+public class PixbufWriter
+{
+ public FileOutputStream stream;
+
+ public PixbufWriter (FileOutputStream stream)
+ {
+ this.stream = stream;
+ }
+
+ public void save (Gdk.Pixbuf image, string type, string[] option_keys, string[] option_values) throws Error
+ {
+ image.save_to_callbackv (write_pixbuf_data, type, option_keys, option_values);
+ }
+
+ private bool write_pixbuf_data (uint8[] buf) throws Error
+ {
+ stream.write_all (buf, null, null);
+ return true;
+ }
+}