/* * Copyright (C) 2009-2015 Canonical Ltd. * Author: Robert Ancell * * 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 : Object { /* 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_ = ScanDirection.TOP_TO_BOTTOM; 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 (); } } /* 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_x + crop_width > scan_width) ? scan_width : crop_width; this.crop_height = (crop_y + crop_height > scan_height) ? scan_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 "A3": w = 11.692; h = 16.535; break; case "A4": w = 8.267; h = 11.692; break; case "A5": w = 5.846; h = 8.267; break; case "A6": w = 4.1335; h = 5.846; 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; } public string? get_icc_data_encoded () { if (color_profile == null) return null; /* Get binary data */ string contents; try { FileUtils.get_contents (color_profile, 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_png (File file) throws Error { var stream = file.replace (null, false, FileCreateFlags.NONE, null); var image = get_image (true); string? icc_profile_data = null; if (color_profile != null) icc_profile_data = get_icc_data_encoded (); string[] keys = { "x-dpi", "y-dpi", "icc-profile", null }; string[] values = { "%d".printf (dpi), "%d".printf (dpi), icc_profile_data, null }; if (icc_profile_data == null) keys[2] = null; image.save_to_callbackv ((buf) => { stream.write_all (buf, null, null); return true; }, "png", keys, values); } }