diff options
Diffstat (limited to 'src/page.vala')
-rw-r--r-- | src/page.vala | 711 |
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; + } +} |