diff --git a/data/icons/hicolor/scalable/org.gnome.SimpleScan.svg b/data/icons/hicolor/scalable/org.gnome.SimpleScan.svg
index 4d1836f69a0f73fdb6db1b05f6351250d5306230..bf93badb71c1ad72c9bb62614d0f70ca183e6110 100644
--- a/data/icons/hicolor/scalable/org.gnome.SimpleScan.svg
+++ b/data/icons/hicolor/scalable/org.gnome.SimpleScan.svg
@@ -1,313 +1,21 @@
-
-
diff --git a/data/icons/scalable/actions/eraser-symbolic.svg b/data/icons/scalable/actions/eraser-symbolic.svg
new file mode 100644
index 0000000000000000000000000000000000000000..eb93d695a86314e861d13961016f1bbbc28b8e8c
--- /dev/null
+++ b/data/icons/scalable/actions/eraser-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/data/icons/scalable/actions/magic-wand-symbolic.svg b/data/icons/scalable/actions/magic-wand-symbolic.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e3c05d0aed3624a2e9925b53f5ddb3d2b45412f2
--- /dev/null
+++ b/data/icons/scalable/actions/magic-wand-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/data/icons/scalable/actions/pointer-primary-click-symbolic.svg b/data/icons/scalable/actions/pointer-primary-click-symbolic.svg
new file mode 100644
index 0000000000000000000000000000000000000000..69f0c8926b8955cbdc3295864d4211edb8ba4126
--- /dev/null
+++ b/data/icons/scalable/actions/pointer-primary-click-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/data/icons/scalable/actions/trash-arrow-back-symbolic.svg b/data/icons/scalable/actions/trash-arrow-back-symbolic.svg
new file mode 100644
index 0000000000000000000000000000000000000000..6a87ac43aac08a71a17afe4e60c93eb759625121
--- /dev/null
+++ b/data/icons/scalable/actions/trash-arrow-back-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/data/meson.build b/data/meson.build
index c245e8f3f7f3b42a3e0adee00366322f2a6356fb..8a93260277c82bc3e743b5d4f12f86c0bb309b30 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -38,6 +38,10 @@ resource_files = files([
'icons/scalable/actions/scan-type-adf-symbolic.svg',
'icons/scalable/actions/scan-type-batch-symbolic.svg',
'icons/scalable/actions/crop-symbolic.svg',
+ 'icons/scalable/actions/trash-arrow-back-symbolic.svg',
+ 'icons/scalable/actions/magic-wand-symbolic.svg',
+ 'icons/scalable/actions/eraser-symbolic.svg',
+ 'icons/scalable/actions/pointer-primary-click-symbolic.svg',
'icons/scalable/actions/right-large-symbolic.svg',
'icons/scalable/actions/interleave-before.svg',
'icons/scalable/actions/interleave-after.svg',
diff --git a/data/org.gnome.SimpleScan.gschema.xml b/data/org.gnome.SimpleScan.gschema.xml
index 03f19227994a804e3163f4c490a045a7fba9ea8e..91e40b6f2b4fd349840f5f02c3d3e03065c218db 100644
--- a/data/org.gnome.SimpleScan.gschema.xml
+++ b/data/org.gnome.SimpleScan.gschema.xml
@@ -1,101 +1,170 @@
-
-
-
-
-
-
-
-
-
- ''
- Device to scan from
- SANE device to acquire images from.
-
-
- 'photo'
-
-
-
-
- Type of document being scanned
- Type of document being scanned. This setting decides on the scan resolution, colors and post-processing.
-
-
- 0
- Width of paper in tenths of a mm
- The width of the paper in tenths of a mm (or 0 for automatic paper detection).
-
-
- 0
- Height of paper in tenths of a mm
- The height of the paper in tenths of a mm (or 0 for automatic paper detection).
-
-
- 0
- Brightness of scan
- The brightness adjustment from -100 to 100 (0 being none).
-
-
- 0
- Contrast of scan
- The contrast adjustment from -100 to 100 (0 being none).
-
-
- 150
- Resolution for text scans
- The resolution in dots-per-inch to use when scanning text.
-
-
- 300
- Resolution for image scans
- The resolution in dots-per-inch to use when scanning images.
-
-
- 'both'
- Page side to scan
- The page side to scan.
-
-
- ''
- Directory to save files to
- The directory to save files to. Defaults to the documents directory if unset.
-
-
- 'application/pdf'
- File format that is used for saving image files
- MIME type that is used for saving image files. Examples of supported MIME types: image/jpeg, image/png, application/pdf
-
-
-
- 75
- Quality value to use for JPEG compression
- Quality value to use for JPEG compression.
-
-
-
- 1000
- Delay in millisecond between pages
- Delay in millisecond between pages.
-
-
- false
- Whether or not postprocessing is enabled
- Whether or not postprocessing is enabled.
-
-
- ''
- The path to the postprocessing script
- The path to the postprocessing script.
-
-
- ''
- Additional arguments for the postprocessing script
- Additional arguments for the postprocessing script.
-
-
- false
- Whether or not to keep the original, unprocessed file
- Whether or not to keep the original, unprocessed file. The "_orig" filename will be added to the filename immediately before the file extension.
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ''
+ Device to scan from
+ SANE device to acquire images from.
+
+
+ 0
+ Width of paper in tenths of a mm
+ The width of the paper in tenths of a mm (or 0 for automatic paper detection).
+
+
+ 0
+ Height of paper in tenths of a mm
+ The height of the paper in tenths of a mm (or 0 for automatic paper detection).
+
+
+ 2100
+ Custom paper width in tenths of a mm
+ The custom paper width in tenths of a mm (or 0 for automatic paper detection).
+
+
+ 2970
+ Custom paper height in tenths of a mm
+ The custom paper height in tenths of a mm (or 0 for automatic paper detection).
+
+
+ "Automatic"
+ Page size
+ The page size.
+
+
+ 0
+ Brightness of scan
+ The brightness adjustment from -100 to 100 (0 being none).
+
+
+ 0
+ Contrast of scan
+ The contrast adjustment from -100 to 100 (0 being none).
+
+
+ 300
+ Resolution for scans
+ The resolution in dots-per-inch to use when scanning.
+
+
+ 'both'
+ Page side to scan
+ The page side to scan.
+
+
+ 'top'
+ Page orientation to scan
+ The page orientation to scan.
+
+
+ ''
+ Directory to save files to
+ The directory to save files to. Defaults to the documents directory if unset.
+
+
+ 'application/pdf'
+ File format that is used for saving image files
+ MIME type that is used for saving image files. Examples of supported MIME types: image/jpeg, image/png, application/pdf
+
+
+ 'Scanned Document'
+ Title metadata for PDF files
+ Title metadata for PDF files.
+
+
+ ''
+ Author metadata for PDF files
+ Author metadata for PDF files.
+
+
+ ''
+ Subject metadata for PDF files
+ Subject metadata for PDF files.
+
+
+ 'Scan'
+ Keywords metadata for PDF files
+ Keywords metadata for PDF files.
+
+
+ true
+ Whether or not to save PDF files as pdf/A conformant.
+ Whether or not to save PDF files as pdf/A conformant.
+
+
+
+ 75
+ Quality value to use for JPEG compression
+ Quality value to use for JPEG compression.
+
+
+
+ 0
+ Delay in millisecond between pages
+ Delay in millisecond between pages.
+
+
+ false
+ Whether or not postprocessing is enabled
+ Whether or not postprocessing is enabled.
+
+
+ ''
+ The path to the postprocessing script
+ The path to the postprocessing script.
+
+
+ ''
+ Additional arguments for the postprocessing script
+ Additional arguments for the postprocessing script.
+
+
+ false
+ Whether or not to keep the original, unprocessed file
+ Whether or not to keep the original, unprocessed file. The "_orig" filename will be added to the filename immediately before the file extension.
+
+
+
+ 0
+ Threshold for detecting empty pages
+ Number of significant brightness changes (change > 95) within any line required in a page before it is considered not empty. Set to 0 to disable empty page detection.
+
+
+ false
+ Enable automatic rotation correction
+ If true, rotate scanned pages automatically so text lines are horizontal.
+
+
+ false
+ Enable automatic color reduction
+ If true, automatically reduce the color depth of scanned pages.
+
+
+ false
+ Enable alternating page orientation
+ If true, automatically flip every second page after scanning.
+
+
+ false
+ Whether the filter undo toast has been shown
+ Whether the toast explaining how to undo filters has been displayed to the user.
+
+
+ false
+ Whether the deskew help toast has been shown
+ Whether the toast explaining how to use the manual deskew tool has been displayed to the user.
+
+
+
diff --git a/data/simple-scan.gresource.xml b/data/simple-scan.gresource.xml
index 2e363bc6f354e8111bd20a44b805c6858ea50cf1..c94a018a89f4e30a232a37f604e5278899e03d74 100644
--- a/data/simple-scan.gresource.xml
+++ b/data/simple-scan.gresource.xml
@@ -12,6 +12,10 @@
icons/scalable/actions/scan-type-adf-symbolic.svg
icons/scalable/actions/scan-type-batch-symbolic.svg
icons/scalable/actions/crop-symbolic.svg
+ icons/scalable/actions/trash-arrow-back-symbolic.svg
+ icons/scalable/actions/magic-wand-symbolic.svg
+ icons/scalable/actions/eraser-symbolic.svg
+ icons/scalable/actions/pointer-primary-click-symbolic.svg
icons/scalable/actions/right-large-symbolic.svg
icons/scalable/actions/interleave-before.svg
diff --git a/data/ui/app-window.ui b/data/ui/app-window.ui
index 9c2c2d9c679b19e70eea48b3ae311d6ee0d39321..63df3dce9d04e46868921f1e4e142d4e9af21460 100644
--- a/data/ui/app-window.ui
+++ b/data/ui/app-window.ui
@@ -1,372 +1,801 @@
-
-
-
-
-
-
-
-
-
-
- 480
- Document Scanner
- org.gnome.SimpleScan
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 480
+ org.gnome.SimpleScan
+
+
+
+ vertical
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+
+
+ startup
+
+
+ org.gnome.SimpleScan-symbolic
+
+
+ vertical
+ 12
+
+
+
+ True
+ center
+ True
+
+
+
+
+
+
+
+ True
+ True
+ center
+ center
+
+
+
+
+
+ False
+ vertical
+ center
+ 12
+
+
+
+ none
+ True
+
+
+
+
+
+
+ horizontal
+ True
+ fill
+ 12
+
+
+
+ Refresh
+ Refresh Device List
+
+ True
+ fill
+
+
+
+
+
+ Scan
+ center
+ Adjust scan settings in the header bar.
+ True
+
+ True
+ fill
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ document
+
+
+ vertical
+
+
+
+
+ 6
+ 6
+
+
+ object-rotate-left-symbolic
+ Rotate Left (anti-clockwise)
+ app.rotate_left
+
+
+
+
+ object-rotate-right-symbolic
+ Rotate Right (clockwise)
+ app.rotate_right
+
+
+
+
+ crop-symbolic
+ Crop Selected Page
+
+ crop-button-menu
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+ True
+
+
+
+
+
+ false
+ 0
+ 0
+ 0
+ 0
+
+
+
+
+ crossfade
+
+
+ scanner-symbolic
+
+
+
+
+ True
+ center
+ center
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 6
+ 6
+
+
+
+
+
+
+
+
+
+
+
+
+ edit-delete-symbolic
+ Delete Selected Pages
+ app.delete_page
+
+
+
+
+
+ Show Deleted Pages
+ True
+ app.toggle_deleted_display
+ view-conceal-symbolic
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/ui/preferences-dialog.ui b/data/ui/preferences-dialog.ui
index e5db2323a819de6cd682a2dd8d37effe648648c3..ef5df8b4f9c5c1f272170fc340696b90a5d8b72f 100644
--- a/data/ui/preferences-dialog.ui
+++ b/data/ui/preferences-dialog.ui
@@ -1,270 +1,279 @@
-
-
-
-
-
- -100
- 100
- 1
- 10
-
-
- -100
- 100
- 1
- 10
-
-
- 75
- 0
- 100
- 1
- 10
- 0
-
-
-
-
-
-
- Scanning
-
-
- Scan _Sides
- True
- scan_side_toggles
-
-
- center
-
-
- True
- _Front
- front
-
-
-
-
- True
- B_ack
- back
-
-
-
-
- True
- B_oth
- both
-
-
-
-
-
-
-
-
- _Page Size
- True
-
-
-
-
- _Delay in Seconds
- Interval to scan multiple pages
- True
- page_delay_toggles
-
-
- center
-
-
- 0
- 0
-
-
-
-
- 3
- 3
-
-
-
-
- 6
- 6
-
-
-
-
- 10
- 10
-
-
-
-
- 15
- 15
-
-
-
-
-
-
-
-
-
-
- Quality
-
-
- _Text Resolution
- True
-
-
-
-
- _Image Resolution
- True
-
-
-
-
- _Brightness
- True
- brightness_scale
-
-
- center
- True
- brightness_adjustment
- True
- 0
-
-
-
-
-
-
- _Contrast
- True
- contrast_scale
-
-
- center
- True
- contrast_adjustment
- True
- 0
-
-
-
-
-
-
- Compression
- True
- compression_scale
-
-
- center
- True
- compression_adjustment
- False
-
-
-
-
-
-
-
-
- Postprocessing
-
-
- _Enable Postprocessing
- True
- postproc_enable_box
-
-
- center
- True
-
-
- False
- True
-
-
-
-
-
-
-
-
- _Script
- True
- postproc_script_entry
-
-
- center
- True
-
-
-
- True
-
-
-
-
-
-
-
-
- _Script arguments
- True
- postproc_args_entry
-
-
- center
- True
-
-
-
- True
-
-
-
-
-
-
-
-
- _Keep original file
- True
- postproc_keep_original_box
-
-
- center
- True
-
-
- False
- True
-
-
-
-
-
-
-
-
-
-
-
-
- GTK_SIZE_GROUP_HORIZONTAL
-
-
-
-
-
-
+
+
+
+
+
+ -100
+ 100
+ 1
+ 10
+
+
+ -100
+ 100
+ 1
+ 10
+
+
+ 75
+ 0
+ 100
+ 1
+ 10
+ 0
+
+
+ 0
+ 0
+ 40
+ 1
+ 5
+
+
+
+
+
+
+ Scanning
+
+
+ Page _Orientation
+ Which part of the page is scanned first
+ True
+ page_orientation_toggles
+
+
+ center
+
+
+ True
+ _Top
+ top
+
+
+
+
+ True
+ Botto_m
+ bottom
+
+
+
+
+ True
+ _Left
+ left
+
+
+
+
+ True
+ _Right
+ right
+
+
+
+
+
+
+
+
+ _Alternating Page Orientation
+ Flip every second page
+ True
+
+
+
+
+ _Delay in Seconds
+ Interval to scan multiple pages
+ True
+ page_delay_toggles
+
+
+ center
+
+
+ 0
+ 0
+
+
+
+
+ 3
+ 3
+
+
+
+
+ 6
+ 6
+
+
+
+
+ 10
+ 10
+
+
+
+
+ 15
+ 15
+
+
+
+
+
+
+
+
+ _Empty Page Detection
+ Required brightness jumps in any line
+ True
+ empty_page_threshold_scale
+
+
+ center
+ True
+ empty_page_threshold_adjustment
+ True
+ 0
+
+
+
+
+
+
+ _Automatic Deskew
+ Rotate scanned pages so text lines are horizontal
+ True
+
+
+
+
+ _Automatic Color Reduction
+ Reduce the color depth of scanned pages
+ True
+
+
+
+
+
+
+ Quality
+
+
+ _Brightness
+ True
+ brightness_scale
+
+
+ center
+ True
+ brightness_adjustment
+ True
+ 0
+
+
+
+
+
+
+ _Contrast
+ True
+ contrast_scale
+
+
+ center
+ True
+ contrast_adjustment
+ True
+ 0
+
+
+
+
+
+
+ Compression
+ True
+ compression_scale
+
+
+ center
+ True
+ compression_adjustment
+ False
+
+
+
+
+
+
+
+
+ Postprocessing
+
+
+ _Enable Postprocessing
+ True
+
+
+
+
+ _Script
+ True
+ postproc_script_entry
+
+
+ center
+ True
+
+
+
+ True
+
+
+
+
+
+
+
+
+ _Script arguments
+ True
+ postproc_args_entry
+
+
+ center
+ True
+
+
+
+ True
+
+
+
+
+
+
+
+
+ _Keep original file
+ True
+
+
+
+
+
+
+
+
+ GTK_SIZE_GROUP_HORIZONTAL
+
+
+
+
+
+
diff --git a/data/ui/shortcuts-dialog.ui b/data/ui/shortcuts-dialog.ui
index d27d13e262556b0e309c1d3fa4ffeef6ff29a156..ede8a7848ce389d7797089175d484b0d85ea704f 100644
--- a/data/ui/shortcuts-dialog.ui
+++ b/data/ui/shortcuts-dialog.ui
@@ -1,139 +1,301 @@
-
-
-
-
-
- General
-
-
- app.help
- Show Help
-
-
-
-
- F10
- Open Menu
-
-
-
-
- app.preferences
- Show Preferences
-
-
-
-
- app.shortcuts
- Keyboard Shortcuts
-
-
-
-
- app.quit
- Quit
-
-
-
-
-
-
- Scanning
-
-
- app.scan_single
- Scan a Single Page
-
-
-
-
- app.scan_adf
- Scan all Pages From Document Feeder
-
-
-
-
- app.scan_batch
- Scan Continuously From Flatbed Scanner
-
-
-
-
- app.scan_stop
- Stop Scan in Progress
-
-
-
-
-
-
- Document Modification
-
-
- app.move_left
- Move Page Left
-
-
-
-
- app.move_right
- Move Page Right
-
-
-
-
- app.rotate_left
- Rotate Page to the Left (Anti-Clockwise)
-
-
-
-
- app.rotate_right
- Rotate Page to the Right (Clockwise)
-
-
-
-
- app.delete_page
- Delete Page
-
-
-
-
-
-
- Document Management
-
-
- app.new_document
- Start new Document
-
-
-
-
- app.save
- Save Scanned Document
-
-
-
-
- app.email
- Email Scanned Document
-
-
-
-
- app.print
- Print Scanned Document
-
-
-
-
- app.copy_page
- Copy Current Page to Clipboard
-
-
-
-
-
-
+
+
+
+
+
+ General
+
+
+ app.help
+ Show Help
+
+
+
+
+ F10
+ Open Menu
+
+
+
+
+ app.preferences
+ Show Preferences
+
+
+
+
+ app.shortcuts
+ Keyboard Shortcuts
+
+
+
+
+ app.quit
+ Quit
+
+
+
+
+
+
+ Scanning
+
+
+ app.scan_start
+ Scan in Selected Mode
+
+
+
+
+ app.scan_single
+ Scan a Single Page
+
+
+
+
+ app.scan_adf
+ Scan all Pages From Document Feeder
+
+
+
+
+ app.scan_batch
+ Scan Continuously From Flatbed Scanner
+
+
+
+
+ app.scan_stop
+ Stop Scan in Progress
+
+
+
+
+ <Ctrl><Shift>F
+ Scan Front Side of Page
+
+
+
+
+ <Ctrl><Shift>B
+ Scan Back Side of Page
+
+
+
+
+ <Ctrl><Shift>O
+ Scan Both Sides of Page
+
+
+
+
+
+
+ Document Modification
+
+
+ app.zoom_in
+ Zoom In
+
+
+
+
+ app.zoom_out
+ Zoom Out
+
+
+
+
+ app.toggle_view
+ Toggle View
+
+
+
+
+ app.select_all
+ Select All Pages
+
+
+
+
+ app.select_odd_pages
+ Select Odd Pages
+
+
+
+
+ app.select_even_pages
+ Select Even Pages
+
+
+
+
+ app.edit_page
+ Edit Pages
+
+
+
+
+ app.move_left
+ Move Page Left
+
+
+
+
+ app.move_right
+ Move Page Right
+
+
+
+
+ app.reverse_order
+ Reverse Order of Selected Pages
+
+
+
+
+ app.rotate_left
+ Rotate Page to the Left (Anti-Clockwise)
+
+
+
+
+ app.rotate_right
+ Rotate Page to the Right (Clockwise)
+
+
+
+
+ app.undo
+ Undo
+
+
+
+
+ app.redo
+ Redo
+
+
+
+
+ app.flip_page
+ Flip Page
+
+
+
+
+ app.deskew_text_page
+ Deskew Text Pages
+
+
+
+
+ app.deskew_page
+ Deskew Page (Draw a Line)
+
+
+
+
+ app.convert_to_grayscale
+ Convert to 8-bit Grayscale
+
+
+
+
+ app.convert_to_2bit_grayscale
+ Convert to 2-bit Grayscale
+
+
+
+
+ app.convert_to_line_art
+ Convert to Line Art
+
+
+
+
+ app.automatic_color_filter
+ Automatic Color Reduction
+
+
+
+
+ app.crop_page
+ Crop Page
+
+
+
+
+ app.split_page
+ Split Page
+
+
+
+
+ app.delete_page
+ Delete Page
+
+
+
+
+ app.detect_empty_pages
+ Detect Empty Pages
+
+
+
+
+ app.toggle_deleted_display
+ Toggle Display of Deleted Pages
+
+
+
+
+
+
+ Document Management
+
+
+ app.new_document
+ Start new Document
+
+
+
+
+ app.save
+ Save Scanned Document
+
+
+
+
+ app.email
+ Email Scanned Document
+
+
+
+
+ app.print
+ Print Scanned Document
+
+
+
+
+ app.copy_page
+ Copy Current Page to Clipboard
+
+
+
+
+ app.paste_image
+ Paste Image From Clipboard
+
+
+
+
+ app.paste_crop
+ Paste Crop from Clipboard to Selected Pages
+
+
+
+
+
+
diff --git a/src/app-window.vala b/src/app-window.vala
index 9d048306771fbc178954ec748f7a4befbfd248c7..c8c29dd0972c754fcf18bfcaa22e4faba0396447 100644
--- a/src/app-window.vala
+++ b/src/app-window.vala
@@ -1,1668 +1,4208 @@
-/*
- * Copyright (C) 2009-2015 Canonical Ltd.
- * Author: Robert Ancell ,
- * Eduard Gotwig
- *
- * 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.
- */
-
-private const int DEFAULT_TEXT_DPI = 150;
-private const int DEFAULT_PHOTO_DPI = 300;
-
-[GtkTemplate (ui = "/org/gnome/SimpleScan/ui/app-window.ui")]
-public class AppWindow : Adw.ApplicationWindow
-{
- private const GLib.ActionEntry[] action_entries =
- {
- { "new_document", new_document_cb },
- { "scan_type", scan_type_action_cb, "s", "'single'"},
- { "document_hint", document_hint_action_cb, "s", "'text'"},
- { "scan_single", scan_single_cb },
- { "scan_adf", scan_adf_cb },
- { "scan_batch", scan_batch_cb },
- { "scan_stop", scan_stop_cb },
- { "rotate_left", rotate_left_cb },
- { "rotate_right", rotate_right_cb },
- { "move_left", move_left_cb },
- { "move_right", move_right_cb },
- { "copy_page", copy_page_cb },
- { "delete_page", delete_page_cb },
- { "reorder", reorder_document_cb },
- { "save", save_document_cb },
- { "open_folder", open_folder_cb, "s" },
- { "email", email_document_cb },
- { "print", print_document_cb },
- { "preferences", preferences_cb },
- { "help", help_cb },
- { "about", about_cb },
- { "quit", quit_cb }
- };
-
- private GLib.SimpleAction scan_type_action;
- private GLib.SimpleAction document_hint_action;
-
- private GLib.SimpleAction delete_page_action;
- private GLib.SimpleAction page_move_left_action;
- private GLib.SimpleAction page_move_right_action;
- private GLib.SimpleAction copy_to_clipboard_action;
-
- private CropActions crop_actions;
-
- private Settings settings;
- private ScanType scan_type = ScanType.SINGLE;
-
- private PreferencesDialog preferences_dialog;
-
- private bool setting_devices;
- private bool user_selected_device;
-
- [GtkChild]
- private unowned Gtk.PopoverMenu page_menu;
- [GtkChild]
- private unowned Gtk.Stack stack;
- [GtkChild]
- private unowned Adw.StatusPage status_page;
- [GtkChild]
- private unowned Gtk.Label status_secondary_label;
- private ListStore device_model;
- [GtkChild]
- private unowned Gtk.Box device_buttons_box;
- [GtkChild]
- private unowned Gtk.DropDown device_drop_down;
- [GtkChild]
- private unowned Gtk.Box main_vbox;
- [GtkChild]
- private unowned Gtk.Button save_button;
- [GtkChild]
- private unowned Gtk.Button stop_button;
- [GtkChild]
- private unowned Gtk.Button scan_button;
- [GtkChild]
- private unowned Gtk.ActionBar action_bar;
- [GtkChild]
- private unowned Gtk.ToggleButton crop_button;
-
- [GtkChild]
- private unowned Adw.ButtonContent scan_button_content;
-
- [GtkChild]
- private unowned Gtk.MenuButton menu_button;
- [GtkChild]
- private unowned Adw.ToastOverlay toast_overlay;
-
- private bool have_devices = false;
- private string? missing_driver = null;
-
- public Book book { get; private set; }
- private bool book_needs_saving;
- private string? book_uri = null;
-
- public Page selected_page
- {
- get
- {
- return book_view.selected_page;
- }
- set
- {
- book_view.selected_page = value;
- }
- }
-
- private AutosaveManager autosave_manager;
-
- private BookView book_view;
- private bool updating_page_menu;
-
- private string document_hint = "photo";
-
- private bool scanning_ = false;
- public bool scanning
- {
- get { return scanning_; }
- set
- {
- scanning_ = value;
- stack.set_visible_child_name ("document");
-
- delete_page_action.set_enabled (!value);
- scan_button.visible = !value;
- stop_button.visible = value;
- }
- }
-
- private int window_width;
- private int window_height;
- private bool window_is_maximized;
- private bool window_is_fullscreen;
-
- private uint save_state_timeout;
-
- public int brightness
- {
- get { return preferences_dialog.get_brightness (); }
- set { preferences_dialog.set_brightness (value); }
- }
-
- public int contrast
- {
- get { return preferences_dialog.get_contrast (); }
- set { preferences_dialog.set_contrast (value); }
- }
-
- public int page_delay
- {
- get { return preferences_dialog.get_page_delay (); }
- set { preferences_dialog.set_page_delay (value); }
- }
-
- public signal void start_scan (string? device, ScanOptions options);
- public signal void stop_scan ();
- public signal void redetect ();
-
- static string get_device_label (ScanDevice device) {
- return device.label;
- }
-
- public AppWindow ()
- {
- settings = new Settings ("org.gnome.SimpleScan");
-
- device_model = new ListStore (typeof (ScanDevice));
- device_drop_down.model = device_model;
- device_drop_down.expression = new Gtk.CClosureExpression (
- typeof (string),
- null,
- {},
- (Callback) get_device_label,
- null,
- null
- );
-
- book = new Book ();
- book.page_added.connect (page_added_cb);
- book.reordered.connect (reordered_cb);
- book.page_removed.connect (page_removed_cb);
- book.changed.connect (book_changed_cb);
-
- load ();
-
- clear_document ();
- }
-
- ~AppWindow ()
- {
- book.page_added.disconnect (page_added_cb);
- book.reordered.disconnect (reordered_cb);
- book.page_removed.disconnect (page_removed_cb);
- }
-
- public void show_error_dialog (string error_title, string error_text)
- {
- var dialog = new Adw.AlertDialog (error_title,
- error_text);
- dialog.add_response ("close", _("_Close"));
- dialog.set_response_appearance ("close", Adw.ResponseAppearance.SUGGESTED);
- dialog.present (this);
- }
-
- public async AuthorizeDialogResponse authorize (string resource)
- {
- /* Label in authorization dialog. “%s” is replaced with the name of the resource requesting authorization */
- var description = _("Username and password required to access “%s”").printf (resource);
- var authorize_dialog = new AuthorizeDialog (this, description);
- authorize_dialog.transient_for = this;
-
- return yield authorize_dialog.open ();
- }
-
- private void update_scan_status ()
- {
- scan_button.sensitive = false;
- if (!have_devices)
- {
- status_page.set_title (/* Label shown when searching for scanners */
- _("Searching for Scanners…"));
- status_secondary_label.visible = false;
- device_buttons_box.visible = false;
- }
- else if (get_selected_device () != null)
- {
- scan_button.sensitive = true;
- status_page.set_title (/* Label shown when detected a scanner */
- _("Ready to Scan"));
- status_secondary_label.set_text (get_selected_device_label ());
- status_secondary_label.visible = false;
- device_buttons_box.visible = true;
- device_buttons_box.sensitive = true;
- device_drop_down.sensitive = true;
- }
- else if (this.missing_driver != null)
- {
- status_page.set_title (/* Warning displayed when no drivers are installed but a compatible scanner is detected */
- _("Additional Software Needed"));
- /* Instructions to install driver software */
- status_secondary_label.set_markup (_("You need to install driver software for your scanner"));
- status_secondary_label.visible = true;
- device_buttons_box.visible = false;
- }
- else
- {
- /* Warning displayed when no scanners are detected */
- status_page.set_title (_("No Scanners Detected"));
- /* Hint to user on why there are no scanners detected */
- status_secondary_label.set_text (_("Please check your scanner is connected and powered on"));
- status_secondary_label.visible = true;
- device_buttons_box.visible = true;
- device_buttons_box.sensitive = true;
- device_drop_down.sensitive = false; // We would like to be refresh button to be active
- }
- }
-
- public void set_scan_devices (List devices, string? missing_driver = null)
- {
- have_devices = true;
- this.missing_driver = missing_driver;
-
- // Ignore selected events during this code, to prevent updating "selected-device"
- setting_devices = true;
-
- {
- /*
- Technically this could be optimized, but:
- a) for the typical amount of scanners that would probably be overkill
- b) we rescan only on user action so this is rarely called
- */
- device_model.remove_all ();
-
- /* Add new devices */
- foreach (var device in devices)
- {
- device_model.append (device);
- }
-
- /* Select the previously selected device or the first available device */
- var device_name = settings.get_string ("selected-device");
-
- uint position = 0;
- if (device_name != null && find_device_by_name (device_name, out position) != null)
- device_drop_down.selected = position;
- else
- device_drop_down.selected = 0;
- }
-
- setting_devices = false;
-
- update_scan_status ();
- }
-
- private async bool prompt_to_load_autosaved_book ()
- {
- var dialog = new Adw.AlertDialog ("",
- /* Contents of dialog that shows if autosaved book should be loaded. */
- _("An autosaved book exists. Do you want to open it?"));
-
- dialog.add_response ("no", _("_No"));
- dialog.add_response ("yes", _("_Yes"));
-
- dialog.set_response_appearance ("no", Adw.ResponseAppearance.DESTRUCTIVE);
- dialog.set_response_appearance ("yes", Adw.ResponseAppearance.SUGGESTED);
-
- dialog.set_default_response("yes");
- dialog.present (this);
-
- string response = "yes";
-
- SourceFunc callback = prompt_to_load_autosaved_book.callback;
- dialog.response.connect((res) => {
- response = res;
- callback();
- });
-
- yield;
-
- return response == "yes";
- }
-
- private string? get_selected_device ()
- {
- if (device_drop_down.selected != Gtk.INVALID_LIST_POSITION)
- {
- return ((ScanDevice) device_model.get_item (device_drop_down.selected)).name;
- }
-
- return null;
- }
-
- private string? get_selected_device_label ()
- {
- if (device_drop_down.selected != Gtk.INVALID_LIST_POSITION)
- {
- return ((ScanDevice) device_model.get_item (device_drop_down.selected)).label;
- }
-
- return null;
- }
-
- public void set_selected_device (string device)
- {
- user_selected_device = true;
-
- uint position;
- find_device_by_name (device, out position);
-
- if (position != Gtk.INVALID_LIST_POSITION)
- return;
-
- device_drop_down.selected = position;
- }
-
- private ScanDevice? find_device_by_name(string name, out uint position)
- {
- for (uint i = 0; i < device_model.get_n_items (); i++)
- {
- var item = (ScanDevice?) device_model.get_item (i);
- if (item.name == name) {
- position = i;
- return item;
- }
- }
-
- position = Gtk.INVALID_LIST_POSITION;
- return null;
- }
-
- private async string? choose_file_location ()
- {
- /* Get directory to save to */
- string? directory = null;
- directory = settings.get_string ("save-directory");
-
- if (directory == null || directory == "")
- directory = GLib.Filename.to_uri(Environment.get_user_special_dir (UserDirectory.DOCUMENTS));
-
- var save_dialog = new Gtk.FileDialog ();
- save_dialog.title = _("Save As…");
- save_dialog.modal = true;
- save_dialog.accept_label = _("_Save");
-
- // TODO(gtk4)
- // save_dialog.local_only = false;
-
- var save_format = settings.get_string ("save-format");
- if (book_uri != null)
- {
- save_dialog.initial_file = GLib.File.new_for_uri (book_uri);
- }
- else
- {
- save_dialog.initial_folder = GLib.File.new_for_uri (directory);
-
- /* Default filename to use when saving document. */
- /* To that filename the extension will be added, eg. "Scanned Document.pdf" */
- save_dialog.initial_name = (_("Scanned Document") + "." + mime_type_to_extension (save_format));
- }
-
- var filters = new ListStore (typeof (Gtk.FileFilter));
-
- var pdf_filter = new Gtk.FileFilter ();
- pdf_filter.set_filter_name (_("PDF (multi-page document)"));
- pdf_filter.add_pattern ("*.pdf" );
- pdf_filter.add_mime_type ("application/pdf");
- filters.append (pdf_filter);
-
- var jpeg_filter = new Gtk.FileFilter ();
- jpeg_filter.set_filter_name (_("JPEG (compressed)"));
- jpeg_filter.add_pattern ("*.jpg" );
- jpeg_filter.add_pattern ("*.jpeg" );
- jpeg_filter.add_mime_type ("image/jpeg");
- filters.append (jpeg_filter);
-
- var png_filter = new Gtk.FileFilter ();
- png_filter.set_filter_name (_("PNG (lossless)"));
- png_filter.add_pattern ("*.png" );
- png_filter.add_mime_type ("image/png");
- filters.append (png_filter);
-
- var webp_filter = new Gtk.FileFilter ();
- webp_filter.set_filter_name (_("WebP (compressed)"));
- webp_filter.add_pattern ("*.webp" );
- webp_filter.add_mime_type ("image/webp");
- filters.append (webp_filter);
-
- var all_filter = new Gtk.FileFilter ();
- all_filter.set_filter_name (_("All Files"));
- all_filter.add_pattern ("*");
- filters.append (all_filter);
-
- save_dialog.filters = filters;
-
- switch (save_format)
- {
- case "application/pdf":
- save_dialog.default_filter = pdf_filter;
- break;
- case "image/jpeg":
- save_dialog.default_filter = jpeg_filter;
- break;
- case "image/png":
- save_dialog.default_filter = png_filter;
- break;
- case "image/webp":
- save_dialog.default_filter = webp_filter;
- break;
- }
-
- var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
- box.visible = true;
- box.spacing = 10;
- box.set_halign (Gtk.Align.CENTER);
-
- while (true)
- {
- File? file = null;
- try {
- file = yield save_dialog.save (this, null);
- }
- catch (Error e)
- {
- warning ("Failed to open save dialog: %s", e.message);
- }
-
- if (file == null)
- {
- return null;
- }
-
- var uri = file.get_uri ();
-
- var extension = uri_extension(uri);
-
- var mime_type = extension_to_mime_type(extension);
- mime_type = mime_type != null ? mime_type : "application/pdf";
-
- settings.set_string ("save-format", mime_type);
-
- if (extension == null)
- uri += "." + mime_type_to_extension (mime_type);
-
- /* Check the file(s) don't already exist */
- var files = new List ();
- if (mime_type == "image/jpeg" || mime_type == "image/png" || mime_type == "image/webp")
- {
- for (var j = 0; j < book.n_pages; j++)
- files.append (make_indexed_file (uri, j, book.n_pages));
- }
- else
- files.append (File.new_for_uri (uri));
-
- var overwrite_check = true;
-
- // We assume that GTK or system file dialog asked about overwrite already so we reask only if there is more than one file or we changed the name
- // Ideally in flatpack era we should not modify file name after save dialog is done
- // but for the sake of keeping old functionality in tact we leave it as it
- if (files.length () > 1 || file.get_uri () != uri)
- {
- overwrite_check = yield check_overwrite (this, files);
- }
-
- if (overwrite_check)
- {
- var directory_uri = uri.substring (0, uri.last_index_of ("/") + 1);
- settings.set_string ("save-directory", directory_uri);
- return uri;
- }
- }
- }
-
- private async bool check_overwrite (Gtk.Window parent, List files)
- {
- foreach (var file in files)
- {
- if (!file.query_exists ())
- continue;
-
- var title = _("A file named “%s” already exists. Do you want to replace it?").printf(file.get_basename ());
-
- var dialog = new Adw.AlertDialog (/* Contents of dialog that shows if saving would overwrite and existing file. %s is replaced with the name of the file. */
- title,
- null);
-
- dialog.add_response ("cancel", _("_Cancel"));
- dialog.add_response ("replace", _("_Replace"));
-
- dialog.set_response_appearance ("replace", Adw.ResponseAppearance.DESTRUCTIVE);
-
- SourceFunc callback = check_overwrite.callback;
- string response = "cancel";
-
- dialog.response.connect ((res) => {
- response = res;
- callback ();
- });
-
- dialog.present (parent);
-
- yield;
-
- if (response != "replace")
- return false;
- }
-
- return true;
- }
-
- private string? mime_type_to_extension (string mime_type)
- {
- if (mime_type == "application/pdf")
- return "pdf";
- else if (mime_type == "image/jpeg")
- return "jpg";
- else if (mime_type == "image/png")
- return "png";
- else if (mime_type == "image/webp")
- return "webp";
- else
- return null;
- }
-
- private string? extension_to_mime_type (string extension)
- {
- var extension_lower = extension.down ();
- if (extension_lower == "pdf")
- return "application/pdf";
- else if (extension_lower == "jpg" || extension_lower == "jpeg")
- return "image/jpeg";
- else if (extension_lower == "png")
- return "image/png";
- else if (extension_lower == "webp")
- return "image/webp";
- else
- return null;
- }
-
- private string? uri_extension (string uri)
- {
- var extension_index = uri.last_index_of_char ('.');
- if (extension_index < 0)
- return null;
-
- return uri.substring (extension_index + 1);
- }
-
- private string uri_to_mime_type (string uri)
- {
- var extension = uri_extension(uri);
- if (extension == null)
- return "image/jpeg";
-
- var mime_type = extension_to_mime_type (extension);
- if (mime_type == null)
- return "image/jpeg";
-
- return mime_type;
- }
-
- private async bool save_document_async ()
- {
- var uri = yield choose_file_location ();
- if (uri == null)
- return false;
-
- var file = File.new_for_uri (uri);
-
- debug ("Saving to '%s'", uri);
-
- var mime_type = uri_to_mime_type (uri);
-
- var cancellable = new Cancellable ();
- var progress_bar = new CancellableProgressBar (_("Saving"), cancellable);
- action_bar.pack_end (progress_bar);
- progress_bar.visible = true;
- save_button.sensitive = false;
- try
- {
- yield book.save_async (mime_type, settings.get_int ("jpeg-quality"), file,
- (fraction) =>
- {
- progress_bar.set_fraction (fraction);
- }, cancellable);
- }
- catch (Error e)
- {
- save_button.sensitive = true;
- progress_bar.destroy ();
- warning ("Error saving file: %s", e.message);
- show_error_dialog (/* Title of error dialog when save failed */
- _("Failed to save file"),
- e.message);
- return false;
- }
- save_button.sensitive = true;
- progress_bar.remove_with_delay (500, action_bar);
-
- try
- {
- yield book.postprocess_async (
- mime_type, file, settings.get_boolean ("postproc-enabled"), settings.get_string ("postproc-script"),
- settings.get_string ("postproc-arguments"), settings.get_boolean ("postproc-keep-original"));
- }
- catch (Error e)
- {
- warning ("Error running postprocessing: %s", e.message);
- show_error_dialog (/* Title of error dialog when postprocessing failed */
- _("Failed to run postprocessing"),
- e.message);
- }
-
- book_needs_saving = false;
- book_uri = uri;
- var saved_toast = new Adw.Toast (_("Document Saved")) {
- button_label = _("Open Folder"),
- action_name = "app.open_folder",
- action_target = new Variant.string (file.get_uri ())
- };
-
- toast_overlay.add_toast (saved_toast);
-
- return true;
- }
-
- private async bool prompt_to_save_async (string title, string discard_label)
- {
- if (!book_needs_saving || (book.n_pages == 0))
- return true;
-
- var dialog = new Adw.AlertDialog (title,
- _("If you don’t save, changes will be permanently lost."));
-
- dialog.add_response ("discard", discard_label);
- dialog.add_response ("cancel", _("_Cancel"));
- dialog.add_response ("save", _("_Save"));
-
- dialog.set_response_appearance ("discard", Adw.ResponseAppearance.DESTRUCTIVE);
- dialog.set_response_appearance ("save", Adw.ResponseAppearance.SUGGESTED);
-
- dialog.present (this);
-
- string response = "cancel";
- SourceFunc callback = prompt_to_save_async.callback;
- dialog.response.connect((res) => {
- response = res;
- callback ();
- });
-
- yield;
-
- switch (response)
- {
- case "save":
- if (yield save_document_async ())
- return true;
- else
- return false;
- case "discard":
- return true;
- default:
- return false;
- }
- }
-
- public void clear_document ()
- {
- book.clear ();
- book_needs_saving = false;
- book_uri = null;
- save_button.sensitive = false;
- copy_to_clipboard_action.set_enabled (false);
- update_scan_status ();
- stack.set_visible_child_name ("startup");
- }
-
- private void new_document ()
- {
- prompt_to_save_async.begin (/* Text in dialog warning when a document is about to be lost */
- _("Save current document?"),
- /* Button in dialog to create new document and discard unsaved document */
- _("_Discard Changes"), (obj, res) =>
- {
- if (!prompt_to_save_async.end(res))
- return;
-
- if (scanning)
- stop_scan ();
-
- clear_document ();
- autosave_manager.cleanup ();
- });
- }
-
- [GtkCallback]
- private bool status_label_activate_link_cb (Gtk.Label label, string uri)
- {
- if (uri == "install-firmware")
- {
- var dialog = new DriversDialog (this, missing_driver);
- dialog.open.begin (() => {});
- return true;
- }
-
- return false;
- }
-
- [GtkCallback]
- private void new_document_cb ()
- {
- new_document ();
- }
-
- [GtkCallback]
- private void crop_toggle_cb (Gtk.ToggleButton btn)
- {
- if (updating_page_menu)
- return;
-
- var page = book_view.selected_page;
- if (page == null)
- {
- warning ("Trying to set crop but no selected page");
- return;
- }
-
- if (btn.active)
- {
- // Avoid overwriting crop name if there is already different crop active
- if (!page.has_crop)
- set_crop ("custom");
- }
- else
- {
- set_crop (null);
- }
- }
-
- [GtkCallback]
- private void redetect_button_clicked_cb (Gtk.Button button)
- {
- have_devices = false;
- update_scan_status ();
- redetect ();
- }
-
- private void scan (ScanOptions options)
- {
- status_page.set_title (/* Label shown when scan started */
- _("Contacting Scanner…"));
- device_buttons_box.visible = true;
- device_buttons_box.sensitive = false;
- start_scan (get_selected_device (), options);
- }
-
- private void scan_type_action_cb (SimpleAction action, Variant? value)
- {
- var type = value.get_string ();
-
- switch (type) {
- case "single":
- set_scan_type (ScanType.SINGLE);
- break;
- case "adf":
- set_scan_type (ScanType.ADF);
- break;
- case "batch":
- set_scan_type (ScanType.BATCH);
- break;
- default:
- return;
- }
- }
-
- private void document_hint_action_cb (SimpleAction action, Variant? value)
- {
- var hint = value.get_string ();
- set_document_hint(hint, true);
- }
-
- private void scan_single_cb ()
- {
- var options = make_scan_options ();
- options.type = ScanType.SINGLE;
- scan (options);
- }
-
- private void scan_adf_cb ()
- {
- var options = make_scan_options ();
- options.type = ScanType.ADF;
- scan (options);
- }
-
- private void scan_batch_cb ()
- {
- var options = make_scan_options ();
- options.type = ScanType.BATCH;
- scan (options);
- }
-
- private void scan_stop_cb ()
- {
- stop_scan ();
- }
-
- private void rotate_left_cb ()
- {
- if (updating_page_menu)
- return;
- var page = book_view.selected_page;
- if (page != null)
- page.rotate_left ();
- }
-
- private void rotate_right_cb ()
- {
- if (updating_page_menu)
- return;
- var page = book_view.selected_page;
- if (page != null)
- page.rotate_right ();
- }
-
- private void move_left_cb ()
- {
- var page = book_view.selected_page;
- var index = book.get_page_index (page);
- if (index > 0)
- book.move_page (page, index - 1);
- }
-
- private void move_right_cb ()
- {
- var page = book_view.selected_page;
- var index = book.get_page_index (page);
- if (index < book.n_pages - 1)
- book.move_page (page, book.get_page_index (page) + 1);
- }
-
- private void copy_page_cb ()
- {
- var page = book_view.selected_page;
- if (page != null)
- page.copy_to_clipboard (this);
- }
-
- private void delete_page_cb ()
- {
- book_view.book.delete_page (book_view.selected_page);
- }
-
- private void set_scan_type (ScanType scan_type)
- {
- this.scan_type = scan_type;
-
- switch (scan_type)
- {
- case ScanType.SINGLE:
- scan_type_action.set_state ("single");
- scan_button_content.icon_name = "scanner-symbolic";
- scan_button.tooltip_text = _("Scan a Single Page");
- break;
- case ScanType.ADF:
- scan_type_action.set_state ("adf");
- scan_button_content.icon_name = "scan-type-adf-symbolic";
- scan_button.tooltip_text = _("Scan Multiple Pages");
- break;
- case ScanType.BATCH:
- scan_type_action.set_state ("batch");
- scan_button_content.icon_name = "scan-type-batch-symbolic";
- scan_button.tooltip_text = _("Scan Multiple Pages");
- break;
- }
- }
-
- private void set_document_hint (string document_hint, bool save = false)
- {
- this.document_hint = document_hint;
-
- document_hint_action.set_state (document_hint);
-
- if (save)
- settings.set_string ("document-type", document_hint);
- }
-
- private ScanOptions make_scan_options ()
- {
- var options = new ScanOptions ();
- if (document_hint == "text")
- {
- options.scan_mode = ScanMode.GRAY;
- options.dpi = preferences_dialog.get_text_dpi ();
- options.depth = 2;
- }
- else
- {
- options.scan_mode = ScanMode.COLOR;
- options.dpi = preferences_dialog.get_photo_dpi ();
- options.depth = 8;
- }
- preferences_dialog.get_paper_size (out options.paper_width, out options.paper_height);
- options.brightness = brightness;
- options.contrast = contrast;
- options.page_delay = page_delay;
- options.side = preferences_dialog.get_page_side ();
-
- return options;
- }
-
- [GtkCallback]
- private void device_drop_down_changed_cb (Object widget, ParamSpec spec)
- {
- if (setting_devices)
- return;
- user_selected_device = true;
- if (get_selected_device () != null)
- settings.set_string ("selected-device", get_selected_device ());
- }
-
- [GtkCallback]
- private void scan_button_clicked_cb (Gtk.Widget widget)
- {
- scan_button.visible = false;
- stop_button.visible = true;
- var options = make_scan_options ();
- options.type = scan_type;
- scan (options);
- }
-
- [GtkCallback]
- private void stop_scan_button_clicked_cb (Gtk.Widget widget)
- {
- scan_button.visible = true;
- stop_button.visible = false;
- stop_scan ();
- }
-
- private void preferences_cb ()
- {
- preferences_dialog.present (this);
- }
-
- private void update_page_menu ()
- {
- var page = book_view.selected_page;
- if (page == null)
- {
- page_move_left_action.set_enabled (false);
- page_move_right_action.set_enabled (false);
- }
- else
- {
- var index = book.get_page_index (page);
- page_move_left_action.set_enabled (index > 0);
- page_move_right_action.set_enabled (index < book.n_pages - 1);
- }
- }
-
- private void page_selected_cb (BookView view, Page? page)
- {
- if (page == null)
- return;
-
- updating_page_menu = true;
-
- update_page_menu ();
-
- crop_actions.update_current_crop (page.crop_name);
- crop_button.active = page.has_crop;
-
- updating_page_menu = false;
- }
-
- private void show_page_cb (BookView view, Page page)
- {
- File file;
- try
- {
- var dir = DirUtils.make_tmp ("simple-scan-XXXXXX");
- file = File.new_for_path (Path.build_filename (dir, "scan.png"));
- page.save_png (file);
- }
- catch (Error e)
- {
- show_error_dialog (/* Error message display when unable to save image for preview */
- _("Unable to save image for preview"),
- e.message);
- return;
- }
-
- var launcher = new Gtk.FileLauncher(file);
- launcher.launch.begin (this, null);
- }
-
- private void show_page_menu_cb (BookView view, Gtk.Widget from, double x, double y)
- {
- double tx, ty;
- from.translate_coordinates(this, x, y, out tx, out ty);
-
- Gdk.Rectangle rect = { x: (int) tx, y: (int) ty, w: 1, h: 1 };
-
- page_menu.set_pointing_to (rect);
- page_menu.popup ();
- }
-
- private void set_crop (string? crop_name)
- {
- if (updating_page_menu)
- return;
-
- if (crop_name == "none")
- crop_name = null;
-
- var page = book_view.selected_page;
- if (page == null)
- {
- warning ("Trying to set crop but no selected page");
- return;
- }
-
- if (crop_name == null)
- page.set_no_crop ();
- else if (crop_name == "custom")
- {
- var width = page.width;
- var height = page.height;
- var crop_width = (int) (width * 0.8 + 0.5);
- var crop_height = (int) (height * 0.8 + 0.5);
- page.set_custom_crop (crop_width, crop_height);
- page.move_crop ((width - crop_width) / 2, (height - crop_height) / 2);
- }
- else
- page.set_named_crop (crop_name);
-
- crop_actions.update_current_crop (crop_name);
- crop_button.active = page.has_crop;
- }
-
- public void crop_set_action_cb (SimpleAction action, Variant? value)
- {
- set_crop (value.get_string ());
- }
-
- public void crop_rotate_action_cb ()
- {
- var page = book_view.selected_page;
- if (page == null)
- return;
- page.rotate_crop ();
- }
-
- private void reorder_document_cb ()
- {
- var dialog = new ReorderPagesDialog ();
- dialog.set_transient_for (this);
-
- /* Button for combining sides in reordering dialog */
- dialog.combine_sides.clicked.connect (() =>
- {
- book.combine_sides ();
- dialog.close ();
- });
-
- /* Button for combining sides in reverse order in reordering dialog */
- dialog.combine_sides_rev.clicked.connect (() =>
- {
- book.combine_sides_reverse ();
- dialog.close ();
- });
-
- /* Button for reversing in reordering dialog */
- dialog.reverse.clicked.connect (() =>
- {
- book.reverse ();
- dialog.close ();
- });
-
- /* Button for keeping the ordering, but flip every second upside down */
- dialog.flip_odd.clicked.connect (() =>
- {
- book.flip_every_second(FlipEverySecond.Odd);
- dialog.close ();
- });
-
- /* Button for keeping the ordering, but flip every second upside down */
- dialog.flip_even.clicked.connect (() =>
- {
- dialog.close ();
- book.flip_every_second(FlipEverySecond.Even);
- });
-
- dialog.present ();
- }
-
- public void save_document_cb ()
- {
- save_document_async.begin ();
- }
-
- private void draw_page (Gtk.PrintOperation operation,
- Gtk.PrintContext print_context,
- int page_number)
- {
- var context = print_context.get_cairo_context ();
- var page = book.get_page (page_number);
-
- /* Rotate to same aspect */
- bool is_landscape = false;
- if (print_context.get_width () > print_context.get_height ())
- is_landscape = true;
- if (page.is_landscape != is_landscape)
- {
- context.translate (print_context.get_width (), 0);
- context.rotate (Math.PI_2);
- }
-
- context.scale (print_context.get_dpi_x () / page.dpi,
- print_context.get_dpi_y () / page.dpi);
-
- var image = page.get_image (true);
- Gdk.cairo_set_source_pixbuf (context, image, 0, 0);
- context.paint ();
- }
-
- private void email_document_cb ()
- {
- email_document_async.begin ();
- }
-
- private async void email_document_async ()
- {
- try
- {
- var dir = DirUtils.make_tmp ("simple-scan-XXXXXX");
- string mime_type, filename;
- if (document_hint == "text")
- {
- mime_type = "application/pdf";
- filename = "scan.pdf";
- }
- else
- {
- mime_type = "image/jpeg";
- filename = "scan.jpg";
- }
- var file = File.new_for_path (Path.build_filename (dir, filename));
- yield book.save_async (mime_type, settings.get_int ("jpeg-quality"), file,
- null, null);
- yield book.postprocess_async (mime_type, file, settings.get_boolean ("postproc-enabled"),
- settings.get_string ("postproc-script"), settings.get_string ("postproc-arguments"),
- settings.get_boolean ("postproc-keep-original"));
- var command_line = "xdg-email";
- if (mime_type == "application/pdf")
- command_line += " --attach %s".printf (file.get_path ());
- else
- {
- for (var i = 0; i < book.n_pages; i++) {
- var indexed_file = make_indexed_file (file.get_uri (), i, book.n_pages);
- command_line += " --attach %s".printf (indexed_file.get_path ());
- }
- }
- Process.spawn_command_line_async (command_line);
- }
- catch (Error e)
- {
- warning ("Unable to email document: %s", e.message);
- }
- }
- private void open_folder_cb (SimpleAction action, Variant? parameter)
- requires (parameter != null)
- {
- string uri = parameter.get_string ();
- var file = File.new_for_uri (uri);
-
- var launcher = new Gtk.FileLauncher (file);
- launcher.open_containing_folder.begin (this, null, (obj, res) => {
- try {
- launcher.open_containing_folder.end (res);
- } catch (Error e) {
- warning ("Failed to open containing folder: %s", e.message);
- }
- });
- }
-
-
- private void print_document ()
- {
- var print = new Gtk.PrintOperation ();
- print.n_pages = (int) book.n_pages;
- print.draw_page.connect (draw_page);
-
- try
- {
- print.run (Gtk.PrintOperationAction.PRINT_DIALOG, this);
- }
- catch (Error e)
- {
- warning ("Error printing: %s", e.message);
- }
-
- print.draw_page.disconnect (draw_page);
- }
-
- private void print_document_cb ()
- {
- print_document ();
- }
-
- private void launch_help ()
- {
- var launcher = new Gtk.UriLauncher ("help:simple-scan");
- launcher.launch.begin (this, null);
- }
-
- private void help_cb ()
- {
- launch_help ();
- }
-
- private void show_about ()
- {
- string[] authors = { "Robert Ancell " };
-
- var about = new Adw.AboutDialog ()
- {
- developers = authors,
- translator_credits = _("translator-credits"),
- copyright = "Copyright © 2009-2018 Canonical Ltd.",
- license_type = Gtk.License.GPL_3_0,
- application_name = _("Document Scanner"),
- application_icon = "org.gnome.SimpleScan",
- version = VERSION,
- website = "https://gitlab.gnome.org/GNOME/simple-scan",
- issue_url = "https://gitlab.gnome.org/GNOME/simple-scan/-/issues/",
- };
-
- about.present (this);
- }
-
- private void about_cb ()
- {
- show_about ();
- }
-
- private void on_quit ()
- {
- prompt_to_save_async.begin (/* Text in dialog warning when a document is about to be lost */
- _("Save document before quitting?"),
- /* Text in dialog warning when a document is about to be lost */
- _("_Quit without Saving"), (obj, res) =>
- {
- if (!prompt_to_save_async.end(res))
- return;
-
- destroy ();
-
- if (save_state_timeout != 0)
- save_state (true);
-
- autosave_manager.cleanup ();
- });
- }
-
- private void quit_cb ()
- {
- on_quit ();
- }
-
- public override void size_allocate (int width, int height, int baseline)
- {
- base.size_allocate (width, height, baseline);
-
- if (!window_is_maximized && !window_is_fullscreen)
- {
- window_width = this.get_width();
- window_height = this.get_height();
- save_state ();
- }
- }
-
- public override void unmap ()
- {
- window_is_maximized = is_maximized ();
- window_is_fullscreen = is_fullscreen ();
- save_state ();
-
- base.unmap ();
- }
-
-
- [GtkCallback]
- private bool window_close_request_cb (Gtk.Window window)
- {
- on_quit ();
- return true; /* Let us quit on our own terms */
- }
-
- private void page_added_cb (Book book, Page page)
- {
- update_page_menu ();
- }
-
- private void reordered_cb (Book book)
- {
- update_page_menu ();
- }
-
- private void page_removed_cb (Book book, Page page)
- {
- update_page_menu ();
- }
-
- private void book_changed_cb (Book book)
- {
- save_button.sensitive = true;
- book_needs_saving = true;
- copy_to_clipboard_action.set_enabled (true);
- }
-
- private void load ()
- {
- preferences_dialog = new PreferencesDialog (settings);
-
- Gtk.Window.set_default_icon_name ("org.gnome.SimpleScan");
-
- var app = Application.get_default () as Gtk.Application;
-
- crop_actions = new CropActions (this);
-
- app.add_action_entries (action_entries, this);
-
- scan_type_action = (GLib.SimpleAction) app.lookup_action("scan_type");
- document_hint_action = (GLib.SimpleAction) app.lookup_action("document_hint");
-
- delete_page_action = (GLib.SimpleAction) app.lookup_action("delete_page");
- page_move_left_action = (GLib.SimpleAction) app.lookup_action("move_left");
- page_move_right_action = (GLib.SimpleAction) app.lookup_action("move_right");
- copy_to_clipboard_action = (GLib.SimpleAction) app.lookup_action("copy_page");
-
- app.set_accels_for_action ("app.new_document", { "N" });
- app.set_accels_for_action ("app.scan_single", { "1" });
- app.set_accels_for_action ("app.scan_adf", { "F" });
- app.set_accels_for_action ("app.scan_batch", { "M" });
- app.set_accels_for_action ("app.scan_stop", { "Escape" });
- app.set_accels_for_action ("app.rotate_left", { "bracketleft" });
- app.set_accels_for_action ("app.rotate_right", { "bracketright" });
- app.set_accels_for_action ("app.move_left", { "less" });
- app.set_accels_for_action ("app.move_right", { "greater" });
- app.set_accels_for_action ("app.copy_page", { "C" });
- app.set_accels_for_action ("app.delete_page", { "Delete" });
- app.set_accels_for_action ("app.save", { "S" });
- app.set_accels_for_action ("app.email", { "E" });
- app.set_accels_for_action ("app.print", { "P" });
- app.set_accels_for_action ("app.help", { "F1" });
- app.set_accels_for_action ("app.quit", { "Q" });
- app.set_accels_for_action ("app.preferences", { "comma" });
- app.set_accels_for_action ("app.shortcuts", { "question" });
-
- var gear_menu = new Menu ();
- var section = new Menu ();
- gear_menu.append_section (null, section);
- section.append (_("_Email…"), "app.email");
- section.append (_("Pri_nt…"), "app.print");
- section.append (C_("menu", "_Reorder Pages…"), "app.reorder");
- section = new Menu ();
- gear_menu.append_section (null, section);
- section.append (_("_Preferences"), "app.preferences");
- section.append (_("_Keyboard Shortcuts"), "app.shortcuts");
- section.append (_("_Help"), "app.help");
- section.append (_("_About Document Scanner"), "app.about");
- menu_button.set_menu_model (gear_menu);
-
- app.add_window (this);
-
- var document_type = settings.get_string ("document-type");
- if (document_type != null)
- set_document_hint (document_type);
-
- book_view = new BookView (book);
- book_view.vexpand = true;
-
- main_vbox.prepend (book_view);
-
- book_view.page_selected.connect (page_selected_cb);
- book_view.show_page.connect (show_page_cb);
- book_view.show_menu.connect (show_page_menu_cb);
- book_view.visible = true;
-
- /* Load previous state */
- load_state ();
-
- /* Restore window size */
- debug ("Restoring window to %dx%d pixels", window_width, window_height);
- set_default_size (window_width, window_height);
- if (window_is_maximized)
- {
- debug ("Restoring window to maximized");
- maximize ();
- }
- if (window_is_fullscreen)
- {
- debug ("Restoring window to fullscreen");
- fullscreen ();
- }
- }
-
- private string state_filename
- {
- owned get { return Path.build_filename (Environment.get_user_config_dir (), "simple-scan", "state"); }
- }
-
- private void load_state ()
- {
- debug ("Loading state from %s", state_filename);
-
- var f = new KeyFile ();
- try
- {
- f.load_from_file (state_filename, KeyFileFlags.NONE);
- }
- catch (Error e)
- {
- if (!(e is FileError.NOENT))
- warning ("Failed to load state: %s", e.message);
- }
- window_width = state_get_integer (f, "window", "width", 600);
- if (window_width <= 0)
- window_width = 600;
- window_height = state_get_integer (f, "window", "height", 400);
- if (window_height <= 0)
- window_height = 400;
- window_is_maximized = state_get_boolean (f, "window", "is-maximized");
- window_is_fullscreen = state_get_boolean (f, "window", "is-fullscreen");
- scan_type = Scanner.type_from_string(state_get_string (f, "scanner", "scan-type"));
- set_scan_type (scan_type);
- }
-
- private string state_get_string (KeyFile f, string group_name, string key, string default = "")
- {
- try
- {
- return f.get_string (group_name, key);
- }
- catch
- {
- return default;
- }
- }
-
- private int state_get_integer (KeyFile f, string group_name, string key, int default = 0)
- {
- try
- {
- return f.get_integer (group_name, key);
- }
- catch
- {
- return default;
- }
- }
-
- private bool state_get_boolean (KeyFile f, string group_name, string key, bool default = false)
- {
- try
- {
- return f.get_boolean (group_name, key);
- }
- catch
- {
- return default;
- }
- }
-
- private static string STATE_DIR = Path.build_filename (Environment.get_user_config_dir (), "simple-scan", null);
- private void save_state (bool force = false)
- {
- if (!force)
- {
- if (save_state_timeout != 0)
- Source.remove (save_state_timeout);
- save_state_timeout = Timeout.add (100, () =>
- {
- save_state (true);
- save_state_timeout = 0;
- return false;
- });
- return;
- }
-
- debug ("Saving state to %s", state_filename);
-
- var f = new KeyFile ();
- f.set_integer ("window", "width", window_width);
- f.set_integer ("window", "height", window_height);
- f.set_boolean ("window", "is-maximized", window_is_maximized);
- f.set_boolean ("window", "is-fullscreen", window_is_fullscreen);
- f.set_string ("scanner", "scan-type", Scanner.type_to_string(scan_type));
- try
- {
- DirUtils.create_with_parents (STATE_DIR, 0700);
- FileUtils.set_contents (state_filename, f.to_data ());
- }
- catch (Error e)
- {
- warning ("Failed to write state: %s", e.message);
- }
- }
-
- public void start ()
- {
- visible = true;
- autosave_manager = new AutosaveManager ();
- autosave_manager.book = book;
-
- if (autosave_manager.exists ())
- {
- prompt_to_load_autosaved_book.begin ((obj, res) => {
- bool restore = prompt_to_load_autosaved_book.end (res);
-
- if (restore)
- {
- autosave_manager.load ();
- }
-
- if (book.n_pages == 0)
- book_needs_saving = false;
- else
- {
- stack.set_visible_child_name ("document");
- book_view.selected_page = book.get_page (0);
- book_needs_saving = true;
- book_changed_cb (book);
- }
- });
- }
- }
-}
-
-private class CancellableProgressBar : Gtk.Box
-{
- private Gtk.ProgressBar bar;
- private Gtk.Button? button;
-
- public CancellableProgressBar (string? text, Cancellable? cancellable)
- {
- this.orientation = Gtk.Orientation.HORIZONTAL;
-
- bar = new Gtk.ProgressBar ();
- bar.visible = true;
- bar.set_text (text);
- bar.set_show_text (true);
- prepend (bar);
-
- if (cancellable != null)
- {
- button = new Gtk.Button.with_label (/* Text of button for cancelling save */
- _("Cancel"));
- button.visible = true;
- button.clicked.connect (() =>
- {
- set_visible (false);
- cancellable.cancel ();
- });
- prepend (button);
- }
- }
-
- public void set_fraction (double fraction)
- {
- bar.set_fraction (fraction);
- }
-
- public void remove_with_delay (uint delay, Gtk.ActionBar parent)
- {
- button.set_sensitive (false);
-
- Timeout.add (delay, () =>
- {
- parent.remove (this);
- return false;
- });
- }
-}
-
-private class CropActions
-{
- private GLib.SimpleActionGroup group;
-
- private GLib.SimpleAction crop_set;
- private GLib.SimpleAction crop_rotate;
-
- private GLib.ActionEntry[] crop_entries =
- {
- { "set", AppWindow.crop_set_action_cb, "s", "'none'" },
- { "rotate", AppWindow.crop_rotate_action_cb },
- };
-
- public CropActions (AppWindow window)
- {
- group = new GLib.SimpleActionGroup ();
- group.add_action_entries (crop_entries, window);
-
- crop_set = (GLib.SimpleAction) group.lookup_action ("set");
- crop_rotate = (GLib.SimpleAction) group.lookup_action ("rotate");
-
- window.insert_action_group ("crop", group);
- }
-
- public void update_current_crop (string? crop_name)
- {
- crop_rotate.set_enabled (crop_name != null);
-
- if (crop_name == null)
- crop_set.set_state ("none");
- else
- crop_set.set_state (crop_name);
- }
-}
+/*
+ * Copyright (C) 2009-2015 Canonical Ltd.
+ * Author: Robert Ancell ,
+ * Eduard Gotwig
+ *
+ * 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.
+ */
+
+private const int DEFAULT_DPI = 300;
+
+// UndoAction: Stores information about a page rotation or move operation
+private class UndoAction
+{
+ public enum ActionType
+ {
+ ROTATE,
+ MOVE,
+ DELETE,
+ CROP,
+ SPLIT_PAGE
+ }
+
+ public int64 timestamp;
+ public ActionType action_type;
+ public int page_index; // Page index (target for move, rotated page for rotate)
+ public int action_value; // Old location for move, number of right rotations for rotate
+ public int crop_x;
+ public int crop_y;
+ public int crop_width;
+ public int crop_height;
+ public string? crop_name;
+
+ public UndoAction (ActionType type, int64 timestamp, int index, int value = 0, int[] crop_data = {}, string? crop_name = null)
+ {
+ this.timestamp = timestamp;
+ this.action_type = type;
+ this.page_index = index;
+ this.action_value = value;
+ this.crop_x = crop_data.length > 0 ? crop_data[0] : 0;
+ this.crop_y = crop_data.length > 1 ? crop_data[1] : 0;
+ this.crop_width = crop_data.length > 2 ? crop_data[2] : 0;
+ this.crop_height = crop_data.length > 3 ? crop_data[3] : 0;
+ this.crop_name = crop_name;
+ }
+}
+
+[GtkTemplate (ui = "/org/gnome/SimpleScan/ui/app-window.ui")]
+public class AppWindow : Adw.ApplicationWindow
+{
+ /* definition of key accelerator arrays (must be static + unowned +
+ null‑terminated to silence compiler warnings - that's why it is here) */
+ private const string[] ACCEL_NEW_DOCUMENT = { "N", null };
+ private const string[] ACCEL_SCAN_SINGLE = { "1", null };
+ private const string[] ACCEL_SCAN_ADF = { "F", null };
+ private const string[] ACCEL_SCAN_BATCH = { "B", null };
+ private const string[] ACCEL_SCAN_SIDE_FRONT = { "F", null };
+ private const string[] ACCEL_SCAN_SIDE_BACK = { "B", null };
+ private const string[] ACCEL_SCAN_SIDE_BOTH = { "O", null };
+ private const string[] ACCEL_SCAN_START = { "F9", null };
+ private const string[] ACCEL_SCAN_STOP = { "Escape", null };
+ private const string[] ACCEL_ROTATE_LEFT = { "Left", null };
+ private const string[] ACCEL_ROTATE_RIGHT = { "Right", null };
+ private const string[] ACCEL_FLIP_PAGE = { "Up", "Down", null };
+ private const string[] ACCEL_DESKEW_TEXT_PAGE = { "T", null };
+ private const string[] ACCEL_DESKEW_PAGE = { "T", null };
+ private const string[] ACCEL_DETECT_EMPTY_PAGES = { "Delete", null };
+ private const string[] ACCEL_SPLIT_PAGE = { "R", null };
+ private const string[] ACCEL_CONVERT_TO_LINE_ART = { "L", null };
+ private const string[] ACCEL_CONVERT_TO_2BIT_GRAYSCALE = { "2", null };
+ private const string[] ACCEL_CONVERT_TO_GRAYSCALE = { "G", null };
+ private const string[] ACCEL_CONVERT_AUTOMATIC = { "G", null };
+ private const string[] ACCEL_RESTORE_IMAGE = { "F5", null };
+ private const string[] ACCEL_MOVE_LEFT = { "Left", "less", null };
+ private const string[] ACCEL_MOVE_RIGHT = { "Right", "greater", null };
+ private const string[] ACCEL_CROP_PAGE = { "R", null };
+ private const string[] ACCEL_COPY_PAGE = { "C", null };
+ private const string[] ACCEL_PASTE_IMAGE = { "V", null };
+ private const string[] ACCEL_PASTE_CROP = { "V", null };
+ private const string[] ACCEL_DELETE_PAGE = { "Delete", null };
+ private const string[] ACCEL_TOGGLE_DELETED_DISPLAY = { "H", null };
+ private const string[] ACCEL_SELECT_ALL = { "A", null };
+ private const string[] ACCEL_SAVE = { "S", null };
+ private const string[] ACCEL_EMAIL = { "E", null };
+ private const string[] ACCEL_PRINT = { "P", null };
+ private const string[] ACCEL_HELP = { "F1", null };
+ private const string[] ACCEL_QUIT = { "Q", null };
+ private const string[] ACCEL_PREFERENCES = { "comma", null };
+ private const string[] ACCEL_SHORTCUTS = { "F1", null };
+ private const string[] ACCEL_ZOOM_IN = { "plus", null };
+ private const string[] ACCEL_ZOOM_OUT = { "minus", null };
+ private const string[] ACCEL_TOGGLE_VIEW = { "F11", null };
+ private const string[] ACCEL_EDIT_PAGE = { "D", null };
+ private const string[] ACCEL_SELECT_EVEN_PAGES = { "A", null };
+ private const string[] ACCEL_SELECT_ODD_PAGES = { "A", null };
+ private const string[] ACCEL_REVERSE_ORDER = { "I", null };
+ private const string[] ACCEL_UNDO = { "Z", null };
+ private const string[] ACCEL_REDO = { "Y", null };
+
+ private const GLib.ActionEntry[] action_entries =
+ {
+ { "scan_type", scan_type_action_cb, "s", "'single'"},
+ { "dpi", dpi_action_cb, "s", "'300'"},
+ { "paper_size", paper_size_action_cb, "s", "'Automatic'"},
+ { "scan_side", scan_side_action_cb, "s", "'both'"},
+ { "scan_single", scan_single_cb },
+ { "scan_adf", scan_adf_cb },
+ { "scan_batch", scan_batch_cb },
+ { "scan_start", scan_start_cb},
+ { "scan_stop", scan_stop_cb },
+ { "rotate_left", rotate_left_cb },
+ { "rotate_right", rotate_right_cb },
+ { "flip_page", flip_page_cb },
+ { "deskew_text_page", deskew_text_page_cb },
+ { "detect_empty_pages", detect_empty_pages_cb },
+ { "convert_to_grayscale", convert_to_grayscale_cb },
+ { "convert_to_2bit_grayscale", convert_to_2bit_grayscale_cb },
+ { "convert_to_line_art", convert_to_line_art_cb },
+ { "automatic_color_filter", automatic_color_filter_cb },
+ { "restore_image", restore_image_cb },
+ { "move_left", move_left_cb },
+ { "move_right", move_right_cb },
+ { "crop_page", crop_page_cb },
+ { "copy_page", copy_page_cb },
+ { "paste_image", paste_image_cb },
+ { "paste_crop", paste_crop_cb },
+ { "delete_page", delete_page_cb },
+ { "toggle_deleted_display", toggle_deleted_display_cb },
+ { "split_page", split_page_cb },
+ { "select_all", select_all_cb },
+ { "select_even_pages", select_even_pages_cb },
+ { "select_odd_pages", select_odd_pages_cb },
+ { "reverse_order", reverse_order_cb },
+ { "reorder", reorder_document_cb },
+ { "new", new_document_cb },
+ { "save", save_document_cb },
+ { "open_folder", open_folder_cb, "s" },
+ { "email", email_document_cb },
+ { "export_selection", export_selection_cb },
+ { "print", print_document_cb },
+ { "preferences", preferences_cb },
+ { "help", help_cb },
+ { "about", about_cb },
+ { "quit", quit_cb },
+ { "zoom_in", zoom_in_cb },
+ { "zoom_out", zoom_out_cb },
+ { "edit_page", edit_button_cb },
+ { "toggle_view", toggle_view_cb },
+ { "undo", undo_cb },
+ { "redo", redo_cb },
+ { "deskew_page", deskew_page_cb },
+ { "dismiss-filter-info", dismiss_filter_info_cb },
+ { "dismiss-deskew-info", dismiss_deskew_info_cb }
+ };
+
+ private GLib.SimpleAction new_document_action;
+ private GLib.SimpleAction save_document_action;
+ private GLib.SimpleAction scan_type_action;
+ private GLib.SimpleAction dpi_action;
+ private GLib.SimpleAction paper_size_action;
+ private GLib.SimpleAction scan_side_action;
+
+ private GLib.SimpleAction delete_page_action;
+ private GLib.SimpleAction toggle_deleted_display_action;
+ private GLib.SimpleAction restore_page_action;
+ private GLib.SimpleAction deskew_text_page_action;
+ private GLib.SimpleAction detect_empty_pages_action;
+ private GLib.SimpleAction convert_to_grayscale_action;
+ private GLib.SimpleAction convert_to_2bit_grayscale_action;
+ private GLib.SimpleAction convert_to_line_art_action;
+ private GLib.SimpleAction automatic_color_filter_action;
+ private GLib.SimpleAction page_move_left_action;
+ private GLib.SimpleAction page_move_right_action;
+ private GLib.SimpleAction copy_to_clipboard_action;
+ private GLib.SimpleAction paste_image_action;
+ private GLib.SimpleAction paste_crop_action;
+ private GLib.SimpleAction reverse_order_action;
+ private GLib.SimpleAction rotate_left_action;
+ private GLib.SimpleAction rotate_right_action;
+ private GLib.SimpleAction flip_page_action;
+ private GLib.SimpleAction crop_page_action;
+ private GLib.SimpleAction split_page_action;
+ private GLib.SimpleAction undo_action;
+ private GLib.SimpleAction redo_action;
+
+ private Array undo_actions;
+ private Array redo_actions;
+
+ private CropActions crop_actions;
+
+ private Settings settings;
+ private ScanType scan_type = ScanType.SINGLE;
+
+ private PreferencesDialog preferences_dialog;
+
+ private bool user_selected_device;
+
+ [GtkChild]
+ private unowned Adw.WindowTitle window_title;
+ [GtkChild]
+ private unowned Gtk.PopoverMenu page_menu;
+ [GtkChild]
+ private unowned Gtk.Stack stack;
+ [GtkChild]
+ private unowned Adw.StatusPage status_page;
+ [GtkChild]
+ private unowned Gtk.Label status_secondary_label;
+ [GtkChild]
+ private unowned Gtk.Spinner status_spinner;
+ private ListStore device_model;
+ [GtkChild]
+ private unowned Gtk.Box device_buttons_box;
+ [GtkChild]
+ private unowned Gtk.ListBox device_list_box;
+ private List device_checkmarks;
+ private uint selected_device_position = Gtk.INVALID_LIST_POSITION;
+ [GtkChild]
+ private unowned Gtk.Box main_vbox;
+ [GtkChild]
+ private unowned Gtk.Button save_button;
+ [GtkChild]
+ private unowned Gtk.Button scan_button;
+ [GtkChild]
+ private unowned Gtk.Button scan_button_startup;
+ [GtkChild]
+ private unowned Gtk.Box progress_box;
+ [GtkChild]
+ private unowned Adw.SplitButton crop_split_button;
+ [GtkChild]
+ private unowned Gtk.MenuButton edit_button;
+ [GtkChild]
+ private unowned Gtk.Button delete_button;
+ [GtkChild]
+ private unowned Gtk.Button hide_deleted_button;
+ [GtkChild]
+ private unowned SnapshotArea scan_progress_arc;
+ [GtkChild]
+ private unowned Gtk.Stack scan_button_stack;
+ [GtkChild]
+ private unowned Gtk.Image scan_button_icon;
+ [GtkChild]
+ private unowned Gtk.Spinner scan_button_spinner;
+ [GtkChild]
+ private unowned Gtk.MenuButton menu_button;
+ [GtkChild]
+ private unowned Gtk.MenuButton dpi_menu;
+ [GtkChild]
+ private unowned Gtk.MenuButton paper_size_menu;
+ [GtkChild]
+ private unowned Adw.ToastOverlay toast_overlay;
+
+ private bool have_devices = false;
+ private string? missing_driver = null;
+
+ public Book book { get; private set; }
+ private bool book_needs_saving;
+ private string? book_uri = null;
+
+ public Page selected_page
+ {
+ get
+ {
+ return book_view.selected_page;
+ }
+ set
+ {
+ book_view.selected_page = value;
+ }
+ }
+
+ private AutosaveManager autosave_manager;
+
+ private ThreadPool page_operation_pool;
+ private ThreadPool crop_operation_pool;
+ private ThreadPool empty_page_detection_pool;
+
+ private BookView book_view;
+ private Page? current_selected_page = null;
+ private bool updating_page_menu;
+ private bool processing_pages;
+
+ private GLib.DateTime meta_created = new GLib.DateTime.now_local();
+
+ private int dpi = DEFAULT_DPI;
+
+ private bool scanning_ = false;
+ private double scan_progress = 0.0;
+ private double time_to_wait = 0.0;
+ public bool scanning
+ {
+ get { return scanning_; }
+ set
+ {
+ scanning_ = value;
+ stack.set_visible_child_name ("document");
+
+ delete_page_action.set_enabled (!value);
+ if (value)
+ {
+ scan_button_icon.icon_name = "media-playback-stop-symbolic";
+ scan_button.add_css_class ("destructive-action");
+ }
+ else
+ {
+ scan_button.remove_css_class ("destructive-action");
+ update_scan_button_icon ();
+ }
+ }
+ }
+
+ private string? page_copy_crop_name = null;
+ private bool page_copy_has_crop = false;
+ private int page_copy_crop_x = 0;
+ private int page_copy_crop_y = 0;
+ private int page_copy_crop_width = 0;
+ private int page_copy_crop_height = 0;
+ private int page_copy_crop_dpi = 1;
+
+ private int window_width;
+ private int window_height;
+ private bool window_is_maximized;
+ private bool window_is_fullscreen;
+
+ private uint save_state_timeout;
+ private uint delay_animation_timeout = 0;
+
+ private int n_tmp_folders = 0;
+
+ public int brightness
+ {
+ get { return preferences_dialog.get_brightness (); }
+ set { preferences_dialog.set_brightness (value); }
+ }
+
+ public int contrast
+ {
+ get { return preferences_dialog.get_contrast (); }
+ set { preferences_dialog.set_contrast (value); }
+ }
+
+ public int page_delay
+ {
+ get { return preferences_dialog.get_page_delay (); }
+ set { preferences_dialog.set_page_delay (value); }
+ }
+
+ private int scan_count = 0;
+
+ public bool auto_rotation_correction
+ {
+ get { return preferences_dialog.get_auto_rotation_correction (); }
+ set { preferences_dialog.set_auto_rotation_correction (value); }
+ }
+
+ public bool auto_color_reduction
+ {
+ get { return preferences_dialog.get_auto_color_reduction (); }
+ set { preferences_dialog.set_auto_color_reduction (value); }
+ }
+
+ public bool alternating_page_orientation
+ {
+ get { return preferences_dialog.get_alternating_page_orientation (); }
+ set { preferences_dialog.set_alternating_page_orientation (value); }
+ }
+
+ public PageOrientation page_orientation
+ {
+ get { return preferences_dialog.get_page_orientation (); }
+ set { preferences_dialog.set_page_orientation (value); }
+ }
+
+ public int empty_page_threshold
+ {
+ get { return preferences_dialog.get_empty_page_threshold (); }
+ set { preferences_dialog.set_empty_page_threshold (value); }
+ }
+
+ public signal void start_scan (string? device, ScanOptions options);
+ public signal void stop_scan ();
+ public signal void redetect ();
+ public signal void device_selected (string device);
+
+ public AppWindow ()
+ {
+ /* make sure a window resize results in updating the layout.
+ before, drawing_area.resize.connect (drawing_area_resize_cb)
+ has been used, but that did not always respond to window resize somehow*/
+ this.map.connect (() => {
+ var surface = this.get_native ().get_surface ();
+
+ surface.notify["width"].connect (() => {
+ book_view.app_window_resize_cb ();
+ });
+ surface.notify["height"].connect (() => {
+ book_view.app_window_resize_cb ();
+ });
+ });
+
+ this.notify["is-active"].connect (() => {
+ if (this.is_active) {
+ // Window regained focus → remove the notification
+ this.application.withdraw_notification ("scan-finished");
+ }
+ });
+
+ /* Clean up old temporary files on startup */
+ cleanup_tmp_dir ();
+
+ settings = new Settings ("org.gnome.SimpleScan");
+
+ device_model = new ListStore (typeof (ScanDevice));
+ device_checkmarks = new List ();
+
+ book = new Book ();
+ book.page_added.connect (page_added_cb);
+ book.reordered.connect (reordered_cb);
+ book.page_removed.connect (page_removed_cb);
+ book.changed.connect (book_changed_cb);
+
+ undo_actions = new Array ();
+ redo_actions = new Array ();
+
+ /* Initialize thread pools */
+ try
+ {
+ page_operation_pool = new ThreadPool.with_owned_data (
+ (task) => { page_operation_worker (task); },
+ int.max((int) GLib.get_num_processors () - 1, 1),
+ true
+ );
+ crop_operation_pool = new ThreadPool.with_owned_data (
+ (task) => { crop_operation_worker (task); },
+ int.max((int) GLib.get_num_processors () - 1, 1),
+ true
+ );
+ empty_page_detection_pool = new ThreadPool.with_owned_data (
+ (task) => { empty_page_detection_worker (task); },
+ int.max((int) GLib.get_num_processors () - 1, 1),
+ true
+ );
+ }
+ catch (Error e)
+ {
+ warning ("Failed to create thread pools: %s\n", e.message);
+ }
+ processing_pages = false;
+
+ load ();
+
+ clear_document ();
+ }
+
+ ~AppWindow ()
+ {
+ book.page_added.disconnect (page_added_cb);
+ book.reordered.disconnect (reordered_cb);
+ book.page_removed.disconnect (page_removed_cb);
+ book.changed.disconnect (book_changed_cb);
+ book_view.move_page_with_undo.disconnect (book_view_move_page_with_undo_cb);
+ }
+
+ private void book_view_move_page_with_undo_cb (BookView view, Page page, uint location)
+ {
+ move_page_with_undo (page, location);
+ }
+
+ public void show_error_dialog (string error_title, string error_text)
+ {
+ var dialog = new Adw.AlertDialog (error_title,
+ error_text);
+ dialog.add_response ("close", _("_Close"));
+ dialog.set_response_appearance ("close", Adw.ResponseAppearance.SUGGESTED);
+ dialog.present (this);
+ }
+
+ public async AuthorizeDialogResponse authorize (string resource)
+ {
+ /* Label in authorization dialog. “%s” is replaced with the name of the resource requesting authorization */
+ var description = _("Username and password required to access “%s”").printf (resource);
+ var authorize_dialog = new AuthorizeDialog (this, description);
+ authorize_dialog.transient_for = this;
+
+ return yield authorize_dialog.open ();
+ }
+
+ private void update_scan_status ()
+ {
+ scan_button.sensitive = false;
+
+ if (!have_devices)
+ {
+ status_page.set_title (/* Label shown when searching for scanners */
+ _("Searching for Scanners…"));
+ status_secondary_label.set_text (_("This takes a few seconds"));
+ device_buttons_box.visible = false;
+ status_spinner.visible = true;
+
+ /* remove highlighting scan button */
+ scan_button.remove_css_class("suggested-action");
+
+ /* display loading animation for scan button */
+ scan_button_stack.set_visible_child(scan_button_spinner);
+ }
+ else if (get_selected_device () != null)
+ {
+ scan_button.sensitive = true;
+ status_page.set_title (/* Label shown when detected a scanner */
+ _("Ready to Scan"));
+ status_secondary_label.set_text (_("Select a device and press the scan button"));
+ device_buttons_box.visible = true;
+ status_spinner.visible = false;
+ device_buttons_box.sensitive = true;
+ device_list_box.sensitive = true;
+
+ /* highlight scan button */
+ scan_button.add_css_class("suggested-action");
+
+ /* display scan icon for scan button */
+ scan_button_stack.set_visible_child(scan_button_icon);
+ }
+ else if (this.missing_driver != null && stack.get_visible_child_name () == "startup")
+ {
+ status_page.set_title (/* Warning displayed when no drivers are installed but a compatible scanner is detected */
+ _("Additional Software Needed"));
+ /* Instructions to install driver software */
+ status_secondary_label.set_markup (_("You need to install driver software for your scanner"));
+ device_buttons_box.visible = false;
+ status_spinner.visible = false;
+ }
+ else
+ {
+ /* Warning displayed when no scanners are detected */
+ status_page.set_title (_("Searching for Scanners…"));
+ /* Hint to user on why there are no scanners detected */
+ status_secondary_label.set_text (_("Please check your scanner is connected and powered on"));
+ status_secondary_label.visible = true;
+ device_buttons_box.visible = false;
+ status_spinner.visible = true;
+ /* remove highlighting scan button */
+ scan_button.remove_css_class("suggested-action");
+
+ /* automatically redetect devices */
+ redetect ();
+
+ /* display loading animation for scan button */
+ scan_button_stack.set_visible_child(scan_button_spinner);
+ }
+ }
+
+ public void set_scan_devices (List devices, string? missing_driver = null)
+ {
+ have_devices = true;
+ this.missing_driver = missing_driver;
+
+ {
+ device_model.remove_all ();
+ selected_device_position = Gtk.INVALID_LIST_POSITION;
+ device_checkmarks = new List ();
+
+ while (device_list_box.get_first_child () != null)
+ device_list_box.remove (device_list_box.get_first_child ());
+
+ /* find devices with identical labels */
+ var seen_labels = new HashTable(str_hash, str_equal);
+ var duplicated_labels = new HashTable(str_hash, str_equal);
+ foreach (var device in devices) {
+ if (seen_labels.contains(device.label)) {
+ duplicated_labels.insert(device.label, true);
+ } else {
+ seen_labels.insert(device.label, true);
+ }
+ }
+
+ /* Add new devices (with device.name subtitle if duplicated label) */
+ uint position = 0;
+ foreach (var device in devices)
+ {
+ device_model.append (device);
+ device_list_box.append (create_device_row (device, position, duplicated_labels.contains(device.label)));
+ position++;
+ }
+
+ /* Select the previously selected device or the first available device if the startup page is visible. Else we loaded an autosave and we don't want to set an arbitrary (the first) scanner, only the one during last scan and display the loading animation until the scanner is found. */
+ var device_name = settings.get_string ("selected-device");
+ if (device_name != null && find_device_by_name (device_name, out position) != null)
+ select_device_position (position);
+ else if (devices.length () > 0 && stack.get_visible_child_name () == "startup")
+ select_device_position (0);
+ }
+
+ update_scan_status ();
+ }
+
+ private async bool prompt_to_load_autosaved_book ()
+ {
+ var dialog = new Adw.AlertDialog ("",
+ /* Contents of dialog that shows if autosaved book should be loaded. */
+ _("An autosaved book exists. Do you want to open it?"));
+
+ dialog.add_response ("no", _("_No"));
+ dialog.add_response ("yes", _("_Yes"));
+
+ dialog.set_response_appearance ("no", Adw.ResponseAppearance.DESTRUCTIVE);
+ dialog.set_response_appearance ("yes", Adw.ResponseAppearance.SUGGESTED);
+
+ dialog.set_default_response("yes");
+ dialog.present (this);
+
+ string response = "yes";
+
+ SourceFunc callback = prompt_to_load_autosaved_book.callback;
+ dialog.response.connect((res) => {
+ response = res;
+ callback();
+ });
+
+ yield;
+
+ return response == "yes";
+ }
+
+ private string? get_selected_device ()
+ {
+ if (selected_device_position != Gtk.INVALID_LIST_POSITION)
+ {
+ return ((ScanDevice) device_model.get_item (selected_device_position)).name;
+ }
+
+ return null;
+ }
+
+ public void display_page_delay_arc (int delay_ms)
+ {
+ /* Cancel any existing animation */
+ if (delay_animation_timeout != 0)
+ {
+ Source.remove (delay_animation_timeout);
+ delay_animation_timeout = 0;
+ }
+
+ if (delay_ms <= 0)
+ return;
+
+ int animation_steps = 200;
+ int step_duration = delay_ms / animation_steps;
+ int current_step = 0;
+
+ delay_animation_timeout = GLib.Timeout.add (step_duration, () => {
+ current_step++;
+ time_to_wait = (double)(animation_steps - current_step) / animation_steps;
+ scan_progress_arc.queue_draw ();
+
+ if (time_to_wait <= 0)
+ {
+ delay_animation_timeout = 0;
+ return false;
+ }
+ return true;
+ });
+ }
+
+ public void set_selected_device (string device)
+ {
+ user_selected_device = true;
+
+ uint position;
+ if (find_device_by_name (device, out position) == null)
+ return;
+
+ select_device_position (position);
+ }
+
+ private ScanDevice? find_device_by_name(string name, out uint position)
+ {
+ for (uint i = 0; i < device_model.get_n_items (); i++)
+ {
+ var item = (ScanDevice?) device_model.get_item (i);
+ if (item.name == name) {
+ position = i;
+ return item;
+ }
+ }
+
+ position = Gtk.INVALID_LIST_POSITION;
+ return null;
+ }
+
+ private void select_device_position (uint position)
+ {
+ if (selected_device_position != Gtk.INVALID_LIST_POSITION && selected_device_position < device_checkmarks.length ())
+ {
+ var old_checkmark = (Gtk.Image) device_checkmarks.nth_data (selected_device_position);
+ old_checkmark.set_opacity (0.0);
+ }
+
+ if (position < device_checkmarks.length ())
+ {
+ var new_checkmark = (Gtk.Image) device_checkmarks.nth_data (position);
+ new_checkmark.set_opacity (1.0);
+ }
+
+ selected_device_position = position;
+
+ /* Request the capabilities of the newly selected device so the resolution
+ and paper size menus can be adjusted to what the device supports. */
+ var device = (ScanDevice?) device_model.get_item (position);
+ if (device != null)
+ device_selected (device.name);
+ }
+
+ private Adw.ActionRow create_device_row (ScanDevice device, uint position, bool use_subtitle)
+ {
+ var row = new Adw.ActionRow ();
+ row.title = device.label;
+ if (use_subtitle)
+ row.subtitle = device.name;
+ row.activatable = true;
+ row.selectable = true;
+
+ var prefix = (Gtk.Image) new Gtk.Image.from_icon_name ("scanner-symbolic");
+ row.add_prefix (prefix);
+
+ var checkmark = (Gtk.Image) new Gtk.Image.from_icon_name ("object-select-symbolic");
+ /* use opacity instead of visible to reserve the space */
+ checkmark.set_opacity (0.0);
+
+ row.add_suffix (checkmark);
+ device_checkmarks.append (checkmark);
+
+ row.activated.connect (() => {
+ select_device_position (position);
+ });
+
+ return row;
+ }
+
+ public void scan_finished ()
+ {
+ /* allow page operations again */
+ processing_pages = false;
+ update_page_menu ();
+
+ /* allow toggling grid */
+ book_view.set_toggle_grid_button_sensitive(true);
+
+ /* only send the notification if app is not active */
+ if (!this.is_active)
+ {
+ /* Create the notification */
+ var n = new GLib.Notification (_("Scan complete"));
+
+ /* Body text shown in the GNOME notification bubble */
+ var notify_text = _("Scanned %d pages.").printf ((int) scan_count);
+ n.set_body (notify_text);
+ n.set_icon (new GLib.ThemedIcon ("org.gnome.SimpleScan"));
+ this.application.send_notification ("scan-finished", n);
+ }
+ }
+
+ public class MetadataDialog : Gtk.Dialog {
+ public Gtk.Entry title_entry;
+ public Gtk.Entry author_entry;
+ public Gtk.Entry subject_entry;
+ public Gtk.Entry keywords_entry;
+ public Gtk.Calendar created_calendar;
+ public Gtk.Switch pdfa_switch;
+
+ public MetadataDialog(Gtk.Window parent, Settings settings) {
+ Object(transient_for: parent, modal: true, title: (_("Document Metadata")));
+
+ var grid = new Gtk.Grid();
+ grid.column_spacing = 12;
+ grid.row_spacing = 12;
+ grid.margin_top = 12;
+ grid.margin_bottom = 12;
+ grid.margin_start = 12;
+ grid.margin_end = 12;
+
+ title_entry = new Gtk.Entry();
+ title_entry.set_text(settings.get_string ("meta-title"));
+ author_entry = new Gtk.Entry();
+ var author = settings.get_string ("meta-author") == "" ? GLib.Environment.get_real_name() : settings.get_string ("meta-author");
+ author_entry.set_text(author);
+ subject_entry = new Gtk.Entry();
+ subject_entry.set_text(settings.get_string ("meta-subject"));
+ keywords_entry = new Gtk.Entry();
+ keywords_entry.set_text(settings.get_string ("meta-keywords"));
+ created_calendar = new Gtk.Calendar();
+ pdfa_switch = new Gtk.Switch();
+ pdfa_switch.set_active(settings.get_boolean ("pdfa-conformance"));
+ pdfa_switch.halign = Gtk.Align.START;
+
+ grid.attach(new Gtk.Label(_("Title")), 0, 0, 1, 1);
+ grid.attach(title_entry, 1, 0, 1, 1);
+
+ grid.attach(new Gtk.Label(_("Author")), 0, 1, 1, 1);
+ grid.attach(author_entry, 1, 1, 1, 1);
+
+ grid.attach(new Gtk.Label(_("Subject")), 0, 2, 1, 1);
+ grid.attach(subject_entry, 1, 2, 1, 1);
+
+ grid.attach(new Gtk.Label(_("Keywords")), 0, 3, 1, 1);
+ grid.attach(keywords_entry, 1, 3, 1, 1);
+
+ grid.attach(new Gtk.Label(_("Created")), 0, 4, 1, 1);
+ grid.attach(created_calendar, 1, 4, 1, 1);
+
+ grid.attach(new Gtk.Label(_("PDF/A Conformance")), 0, 5, 1, 1);
+ grid.attach(pdfa_switch, 1, 5, 1, 1);
+
+ get_content_area().append(grid);
+
+ add_button(_("Cancel"), Gtk.ResponseType.CANCEL);
+ add_button(_("OK"), Gtk.ResponseType.OK);
+ }
+ }
+
+ private async bool edit_metadata_async() {
+ var dialog = new MetadataDialog(this, settings);
+ bool is_ok = false;
+
+ var source = new GLib.IdleSource();
+ source.set_priority(GLib.Priority.DEFAULT);
+ source.set_callback(edit_metadata_async.callback);
+
+ dialog.response.connect((response_id) => {
+ if (response_id == Gtk.ResponseType.OK) {
+ settings.set_string ("meta-title", dialog.title_entry.text);
+ settings.set_string ("meta-author", dialog.author_entry.text);
+ settings.set_string ("meta-subject", dialog.subject_entry.text);
+ settings.set_string ("meta-keywords", dialog.keywords_entry.text);
+ meta_created = dialog.created_calendar.get_date();
+ settings.set_boolean ("pdfa-conformance" , dialog.pdfa_switch.get_active());
+ is_ok = true;
+ }
+
+ dialog.destroy();
+
+ source.attach(GLib.MainContext.default());
+ });
+
+ dialog.present();
+
+ yield;
+
+ return is_ok;
+ }
+
+ private async string? choose_file_location (uint export_n_pages)
+ {
+ /* Get directory to save to */
+ string? directory = null;
+ directory = settings.get_string ("save-directory");
+
+ if (directory == null || directory == "")
+ try
+ {
+ directory = GLib.Filename.to_uri(Environment.get_user_special_dir (UserDirectory.DOCUMENTS));
+ }
+ catch (Error e)
+ {
+ warning ("Failed to open chosen directory: %s", e.message);
+ }
+
+ var save_dialog = new Gtk.FileDialog ();
+ save_dialog.title = _("Save As…");
+ save_dialog.modal = true;
+ save_dialog.accept_label = _("_Save");
+
+ var save_format = settings.get_string ("save-format");
+ if (book_uri != null)
+ {
+ save_dialog.initial_file = GLib.File.new_for_uri (book_uri);
+ }
+ else
+ {
+ save_dialog.initial_folder = GLib.File.new_for_uri (directory);
+
+ /* Default filename to use when saving document. */
+ /* To that filename the extension will be added, eg. ".pdf" */
+ var now = new DateTime.now_local ();
+ string timestamp = now.format ("_%Y%m%d_%H%M");
+ save_dialog.initial_name = (_("Scan") + timestamp + "." + mime_type_to_extension (save_format));
+ }
+
+ var filters = new ListStore (typeof (Gtk.FileFilter));
+
+ var pdf_filter = new Gtk.FileFilter ();
+ pdf_filter.set_filter_name (_("PDF (multi-page document)"));
+ pdf_filter.add_pattern ("*.pdf" );
+ pdf_filter.add_mime_type ("application/pdf");
+ filters.append (pdf_filter);
+
+ var jpeg_filter = new Gtk.FileFilter ();
+ jpeg_filter.set_filter_name (_("JPEG (compressed)"));
+ jpeg_filter.add_pattern ("*.jpg" );
+ jpeg_filter.add_pattern ("*.jpeg" );
+ jpeg_filter.add_mime_type ("image/jpeg");
+ filters.append (jpeg_filter);
+
+ var png_filter = new Gtk.FileFilter ();
+ png_filter.set_filter_name (_("PNG (lossless)"));
+ png_filter.add_pattern ("*.png" );
+ png_filter.add_mime_type ("image/png");
+ filters.append (png_filter);
+
+ var webp_filter = new Gtk.FileFilter ();
+ webp_filter.set_filter_name (_("WebP (compressed)"));
+ webp_filter.add_pattern ("*.webp" );
+ webp_filter.add_mime_type ("image/webp");
+ filters.append (webp_filter);
+
+ var all_filter = new Gtk.FileFilter ();
+ all_filter.set_filter_name (_("All Files"));
+ all_filter.add_pattern ("*");
+ filters.append (all_filter);
+
+ save_dialog.filters = filters;
+
+ switch (save_format)
+ {
+ case "application/pdf":
+ save_dialog.default_filter = pdf_filter;
+ break;
+ case "image/jpeg":
+ save_dialog.default_filter = jpeg_filter;
+ break;
+ case "image/png":
+ save_dialog.default_filter = png_filter;
+ break;
+ case "image/webp":
+ save_dialog.default_filter = webp_filter;
+ break;
+ }
+
+ var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
+ box.visible = true;
+ box.spacing = 10;
+ box.set_halign (Gtk.Align.CENTER);
+
+ while (true)
+ {
+ File? file = null;
+ try {
+ file = yield save_dialog.save (this, null);
+ }
+ catch (Error e)
+ {
+ warning ("Failed to open save dialog: %s", e.message);
+ }
+
+ if (file == null)
+ {
+ return null;
+ }
+
+ var uri = file.get_uri ();
+
+ var extension = uri_extension(uri);
+
+ var mime_type = extension_to_mime_type(extension);
+ mime_type = mime_type != null ? mime_type : "application/pdf";
+
+ settings.set_string ("save-format", mime_type);
+
+ if (extension == null)
+ uri += "." + mime_type_to_extension (mime_type);
+
+ /* Check the file(s) don't already exist */
+ var files = new List ();
+ if (mime_type == "image/jpeg" || mime_type == "image/png" || mime_type == "image/webp")
+ {
+ for (var j = 0; j < export_n_pages; j++)
+ {
+ files.append (make_indexed_file (uri, j, export_n_pages));
+ }
+ }
+ else
+ files.append (File.new_for_uri (uri));
+
+ var overwrite_check = true;
+
+ /* We assume that GTK or system file dialog asked about overwrite already so we reask only if there is more than one file or we changed the name
+ Ideally in flatpack era we should not modify file name after save dialog is done
+ but for the sake of keeping old functionality in tact we leave it as it */
+ if (files.length () > 1 || file.get_uri () != uri)
+ {
+ overwrite_check = yield check_overwrite (this, files);
+ }
+
+ if (overwrite_check)
+ {
+ var directory_uri = uri.substring (0, uri.last_index_of ("/") + 1);
+ settings.set_string ("save-directory", directory_uri);
+ return uri;
+ }
+ }
+ }
+
+ private async bool check_overwrite (Gtk.Window parent, List files)
+ {
+ foreach (var file in files)
+ {
+ if (!file.query_exists ())
+ continue;
+
+ var title = _("A file named “%s” already exists. Do you want to replace it?").printf(file.get_basename ());
+
+ var dialog = new Adw.AlertDialog (/* Contents of dialog that shows if saving would overwrite and existing file. %s is replaced with the name of the file. */
+ title,
+ null);
+
+ dialog.add_response ("cancel", _("_Cancel"));
+ dialog.add_response ("replace", _("_Replace"));
+
+ dialog.set_response_appearance ("replace", Adw.ResponseAppearance.DESTRUCTIVE);
+
+ SourceFunc callback = check_overwrite.callback;
+ string response = "cancel";
+
+ dialog.response.connect ((res) => {
+ response = res;
+ callback ();
+ });
+
+ dialog.present (parent);
+
+ yield;
+
+ if (response != "replace")
+ return false;
+ }
+
+ return true;
+ }
+
+ private string? mime_type_to_extension (string mime_type)
+ {
+ if (mime_type == "application/pdf")
+ return "pdf";
+ else if (mime_type == "image/jpeg")
+ return "jpg";
+ else if (mime_type == "image/png")
+ return "png";
+ else if (mime_type == "image/webp")
+ return "webp";
+ else
+ return null;
+ }
+
+ private string? extension_to_mime_type (string extension)
+ {
+ var extension_lower = extension.down ();
+ if (extension_lower == "pdf")
+ return "application/pdf";
+ else if (extension_lower == "jpg" || extension_lower == "jpeg")
+ return "image/jpeg";
+ else if (extension_lower == "png")
+ return "image/png";
+ else if (extension_lower == "webp")
+ return "image/webp";
+ else
+ return null;
+ }
+
+ private string? uri_extension (string uri)
+ {
+ var extension_index = uri.last_index_of_char ('.');
+ if (extension_index < 0)
+ return null;
+
+ return uri.substring (extension_index + 1);
+ }
+
+ private string uri_to_mime_type (string uri)
+ {
+ var extension = uri_extension(uri);
+ if (extension == null)
+ return "image/jpeg";
+
+ var mime_type = extension_to_mime_type (extension);
+ if (mime_type == null)
+ return "image/jpeg";
+
+ return mime_type;
+ }
+
+ private async bool save_document_async (bool selected_pages_only = false)
+ {
+ uint export_n_pages = book.n_pages;
+
+ if (selected_pages_only)
+ {
+ export_n_pages = book_view.get_n_selected_pages ();
+ if (export_n_pages == 0)
+ return false;
+ }
+
+ var uri = yield choose_file_location (export_n_pages);
+ if (uri == null)
+ return false;
+
+ var file = File.new_for_uri (uri);
+ if (selected_pages_only)
+ debug ("Exporting selection to '%s'", uri);
+ else
+ debug ("Saving to '%s'", uri);
+
+ var mime_type = uri_to_mime_type (uri);
+
+ if (mime_type == "application/pdf")
+ {
+ bool ok = yield edit_metadata_async();
+ if (!ok)
+ return false;
+ }
+
+ processing_pages = true;
+ update_page_menu ();
+
+ var cancellable = new Cancellable ();
+ var progress_label = selected_pages_only ? _("Exporting") : _("Saving");
+ var progress_bar = new CancellableProgressBar (progress_label, cancellable);
+ progress_box.append (progress_bar);
+ progress_bar.visible = true;
+ save_button.sensitive = false;
+ try
+ {
+ yield book.save_async (mime_type, settings.get_int ("jpeg-quality"), file,
+ (fraction) =>
+ {
+ progress_bar.set_fraction (fraction);
+ }, cancellable, selected_pages_only,
+ settings.get_string ("meta-title"), settings.get_string ("meta-author"), settings.get_string ("meta-subject"), settings.get_string ("meta-keywords"), meta_created, settings.get_boolean ("pdfa-conformance"));
+ }
+ catch (Error e)
+ {
+ save_button.sensitive = true;
+ progress_bar.remove_with_delay (0, progress_box);
+ if (selected_pages_only)
+ warning ("Error exporting file: %s", e.message);
+ else
+ warning ("Error saving file: %s", e.message);
+ show_error_dialog (/* Title of error dialog when save failed */
+ selected_pages_only ? _("Failed to export file") : _("Failed to save file"),
+ e.message);
+ processing_pages = false;
+ update_page_menu ();
+ return false;
+ }
+ save_button.sensitive = true;
+ progress_bar.remove_with_delay (500, progress_box);
+
+ try
+ {
+ yield book.postprocess_async (
+ mime_type, file, settings.get_boolean ("postproc-enabled"), settings.get_string ("postproc-script"),
+ settings.get_string ("postproc-arguments"), settings.get_boolean ("postproc-keep-original"));
+ }
+ catch (Error e)
+ {
+ warning ("Error running postprocessing: %s", e.message);
+ show_error_dialog (/* Title of error dialog when postprocessing failed */
+ _("Failed to run postprocessing"),
+ e.message);
+ }
+
+ if (!selected_pages_only)
+ {
+ book_needs_saving = false;
+ book_uri = uri;
+ }
+
+ var toast_message = selected_pages_only ? _("Selection Exported") : _("Document Saved");
+ var toast = new Adw.Toast (toast_message) {
+ button_label = _("Open Folder"),
+ action_name = "app.open_folder",
+ action_target = new Variant.string (file.get_uri ())
+ };
+
+ toast_overlay.add_toast (toast);
+ processing_pages = false;
+ update_page_menu ();
+ return true;
+ }
+
+
+ private async bool prompt_to_save_async (string title, string discard_label)
+ {
+ if (!book_needs_saving || (book.n_pages == 0))
+ return true;
+
+ var dialog = new Adw.AlertDialog (title,
+ _("If you don’t save, changes will be permanently lost."));
+
+ dialog.add_response ("discard", discard_label);
+ dialog.add_response ("cancel", _("_Cancel"));
+ dialog.add_response ("save", _("_Save"));
+
+ dialog.set_response_appearance ("discard", Adw.ResponseAppearance.DESTRUCTIVE);
+ dialog.set_response_appearance ("save", Adw.ResponseAppearance.SUGGESTED);
+
+ dialog.present (this);
+
+ string response = "cancel";
+ SourceFunc callback = prompt_to_save_async.callback;
+ dialog.response.connect((res) => {
+ response = res;
+ callback ();
+ });
+
+ yield;
+
+ switch (response)
+ {
+ case "save":
+ if (yield save_document_async ())
+ return true;
+ else
+ return false;
+ case "discard":
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public void clear_document ()
+ {
+ book.clear ();
+ book_needs_saving = false;
+ book_uri = null;
+ save_button.sensitive = false;
+ copy_to_clipboard_action.set_enabled (false);
+ paste_crop_action.set_enabled (false);
+ undo_actions.set_size (0);
+ redo_actions.set_size (0);
+ undo_action.set_enabled (false);
+ redo_action.set_enabled (false);
+ update_scan_status ();
+ stack.set_visible_child_name ("startup");
+ scan_count = 0;
+ update_info_label ();
+ }
+
+ private void new_document ()
+ {
+ prompt_to_save_async.begin (/* Text in dialog warning when a document is about to be lost */
+ _("Save current document?"),
+ /* Button in dialog to create new document and discard unsaved document */
+ _("_Discard Changes"), (obj, res) =>
+ {
+ if (!prompt_to_save_async.end(res))
+ return;
+
+ if (scanning)
+ stop_scan ();
+
+ clear_document ();
+ autosave_manager.cleanup ();
+ });
+ }
+
+ [GtkCallback]
+ private bool status_label_activate_link_cb (Gtk.Label label, string uri)
+ {
+ if (uri == "install-firmware")
+ {
+ var dialog = new DriversDialog (this, missing_driver);
+ dialog.open.begin (() => {});
+ return true;
+ }
+
+ return false;
+ }
+
+ private void scan_progress_changed_cb (double progress)
+ {
+ scan_progress = progress;
+ scan_progress_arc.queue_draw ();
+ }
+
+ [GtkCallback]
+ private void crop_toggle_cb (Adw.SplitButton btn)
+ {
+ if (updating_page_menu)
+ return;
+
+ var page = book_view.selected_page;
+ if (page == null)
+ {
+ warning ("Trying to set crop but no selected page");
+ return;
+ }
+
+ if (btn.has_css_class ("flat"))
+ {
+ // Avoid overwriting crop name if there is already different crop active
+ if (!page.has_crop)
+ {
+ set_crop ("custom");
+ }
+ }
+ else
+ {
+ set_crop (null);
+ }
+ update_page_menu ();
+ }
+
+ [GtkCallback]
+ private void redetect_button_clicked_cb (Gtk.Button button)
+ {
+ have_devices = false;
+ update_scan_status ();
+ redetect ();
+ }
+
+ [GtkCallback]
+ private void on_more_menu_toggled(GLib.Object sender, GLib.ParamSpec pspec) {
+ paste_image_action.set_enabled (clipboard_has_image ());
+ }
+
+ private void scan (ScanOptions options)
+ {
+ status_page.set_title (/* Label shown when scan started */
+ _("Contacting Scanner…"));
+ device_buttons_box.visible = true;
+ device_buttons_box.sensitive = false;
+ /* remove highlighting scan button */
+ scan_button.remove_css_class("suggested-action");
+ var scan_device = get_selected_device ();
+ /* store the selected device */
+ settings.set_string ("selected-device", scan_device);
+
+ /* toggle grid view while scanning */
+ if (!book_view.show_grid)
+ book_view.toggle_grid ();
+ book_view.set_toggle_grid_button_sensitive(false);
+
+ /* prevent operations during scanning */
+ processing_pages = true;
+ update_page_menu ();
+
+ start_scan (scan_device, options);
+ }
+
+ private void scan_type_action_cb (SimpleAction action, Variant? value)
+ {
+ var type = value.get_string ();
+
+ switch (type) {
+ case "single":
+ set_scan_type (ScanType.SINGLE);
+ break;
+ case "adf":
+ set_scan_type (ScanType.ADF);
+ break;
+ case "batch":
+ set_scan_type (ScanType.BATCH);
+ break;
+ default:
+ return;
+ }
+ }
+
+ private void paper_size_action_cb (SimpleAction action, Variant? value)
+ {
+ var size_id = value.get_string ();
+ if (size_id == null)
+ return;
+ if (size_id == "Custom")
+ {
+ run_custom_paper_size_flow.begin (action, value);
+ return;
+ }
+
+ set_paper_size (size_id);
+ action.set_state (value);
+ update_info_label ();
+ }
+
+ private void scan_side_action_cb (SimpleAction action, Variant? value)
+ {
+ var side_id = value.get_string ();
+
+ switch (side_id) {
+ case "front":
+ preferences_dialog.set_page_side (ScanSide.FRONT);
+ break;
+ case "back":
+ preferences_dialog.set_page_side (ScanSide.BACK);
+ break;
+ case "both":
+ preferences_dialog.set_page_side (ScanSide.BOTH);
+ break;
+ default:
+ return;
+ }
+
+ action.set_state (value);
+ update_info_label ();
+ }
+
+ private void load_scan_side_from_settings ()
+ {
+ var side = (ScanSide) settings.get_enum ("page-side");
+ string side_id;
+
+ switch (side) {
+ case ScanSide.FRONT:
+ side_id = "front";
+ break;
+ case ScanSide.BACK:
+ side_id = "back";
+ break;
+ default:
+ case ScanSide.BOTH:
+ side_id = "both";
+ break;
+ }
+
+ if (scan_side_action != null)
+ scan_side_action.set_state (side_id);
+ }
+
+ private void load_dpi_from_settings ()
+ {
+ var stored_dpi = settings.get_int ("dpi");
+ if (stored_dpi <= 0)
+ stored_dpi = DEFAULT_DPI;
+
+ dpi = stored_dpi;
+
+ if (dpi_action != null)
+ dpi_action.set_state (dpi.to_string ());
+ }
+
+ private async void run_custom_paper_size_flow (SimpleAction action, Variant value)
+ {
+ bool ok = yield edit_paper_size_async ();
+ if (!ok)
+ return;
+
+ var size_id = value.get_string ();
+ set_paper_size (size_id);
+ action.set_state (value);
+ update_info_label ();
+ }
+
+ private class PaperSize
+ {
+ public double width_mm;
+ public double height_mm;
+
+ public PaperSize(double w, double h) {
+ width_mm = w;
+ height_mm = h;
+ }
+ }
+
+
+ public class CustomPaperSizeDialog : Gtk.Dialog {
+ public Gtk.SpinButton paper_height_entry;
+ public Gtk.SpinButton paper_width_entry;
+
+ private Gtk.Entry paper_search_entry;
+ private Gtk.EntryCompletion completion;
+
+ public CustomPaperSizeDialog(Gtk.Window parent, double initial_height, double initial_width) {
+ Object(transient_for: parent, modal: true, title: (_("Custom Paper Size")));
+
+ paper_height_entry = new Gtk.SpinButton.with_range(1.0, 2000.0, 0.1);
+ paper_width_entry = new Gtk.SpinButton.with_range(1.0, 2000.0, 0.1);
+ paper_height_entry.set_value(initial_height / 10.0);
+ paper_width_entry.set_value(initial_width / 10.0);
+
+ paper_height_entry.set_digits(1);
+ paper_width_entry.set_digits(1);
+
+ var paper_sizes = new HashTable(str_hash, str_equal);
+ var paper_sizes_ordered = new GLib.List();
+
+ // --- ISO A series ---
+ paper_sizes_ordered.append("A0");
+ paper_sizes.insert("A0", new PaperSize(841, 1189));
+ paper_sizes_ordered.append("A1");
+ paper_sizes.insert("A1", new PaperSize(594, 841));
+ paper_sizes_ordered.append("A2");
+ paper_sizes.insert("A2", new PaperSize(420, 594));
+ paper_sizes_ordered.append("A3");
+ paper_sizes.insert("A3", new PaperSize(297, 420));
+ paper_sizes_ordered.append("A4");
+ paper_sizes.insert("A4", new PaperSize(210, 297));
+ paper_sizes_ordered.append("A5");
+ paper_sizes.insert("A5", new PaperSize(148, 210));
+ paper_sizes_ordered.append("A6");
+ paper_sizes.insert("A6", new PaperSize(105, 148));
+ paper_sizes_ordered.append("A7");
+ paper_sizes.insert("A7", new PaperSize(74, 105));
+ paper_sizes_ordered.append("A8");
+ paper_sizes.insert("A8", new PaperSize(52, 74));
+ paper_sizes_ordered.append("A9");
+ paper_sizes.insert("A9", new PaperSize(37, 52));
+ paper_sizes_ordered.append("A10");
+ paper_sizes.insert("A10", new PaperSize(26, 37));
+
+ // --- ISO B series ---
+ paper_sizes_ordered.append("B0");
+ paper_sizes.insert("B0", new PaperSize(1000, 1414));
+ paper_sizes_ordered.append("B1");
+ paper_sizes.insert("B1", new PaperSize(707, 1000));
+ paper_sizes_ordered.append("B2");
+ paper_sizes.insert("B2", new PaperSize(500, 707));
+ paper_sizes_ordered.append("B3");
+ paper_sizes.insert("B3", new PaperSize(353, 500));
+ paper_sizes_ordered.append("B4");
+ paper_sizes.insert("B4", new PaperSize(250, 353));
+ paper_sizes_ordered.append("B5");
+ paper_sizes.insert("B5", new PaperSize(176, 250));
+ paper_sizes_ordered.append("B6");
+ paper_sizes.insert("B6", new PaperSize(125, 176));
+ paper_sizes_ordered.append("B7");
+ paper_sizes.insert("B7", new PaperSize(88, 125));
+ paper_sizes_ordered.append("B8");
+ paper_sizes.insert("B8", new PaperSize(62, 88));
+ paper_sizes_ordered.append("B9");
+ paper_sizes.insert("B9", new PaperSize(44, 62));
+ paper_sizes_ordered.append("B10");
+ paper_sizes.insert("B10", new PaperSize(31, 44));
+
+ // --- ISO C series ---
+ paper_sizes_ordered.append("C0");
+ paper_sizes.insert("C0", new PaperSize(917, 1297));
+ paper_sizes_ordered.append("C1");
+ paper_sizes.insert("C1", new PaperSize(648, 917));
+ paper_sizes_ordered.append("C2");
+ paper_sizes.insert("C2", new PaperSize(458, 648));
+ paper_sizes_ordered.append("C3");
+ paper_sizes.insert("C3", new PaperSize(324, 458));
+ paper_sizes_ordered.append("C4");
+ paper_sizes.insert("C4", new PaperSize(229, 324));
+ paper_sizes_ordered.append("C5");
+ paper_sizes.insert("C5", new PaperSize(162, 229));
+ paper_sizes_ordered.append("C6");
+ paper_sizes.insert("C6", new PaperSize(114, 162));
+ paper_sizes_ordered.append("C7");
+ paper_sizes.insert("C7", new PaperSize(81, 114));
+ paper_sizes_ordered.append("C8");
+ paper_sizes.insert("C8", new PaperSize(57, 81));
+ paper_sizes_ordered.append("C9");
+ paper_sizes.insert("C9", new PaperSize(40, 57));
+ paper_sizes_ordered.append("C10");
+ paper_sizes.insert("C10", new PaperSize(28, 40));
+
+ // --- North American sizes ---
+ paper_sizes_ordered.append("Letter");
+ paper_sizes.insert("Letter", new PaperSize(216, 279));
+ paper_sizes_ordered.append("Legal");
+ paper_sizes.insert("Legal", new PaperSize(216, 356));
+ paper_sizes_ordered.append("Junior Legal");
+ paper_sizes.insert("Junior Legal", new PaperSize(127, 203));
+ paper_sizes_ordered.append("Ledger");
+ paper_sizes.insert("Ledger", new PaperSize(432, 279));
+ paper_sizes_ordered.append("Tabloid");
+ paper_sizes.insert("Tabloid", new PaperSize(279, 432));
+ paper_sizes_ordered.append("Executive");
+ paper_sizes.insert("Executive", new PaperSize(184, 267));
+
+ // --- Miscellaneous common sizes ---
+ paper_sizes_ordered.append("Folio");
+ paper_sizes.insert("Folio", new PaperSize(210, 330));
+ paper_sizes_ordered.append("Statement");
+ paper_sizes.insert("Statement", new PaperSize(140, 216));
+ paper_sizes_ordered.append("Half Letter");
+ paper_sizes.insert("Half Letter", new PaperSize(140, 216));
+ paper_sizes_ordered.append("Government Letter");
+ paper_sizes.insert("Government Letter", new PaperSize(203, 267));
+ paper_sizes_ordered.append("Government Legal");
+ paper_sizes.insert("Government Legal", new PaperSize(216, 330));
+
+ // --- Japanese JIS B series ---
+ paper_sizes_ordered.append("JIS B0");
+ paper_sizes.insert("JIS B0", new PaperSize(1030, 1456));
+ paper_sizes_ordered.append("JIS B1");
+ paper_sizes.insert("JIS B1", new PaperSize(728, 1030));
+ paper_sizes_ordered.append("JIS B2");
+ paper_sizes.insert("JIS B2", new PaperSize(515, 728));
+ paper_sizes_ordered.append("JIS B3");
+ paper_sizes.insert("JIS B3", new PaperSize(364, 515));
+ paper_sizes_ordered.append("JIS B4");
+ paper_sizes.insert("JIS B4", new PaperSize(257, 364));
+ paper_sizes_ordered.append("JIS B5");
+ paper_sizes.insert("JIS B5", new PaperSize(182, 257));
+ paper_sizes_ordered.append("JIS B6");
+ paper_sizes.insert("JIS B6", new PaperSize(128, 182));
+ paper_sizes_ordered.append("JIS B7");
+ paper_sizes.insert("JIS B7", new PaperSize(91, 128));
+ paper_sizes_ordered.append("JIS B8");
+ paper_sizes.insert("JIS B8", new PaperSize(64, 91));
+ paper_sizes_ordered.append("JIS B9");
+ paper_sizes.insert("JIS B9", new PaperSize(45, 64));
+ paper_sizes_ordered.append("JIS B10");
+ paper_sizes.insert("JIS B10", new PaperSize(32, 45));
+
+ // --- Additional international sizes ---
+ paper_sizes_ordered.append("RA0");
+ paper_sizes.insert("RA0", new PaperSize(860, 1220));
+ paper_sizes_ordered.append("RA1");
+ paper_sizes.insert("RA1", new PaperSize(610, 860));
+ paper_sizes_ordered.append("RA2");
+ paper_sizes.insert("RA2", new PaperSize(430, 610));
+ paper_sizes_ordered.append("RA3");
+ paper_sizes.insert("RA3", new PaperSize(305, 430));
+ paper_sizes_ordered.append("RA4");
+ paper_sizes.insert("RA4", new PaperSize(215, 305));
+
+ paper_sizes_ordered.append("SRA0");
+ paper_sizes.insert("SRA0", new PaperSize(900, 1280));
+ paper_sizes_ordered.append("SRA1");
+ paper_sizes.insert("SRA1", new PaperSize(640, 900));
+ paper_sizes_ordered.append("SRA2");
+ paper_sizes.insert("SRA2", new PaperSize(450, 640));
+ paper_sizes_ordered.append("SRA3");
+ paper_sizes.insert("SRA3", new PaperSize(320, 450));
+ paper_sizes_ordered.append("SRA4");
+ paper_sizes.insert("SRA4", new PaperSize(225, 320));
+
+ paper_search_entry = new Gtk.Entry();
+ paper_search_entry.placeholder_text = _("Search Paper Size");
+
+ var store = new Gtk.ListStore(1, typeof(string));
+
+ foreach (string name in paper_sizes_ordered) {
+ Gtk.TreeIter iter;
+ store.append(out iter);
+ store.set(iter, 0, name);
+ }
+
+ completion = new Gtk.EntryCompletion();
+ completion.set_model(store);
+ completion.set_text_column(0);
+ completion.inline_completion = true;
+ completion.popup_completion = true;
+
+ paper_search_entry.set_completion(completion);
+
+ completion.match_selected.connect((model, iter) => {
+ string name;
+ model.get(iter, 0, out name);
+
+ PaperSize size = paper_sizes.lookup(name);
+
+ paper_width_entry.set_value(size.width_mm);
+ paper_height_entry.set_value(size.height_mm);
+
+ return false;
+ });
+
+ var grid = new Gtk.Grid();
+ grid.column_spacing = 12;
+ grid.row_spacing = 12;
+ grid.margin_top = 12;
+ grid.margin_bottom = 12;
+ grid.margin_start = 12;
+ grid.margin_end = 12;
+
+ grid.attach(new Gtk.Label(_("Search Common Sizes")), 0, 0, 1, 1);
+ grid.attach(paper_search_entry, 1, 0, 1, 1);
+
+ grid.attach(new Gtk.Label(_("Paper Height (mm)")), 0, 1, 1, 1);
+ grid.attach(paper_height_entry, 1, 1, 1, 1);
+
+ grid.attach(new Gtk.Label(_("Paper Width (mm)")), 0, 2, 1, 1);
+ grid.attach(paper_width_entry, 1, 2, 1, 1);
+
+ get_content_area().append(grid);
+
+ add_button(_("Cancel"), Gtk.ResponseType.CANCEL);
+ add_button(_("OK"), Gtk.ResponseType.OK);
+ }
+ }
+
+ private async bool edit_paper_size_async() {
+ var dialog = new CustomPaperSizeDialog(this, settings.get_int ("custom-paper-height"), settings.get_int ("custom-paper-width"));
+ bool is_ok = false;
+
+ var source = new GLib.IdleSource();
+ source.set_priority(GLib.Priority.DEFAULT);
+ source.set_callback(edit_paper_size_async.callback);
+
+ dialog.response.connect((response_id) => {
+ if (response_id == Gtk.ResponseType.OK) {
+ settings.set_int ("custom-paper-height", (int)(dialog.paper_height_entry.get_value() * 10.0));
+ settings.set_int ("custom-paper-width", (int)(dialog.paper_width_entry.get_value() * 10.0));
+ is_ok = true;
+ }
+
+ dialog.destroy();
+
+ source.attach(GLib.MainContext.default());
+ });
+
+ dialog.present();
+
+ yield;
+
+ return is_ok;
+ }
+
+ private void set_paper_size (string size_id)
+ {
+ switch (size_id) {
+ case "Automatic":
+ settings.set_int ("paper-width", 0);
+ settings.set_int ("paper-height", 0);
+ settings.set_string("page-size", size_id);
+ break;
+ case "Custom":
+ settings.set_int ("paper-width", settings.get_int ("custom-paper-width"));
+ settings.set_int ("paper-height", settings.get_int ("custom-paper-height"));
+ settings.set_string("page-size", size_id);
+ break;
+ case "A6":
+ settings.set_int ("paper-width", 1050);
+ settings.set_int ("paper-height", 1480);
+ settings.set_string("page-size", size_id);
+ break;
+ case "A5":
+ settings.set_int ("paper-width", 1480);
+ settings.set_int ("paper-height", 2100);
+ settings.set_string("page-size", size_id);
+ break;
+ case "A4":
+ settings.set_int ("paper-width", 2100);
+ settings.set_int ("paper-height", 2970);
+ settings.set_string("page-size", size_id);
+ break;
+ case "A3":
+ settings.set_int ("paper-width", 2970);
+ settings.set_int ("paper-height", 4200);
+ settings.set_string("page-size", size_id);
+ break;
+ case "letter":
+ settings.set_int ("paper-width", 2159);
+ settings.set_int ("paper-height", 2794);
+ settings.set_string("page-size", size_id);
+ break;
+ case "legal":
+ settings.set_int ("paper-width", 2159);
+ settings.set_int ("paper-height", 3556);
+ settings.set_string("page-size", size_id);
+ break;
+ case "4x6":
+ settings.set_int ("paper-width", 1016);
+ settings.set_int ("paper-height", 1524);
+ settings.set_string("page-size", size_id);
+ break;
+ default:
+ return;
+ }
+ }
+
+ private void load_paper_size_from_settings ()
+ {
+ var width = settings.get_int ("paper-width");
+ var height = settings.get_int ("paper-height");
+ string size_id = "Automatic";
+
+ if (width == 1050 && height == 1480) {
+ size_id = "A6";
+ } else if (width == 1480 && height == 2100) {
+ size_id = "A5";
+ } else if (width == 2100 && height == 2970) {
+ size_id = "A4";
+ } else if (width == 2970 && height == 4200) {
+ size_id = "A3";
+ } else if (width == 2159 && height == 2794) {
+ size_id = "letter";
+ } else if (width == 2159 && height == 3556) {
+ size_id = "legal";
+ } else if (width == 1016 && height == 1524) {
+ size_id = "4x6";
+ } else if (width > 0 && height > 0) {
+ size_id = "Custom";
+ } else {
+ size_id = "Automatic";
+ }
+
+ if (paper_size_action != null)
+ paper_size_action.set_state (size_id);
+ settings.set_string("page-size", size_id);
+ }
+
+ /* Adjust the resolution and paper size menus to reflect the capabilities read
+ from the currently selected scanner. */
+ public void set_device_capabilities (string device, ScanDeviceCapabilities capabilities)
+ {
+ rebuild_dpi_menu (capabilities.resolutions);
+ rebuild_paper_size_menu (capabilities.max_width);
+ }
+
+ private void rebuild_dpi_menu (int[] resolutions)
+ {
+ /* Fall back to the static menu if the device didn't report any resolutions */
+ if (resolutions.length == 0)
+ return;
+
+ var section = new GLib.Menu ();
+ foreach (var res in resolutions)
+ {
+ string dpi = _(" dpi");
+ string item_label = "%d %s".printf (res, dpi);
+ /* adding keyboard shortcuts (underline) and comments
+ to default resolutions */
+ if (item_label == "75 %s".printf (dpi))
+ item_label = _("_75 dpi (draft)");
+ else if (item_label == "150 %s".printf (dpi))
+ item_label = _("1_50 dpi");
+ else if (item_label == "200 %s".printf (dpi))
+ item_label = _("_200 dpi");
+ else if (item_label == "300 %s".printf (dpi))
+ item_label = _("_300 dpi (default)");
+ else if (item_label == "600 %s".printf (dpi))
+ item_label = _("_600 dpi");
+ else if (item_label == "1200 %s".printf (dpi))
+ item_label = _("_1200 dpi (high resolution)");
+ else if (item_label == "2400 %s".printf (dpi))
+ item_label = _("_2400 dpi");
+ var item = new GLib.MenuItem (item_label, null);
+ item.set_action_and_target_value ("app.dpi", new Variant.string (res.to_string ()));
+ section.append_item (item);
+ }
+
+ var menu = new GLib.Menu ();
+ menu.append_section (null, section);
+ dpi_menu.set_menu_model (menu);
+
+ /* If the current resolution isn't supported, snap to the nearest one */
+ var supported = false;
+ foreach (var res in resolutions)
+ {
+ if (res == dpi)
+ {
+ supported = true;
+ break;
+ }
+ }
+
+ if (!supported)
+ {
+ var nearest = resolutions[0];
+ var best_distance = (resolutions[0] - dpi).abs ();
+ foreach (var res in resolutions)
+ {
+ var distance = (res - dpi).abs ();
+ if (distance < best_distance)
+ {
+ best_distance = distance;
+ nearest = res;
+ }
+ }
+
+ set_dpi (nearest, true);
+ if (dpi_action != null)
+ dpi_action.set_state (nearest.to_string ());
+ }
+ }
+
+ private void rebuild_paper_size_menu (int max_width)
+ {
+ var section = new GLib.Menu ();
+
+ /* Automatic and Custom are always available */
+ append_paper_size_item (section, _("A_utomatic"), "Automatic");
+ append_paper_size_item (section, _("_Custom"), "Custom");
+
+ /* Standard sizes with their width in tenths of a millimetre */
+ string[] standard_labels = { _("A_6"), _("A_5"), _("_A4"), _("A_3"), _("_Letter"), _("Le_gal"), _("_4×6") };
+ string[] standard_targets = { "A6", "A5", "A4", "A3", "letter", "legal", "4x6" };
+ int[] standard_widths = { 1050, 1480, 2100, 2970, 2159, 2159, 1016 };
+
+ for (var i = 0; i < standard_targets.length; i++)
+ {
+ /* Only show sizes that are not wider than the scanner supports */
+ if (max_width <= 0 || standard_widths[i] <= max_width)
+ append_paper_size_item (section, standard_labels[i], standard_targets[i]);
+ }
+
+ var menu = new GLib.Menu ();
+ menu.append_section (null, section);
+ paper_size_menu.set_menu_model (menu);
+
+ /* If the currently selected paper size is now too wide, reset to Automatic */
+ if (max_width > 0 && settings.get_int ("paper-width") > max_width)
+ {
+ set_paper_size ("Automatic");
+ if (paper_size_action != null)
+ paper_size_action.set_state ("Automatic");
+ update_info_label ();
+ }
+ }
+
+ private void append_paper_size_item (GLib.Menu section, string label, string target)
+ {
+ var item = new GLib.MenuItem (label, null);
+ item.set_action_and_target_value ("app.paper_size", new Variant.string (target));
+ section.append_item (item);
+ }
+
+ private void dpi_action_cb (SimpleAction action, Variant? value)
+ {
+ var dpi_id = value.get_string ();
+ if (dpi_id == null)
+ return;
+ action.set_state (value);
+ set_dpi (int.parse (dpi_id), true);
+ }
+
+ private void scan_single_cb ()
+ {
+ var options = make_scan_options ();
+ options.type = ScanType.SINGLE;
+ scan (options);
+ }
+
+ private void scan_adf_cb ()
+ {
+ var options = make_scan_options ();
+ options.type = ScanType.ADF;
+ scan (options);
+ }
+
+ private void scan_batch_cb ()
+ {
+ var options = make_scan_options ();
+ options.type = ScanType.BATCH;
+ scan (options);
+ }
+
+ private void scan_start_cb ()
+ {
+ if (scanning_ || get_selected_device () == null)
+ return;
+ else
+ {
+ var options = make_scan_options ();
+ options.type = this.scan_type;
+ scan (options);
+ }
+ }
+
+ private void scan_stop_cb ()
+ {
+ stop_scan ();
+ }
+
+ private void rotate_left_cb ()
+ {
+ if (updating_page_menu)
+ return;
+ var timestamp = GLib.get_monotonic_time ();
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null && page.is_selected)
+ rotate_page_left_with_undo (page, i, timestamp);
+ }
+ }
+
+ private void rotate_right_cb ()
+ {
+ if (updating_page_menu)
+ return;
+ var timestamp = GLib.get_monotonic_time ();
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null && page.is_selected)
+ rotate_page_right_with_undo (page, i, timestamp);
+ }
+ }
+
+ private void flip_page_cb ()
+ {
+ if (updating_page_menu)
+ return;
+ var timestamp = GLib.get_monotonic_time ();
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null && page.is_selected)
+ {
+ rotate_page_right_with_undo (page, i, timestamp);
+ rotate_page_right_with_undo (page, i, timestamp);
+ }
+ }
+ }
+
+ private delegate void PageOperationDelegate (Page page);
+
+ private class PageOperationTask
+ {
+ public Page page;
+ public unowned PageOperationDelegate operation;
+ public SimpleProgressBar progress_bar;
+ public int selected_found;
+ public int n_selected_pages;
+
+ public PageOperationTask (Page page, PageOperationDelegate operation, SimpleProgressBar progress_bar, int selected_found, int n_selected_pages)
+ {
+ this.page = page;
+ this.operation = operation;
+ this.progress_bar = progress_bar;
+ this.selected_found = selected_found;
+ this.n_selected_pages = n_selected_pages;
+ }
+ }
+
+ private class CropOperationTask
+ {
+ public Page page;
+ public int page_index;
+ public AppWindow app_window;
+ public int64 timestamp;
+
+ public CropOperationTask (Page page, int page_index, AppWindow app_window, int64 timestamp)
+ {
+ this.page = page;
+ this.page_index = page_index;
+ this.app_window = app_window;
+ this.timestamp = timestamp;
+ }
+ }
+
+ private class EmptyPageDetectionTask
+ {
+ public Page page;
+ public int page_index;
+ public AppWindow app_window;
+ public int64 timestamp;
+ public int threshold;
+ public SimpleProgressBar progress_bar;
+ public int selected_found;
+ public int n_selected_pages;
+
+ public EmptyPageDetectionTask (Page page, int page_index, AppWindow app_window, int64 timestamp, int threshold, SimpleProgressBar progress_bar, int selected_found, int n_selected_pages)
+ {
+ this.page = page;
+ this.page_index = page_index;
+ this.app_window = app_window;
+ this.timestamp = timestamp;
+ this.threshold = threshold;
+ this.progress_bar = progress_bar;
+ this.selected_found = selected_found;
+ this.n_selected_pages = n_selected_pages;
+ }
+ }
+
+ private void page_operation_worker (PageOperationTask? task)
+ {
+ task.operation (task.page);
+ Idle.add (() => {
+ task.progress_bar.add_fraction (1.0 / task.n_selected_pages);
+ if (task.selected_found == task.n_selected_pages) {
+ task.progress_bar.remove_with_delay (500, progress_box);
+ processing_pages = false;
+ update_page_menu ();
+ }
+ return false;
+ });
+ }
+
+ private void empty_page_detection_worker (EmptyPageDetectionTask? task)
+ {
+ bool is_empty = task.page.is_empty_page (task.threshold);
+
+ Idle.add (() => {
+ if (is_empty && !task.page.is_deleted) {
+ task.app_window.book.delete_page (task.page);
+ task.app_window.add_undo_action (UndoAction.ActionType.DELETE, task.page_index, 0, {}, null, task.timestamp);
+ }
+ task.progress_bar.add_fraction (1.0 / task.n_selected_pages);
+ if (task.selected_found == task.n_selected_pages) {
+ task.progress_bar.remove_with_delay (500, progress_box);
+ processing_pages = false;
+ update_page_menu ();
+ }
+ return false;
+ });
+ }
+
+ private void crop_operation_worker (CropOperationTask? task)
+ {
+ var border = task.page.get_border();
+
+ if (border[0] >= 0) {
+ /* add 1 / 3 cm as a border - usually more convenient */
+ int extra_border_size = (int)(task.page.dpi / 2.54 / 3 + 0.5);
+ var new_x_0 = border[0] - extra_border_size > 0 ? border[0] - extra_border_size : 0;
+ var new_x_1 = border[2] + extra_border_size < task.page.width ? border[2] + extra_border_size : task.page.width;
+ var new_y_0 = border[1] - extra_border_size > 0 ? border[1] - extra_border_size : 0;
+ var new_y_1 = border[3] + extra_border_size < task.page.height ? border[3] + extra_border_size : task.page.height;
+
+ var crop_width = new_x_1 - new_x_0;
+ var crop_height = new_y_1 - new_y_0;
+ task.page.set_custom_crop (crop_width, crop_height);
+ task.page.move_crop (new_x_0, new_y_0);
+ }
+ else {
+ var width = task.page.width;
+ var height = task.page.height;
+ var crop_width = (int) (width * 0.8 + 0.5);
+ var crop_height = (int) (height * 0.8 + 0.5);
+ task.page.set_custom_crop (crop_width, crop_height);
+ task.page.move_crop ((width - crop_width) / 2, (height - crop_height) / 2);
+ }
+ Idle.add (() => {
+ task.app_window.add_undo_action (UndoAction.ActionType.CROP, task.page_index, 1, new int[] {task.page.crop_x, task.page.crop_y, task.page.crop_width, task.page.crop_height}, task.page.crop_name, task.timestamp);
+ if (task.page.has_crop)
+ task.app_window.crop_split_button.add_css_class ("raised");
+ /* make sure the crop_actions get the current crop */
+ crop_actions.update_current_crop (book_view.selected_page.crop_name);
+ return false;
+ });
+ }
+
+ private void apply_page_operation_with_progress (string progress_label,
+ PageOperationDelegate operation
+ )
+ {
+ if (updating_page_menu)
+ return;
+ processing_pages = true;
+ update_page_menu ();
+
+ var n_selected_pages = book_view.get_n_selected_pages ();
+ if (n_selected_pages == 0)
+ return;
+ /* do not perform if another process is still running */
+ if (progress_box.get_first_child () != null)
+ return;
+
+ var progress_bar = new SimpleProgressBar (progress_label);
+ progress_box.append (progress_bar);
+ progress_bar.visible = true;
+ int selected_found = 0;
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null && page.is_selected)
+ {
+ selected_found ++;
+ /* Run in background thread */
+ try
+ {
+ page_operation_pool.add (
+ new PageOperationTask (
+ page,
+ operation,
+ progress_bar,
+ selected_found,
+ n_selected_pages
+ )
+ );
+ }
+ catch (Error e)
+ {
+ warning ("Failed to start page operation: %s", e.message);
+ processing_pages = false;
+ update_page_menu ();
+ progress_bar.remove_with_delay (0, progress_box);
+ show_error_dialog (/* Title of error dialog when page operation failed */
+ _("Failed to process pages"),
+ e.message);
+ return;
+ }
+
+ }
+ }
+
+ processing_pages = false;
+ update_page_menu ();
+
+ /* Tell the user how to undo that */
+ if (!settings.get_boolean ("filter-undo-toast-shown"))
+ show_filter_undo_toast ();
+
+ }
+
+ private void deskew_text_page_cb ()
+ {
+ apply_page_operation_with_progress (_("Deskewing"), (page) => {
+ page.deskew_text_page ();
+ });
+ }
+
+ private void detect_empty_pages_cb ()
+ {
+ if (updating_page_menu)
+ return;
+ processing_pages = true;
+ update_page_menu ();
+
+ if (progress_box.get_first_child () != null)
+ return;
+
+ int n_selected_pages = book_view.get_n_selected_pages ();
+ if (n_selected_pages == 0)
+ {
+ processing_pages = false;
+ update_page_menu ();
+ return;
+ }
+
+ var progress_bar = new SimpleProgressBar (_("Detecting Empty Pages"));
+ progress_box.append (progress_bar);
+ progress_bar.visible = true;
+
+ int selected_found = 0;
+ var timestamp = GLib.get_monotonic_time ();
+ int threshold = settings.get_int ("empty-page-threshold");
+
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null && page.is_selected)
+ {
+ selected_found++;
+ try
+ {
+ empty_page_detection_pool.add (
+ new EmptyPageDetectionTask (
+ page,
+ i,
+ this,
+ timestamp,
+ threshold,
+ progress_bar,
+ selected_found,
+ n_selected_pages
+ )
+ );
+ }
+ catch (Error e)
+ {
+ warning ("Failed to start empty page detection: %s", e.message);
+ processing_pages = false;
+ update_page_menu ();
+ progress_bar.remove_with_delay (0, progress_box);
+ show_error_dialog (_("Failed to process pages"), e.message);
+ return;
+ }
+ }
+ }
+
+ if (selected_found == 0)
+ {
+ progress_bar.remove_with_delay (0, progress_box);
+ processing_pages = false;
+ update_page_menu ();
+ }
+ }
+
+ private void convert_to_grayscale_cb ()
+ {
+ apply_page_operation_with_progress (_("Converting to Grayscale"), (page) => {
+ page.convert_to_grayscale ();
+ });
+ }
+
+ private void convert_to_2bit_grayscale_cb ()
+ {
+ apply_page_operation_with_progress (_("Converting to 2-Bit Grayscale"), (page) => {
+ page.convert_to_2bit_grayscale ();
+ });
+ }
+
+ private void convert_to_line_art_cb ()
+ {
+ apply_page_operation_with_progress (_("Converting to Line Art"), (page) => {
+ page.convert_to_line_art ();
+ });
+ }
+
+ private void automatic_color_filter_cb ()
+ {
+ apply_page_operation_with_progress (_("Applying Automatic Color Filter"), (page) => {
+ page.automatic_color_filter ();
+ });
+ }
+
+ private void restore_image_cb ()
+ {
+ apply_page_operation_with_progress (_("Restoring Original Images"), (page) => {
+ page.restore_image ();
+ });
+ }
+
+ private void move_left_cb ()
+ {
+ var timestamp = GLib.get_monotonic_time ();
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if ((page != null) && page.is_selected)
+ {
+ /* not if first page! */
+ if (i == 0)
+ break;
+ move_page_with_undo (page, (uint) (i - 1), timestamp);
+ }
+ }
+ }
+
+ private void move_right_cb ()
+ {
+ var timestamp = GLib.get_monotonic_time ();
+ for (int i = (int) book.n_pages - 1; i >= 0; i--)
+ {
+ var page = book.get_page (i);
+ if ((page != null) && page.is_selected)
+ {
+ /* not if last page! */
+ if (i == book.n_pages - 1)
+ break;
+ move_page_with_undo (page, (uint) (i + 1), timestamp);
+ }
+ }
+ }
+
+ private void crop_page_cb ()
+ {
+ var page = book_view.selected_page;
+ var page_index = (int)book.get_page_index (page);
+ if (page != null)
+ {
+ if (page.has_crop)
+ {
+ add_undo_action (UndoAction.ActionType.CROP, page_index, 0, new int[] {page.crop_x, page.crop_y, page.crop_width, page.crop_height}, page.crop_name);
+ set_crop (null);
+ }
+ else
+ {
+ set_crop ("custom");
+ add_undo_action (UndoAction.ActionType.CROP, page_index, 1, new int[] {page.crop_x, page.crop_y, page.crop_width, page.crop_height}, page.crop_name);
+ }
+ }
+ }
+
+ private void copy_page_cb ()
+ {
+ var page = book_view.selected_page;
+ if (page != null)
+ {
+ page_copy_crop_name = page.crop_name;
+ page_copy_has_crop = page.has_crop;
+ page_copy_crop_x = page.crop_x;
+ page_copy_crop_y = page.crop_y;
+ page_copy_crop_width = page.crop_width;
+ page_copy_crop_height = page.crop_height;
+ page_copy_crop_dpi = page.dpi;
+ if (page_copy_has_crop)
+ paste_crop_action.set_enabled (true);
+ else
+ paste_crop_action.set_enabled (false);
+ page.copy_to_clipboard (this);
+ }
+ }
+
+ private void paste_crop_cb ()
+ {
+ /* no crop copied - return */
+ if (!page_copy_has_crop)
+ {
+ return;
+ }
+ var timestamp = GLib.get_monotonic_time ();
+
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if (book_page != null && book_page.is_selected)
+ {
+ /* scale the cropping area if a different dpi is used
+ don't do it if the cropping area is outside the page
+ */
+ double scaling_factor = (double)book_page.dpi / (double)page_copy_crop_dpi;
+
+ if (((int)((page_copy_crop_width + page_copy_crop_x) * scaling_factor + 0.5) > book_page.width)
+ || ((int)((page_copy_crop_height + page_copy_crop_y) * scaling_factor + 0.5) > book_page.height))
+ {
+ scaling_factor = 1;
+ /* check if still too large */
+ if (((int)((page_copy_crop_width + page_copy_crop_x) + 0.5) > book_page.width)
+ || ((int)((page_copy_crop_height + page_copy_crop_y) + 0.5) > book_page.height))
+ {
+ continue;
+ }
+ }
+
+ book_page.set_custom_crop ((int)(page_copy_crop_width * scaling_factor + 0.5), (int)(page_copy_crop_height * scaling_factor + 0.5));
+ book_page.move_crop ((int)(page_copy_crop_x * scaling_factor + 0.5), (int)(page_copy_crop_y * scaling_factor + 0.5));
+ book_page.set_only_crop_name(page_copy_crop_name);
+ add_undo_action (UndoAction.ActionType.CROP, i, 1, {book_page.crop_x, book_page.crop_y, book_page.crop_width, book_page.crop_height}, book_page.crop_name, timestamp);
+ }
+ }
+ }
+
+ private void paste_image_cb ()
+ {
+ paste_image_from_clipboard.begin ();
+ }
+
+ /* Paste an image from the clipboard as a new page.
+ Assumes the image has the currently selected dpi,
+ which allows for copying a page, paste it into another
+ application, copy it there and paste it back to
+ Document Scanner. */
+ public async void paste_image_from_clipboard ()
+ {
+ /* only paste if the start page is not displayed */
+ if (stack.get_visible_child_name () == "startup")
+ return;
+
+ var clipboard = this.get_clipboard ();
+
+ Gdk.Texture? texture = null;
+ try
+ {
+ texture = yield clipboard.read_texture_async (null);
+ }
+ catch (Error e)
+ {
+ warning ("Failed to read image from clipboard: %s", e.message);
+ return;
+ }
+
+ if (texture == null)
+ return;
+
+ int w = texture.get_width ();
+ int h = texture.get_height ();
+ if (w <= 0 || h <= 0)
+ return;
+
+ var downloader = new Gdk.TextureDownloader(texture);
+ downloader.set_format(Gdk.MemoryFormat.R8G8B8);
+ size_t dst_rowstride;
+ GLib.Bytes bytes = downloader.download_bytes(out dst_rowstride);
+ unowned uchar[] pixels = (uchar[]) bytes.get_data();
+
+ var current_dpi = dpi;
+
+ var page = new Page.from_data (
+ w, /* scan_width */
+ h, /* scan_height */
+ "Automatic", /* page_size */
+ (int)dst_rowstride, /* rowstride */
+ 3, /* n_channels */
+ 8, /* depth */
+ current_dpi, /* dpi */
+ ScanDirection.TOP_TO_BOTTOM,
+ null, /* color_profile */
+ pixels,
+ "", /* raw_backup_path */
+ "", /* fullres_pixels_path */
+ 0, 0, 0, /* pixels_width / height / rowstride */
+ false, /* has_auto_rotate */
+ false, null, 0, 0, 0, 0, /* crop fields */
+ false /* is_deleted */
+ );
+
+ book.append_page (page);
+ page.save_fullres_pixels ();
+ page.scan_finished ();
+
+ save_button.sensitive = true;
+ book_needs_saving = true;
+ copy_to_clipboard_action.set_enabled (true);
+ update_info_label ();
+ }
+
+ private void toggle_deleted_display_cb ()
+ {
+ book_view.toggle_deleted_display ();
+ hide_deleted_button.set_icon_name (book_view.show_deleted_pages ? "view-conceal-symbolic" : "view-reveal-symbolic");
+ if (book_view.show_deleted_pages)
+ hide_deleted_button.tooltip_text = _("Hide Deleted Pages");
+ else
+ hide_deleted_button.tooltip_text = _("Show Deleted Pages");
+ }
+
+ private void delete_page_cb ()
+ {
+ /* Decide if most selected pages are marked deleted,
+ If yes, restore all pages, else delete all! */
+ var marked_deleted_pages = 0;
+ var marked_nondeleted_pages = 0;
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if ((page != null) && page.is_selected)
+ if (page.is_deleted)
+ marked_deleted_pages++;
+ else
+ marked_nondeleted_pages++;
+ }
+ bool most_deleted = marked_deleted_pages > marked_nondeleted_pages;
+
+ var timestamp = GLib.get_monotonic_time ();
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if ((page != null) && page.is_selected && page.is_deleted == most_deleted)
+ {
+ book.delete_page (page);
+ add_undo_action (UndoAction.ActionType.DELETE, i, 0, {}, null, timestamp);
+ }
+
+ }
+ update_page_menu ();
+ }
+
+ private void select_all_cb ()
+ {
+ book_view.select_first_page ();
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null)
+ page.is_selected = true;
+ }
+ }
+
+ private void select_even_pages_cb ()
+ {
+ /* return if there are no even pages */
+ if (book.n_pages < 2)
+ return;
+
+ book_view.select_nth_page (1);
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null)
+ page.is_selected = (i % 2 == 1); // 0-indexed, so odd indices are even page numbers
+ }
+ }
+
+ private void select_odd_pages_cb ()
+ {
+ /* return if there are no odd pages */
+ if (book.n_pages < 1)
+ return;
+
+ book_view.select_first_page ();
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null)
+ page.is_selected = (i % 2 == 0); // 0-indexed, so even indices are odd page numbers
+ }
+ }
+
+ private void reverse_order_cb ()
+ {
+ int first_selected = -1;
+ var timestamp = GLib.get_monotonic_time ();
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null && page.is_selected) {
+ if (first_selected < 0)
+ {
+ first_selected = i;
+ }
+ else
+ {
+ move_page_with_undo (page, first_selected, timestamp);
+ }
+ }
+ }
+ }
+
+ /* set crop, copy page and split crop */
+ private void split_page_cb ()
+ {
+ if (updating_page_menu)
+ return;
+ processing_pages = true;
+ update_page_menu ();
+
+ var timestamp = GLib.get_monotonic_time ();
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null && page.is_selected)
+ {
+ add_undo_action (UndoAction.ActionType.CROP, i, 0, new int[] {page.crop_x, page.crop_y, page.crop_width, page.crop_height}, page.crop_name, timestamp);
+ var new_page = page.split_page ();
+ add_undo_action (UndoAction.ActionType.CROP, i, 1, new int[] {page.crop_x, page.crop_y, page.crop_width, page.crop_height}, page.crop_name, timestamp);
+ book.append_page (new_page);
+ new_page.scan_finished ();
+ book.move_page (new_page, (uint)(i + 1));
+ add_undo_action (UndoAction.ActionType.SPLIT_PAGE, i + 1, 0, {}, null, timestamp);
+ add_undo_action (UndoAction.ActionType.CROP, i + 1, 1, new int[] {page.crop_x, page.crop_y, page.crop_width, page.crop_height}, page.crop_name, timestamp);
+ }
+ }
+
+ processing_pages = false;
+ update_page_menu ();
+ update_info_label ();
+ }
+
+ public ScanType get_scan_type ()
+ {
+ return this.scan_type;
+ }
+
+ private void set_scan_type (ScanType scan_type)
+ {
+ this.scan_type = scan_type;
+
+ switch (scan_type)
+ {
+ case ScanType.SINGLE:
+ scan_type_action.set_state ("single");
+ break;
+ case ScanType.ADF:
+ scan_type_action.set_state ("adf");
+ break;
+ case ScanType.BATCH:
+ scan_type_action.set_state ("batch");
+ break;
+ }
+
+ update_scan_button_icon ();
+ }
+
+ private void update_scan_button_icon ()
+ {
+ if (scanning_)
+ return;
+
+ string icon_name;
+ string tooltip_text;
+
+ switch (scan_type)
+ {
+ case ScanType.SINGLE:
+ icon_name = "scanner-symbolic";
+ tooltip_text = _("Scan a Single Page");
+ break;
+ case ScanType.ADF:
+ icon_name = "scan-type-adf-symbolic";
+ tooltip_text = _("Scan Multiple Pages");
+ break;
+ case ScanType.BATCH:
+ icon_name = "scan-type-batch-symbolic";
+ tooltip_text = _("Scan Multiple Pages");
+ break;
+ default:
+ icon_name = "scanner-symbolic";
+ tooltip_text = _("Scan a Single Page");
+ break;
+ }
+
+ scan_button_icon.icon_name = icon_name;
+ scan_button.tooltip_text = tooltip_text;
+ scan_button_startup.label = tooltip_text;
+
+ scan_progress_arc.queue_draw ();
+ }
+
+ private void set_dpi (int dpi, bool save = false)
+ {
+ this.dpi = dpi;
+ if (save)
+ settings.set_int ("dpi", dpi);
+ update_info_label ();
+ }
+
+ private ScanOptions make_scan_options ()
+ {
+ var options = new ScanOptions ();
+ options.scan_mode = ScanMode.COLOR;
+ options.dpi = dpi;
+ options.depth = 8;
+ preferences_dialog.get_paper_size (out options.paper_width, out options.paper_height, out options.page_size);
+ options.brightness = brightness;
+ options.contrast = contrast;
+ options.page_delay = page_delay;
+ options.side = preferences_dialog.get_page_side ();
+
+ return options;
+ }
+
+ [GtkCallback]
+ private void scan_button_clicked_cb (Gtk.Button btn)
+ {
+ if (scanning_)
+ stop_scan ();
+ else
+ {
+ var options = make_scan_options ();
+ options.type = scan_type;
+ scan (options);
+ }
+ }
+
+ private void preferences_cb ()
+ {
+ preferences_dialog.present (this);
+ }
+
+ public bool clipboard_has_image () {
+ var clipboard = this.get_clipboard ();
+ Gdk.ContentFormats formats = clipboard.get_formats();
+
+ if (formats.contain_gtype(typeof(Gdk.Texture))) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /* This is an important function to prevent actions that do
+ not make sense for the current selection. It also tells
+ the user which actions are possible in the context menu. */
+ private void update_page_menu ()
+ {
+ updating_page_menu = true;
+ var page = book_view.selected_page;
+ if (page == null || processing_pages)
+ {
+ new_document_action.set_enabled (false);
+ save_document_action.set_enabled (false);
+ page_move_left_action.set_enabled (false);
+ page_move_right_action.set_enabled (false);
+ restore_page_action.set_enabled (false);
+ deskew_text_page_action.set_enabled (false);
+ detect_empty_pages_action.set_enabled (false);
+ convert_to_grayscale_action.set_enabled (false);
+ convert_to_2bit_grayscale_action.set_enabled (false);
+ convert_to_line_art_action.set_enabled (false);
+ automatic_color_filter_action.set_enabled (false);
+ reverse_order_action.set_enabled (false);
+ delete_page_action.set_enabled (false);
+ rotate_left_action.set_enabled (false);
+ rotate_right_action.set_enabled (false);
+ flip_page_action.set_enabled (false);
+ crop_page_action.set_enabled (false);
+ split_page_action.set_enabled (false);
+ copy_to_clipboard_action.set_enabled (false);
+ crop_actions.set_rotate_enabled (false);
+ crop_split_button.remove_css_class ("raised");
+ crop_split_button.add_css_class ("flat");
+ }
+ else
+ {
+ new_document_action.set_enabled (true);
+ save_document_action.set_enabled (true);
+ delete_page_action.set_enabled (true);
+ rotate_left_action.set_enabled (true);
+ rotate_right_action.set_enabled (true);
+ flip_page_action.set_enabled (true);
+ crop_page_action.set_enabled (true);
+ split_page_action.set_enabled (true);
+ copy_to_clipboard_action.set_enabled (true);
+
+ page_move_left_action.set_enabled (book.get_page (0).is_selected == false);
+ page_move_right_action.set_enabled (book.get_page (-1).is_selected == false);
+
+ /* decide if most selected pages are deleted
+ if yes, restore all pages, else delete all action */
+ var marked_deleted_pages = 0;
+ var marked_nondeleted_pages = 0;
+
+ /* check for filters */
+ bool any_filtered = false;
+ bool any_no_auto_rotate = false;
+
+ /* check color depth and channels */
+ int max_depth = 1;
+ int max_channels = 1;
+
+ /* check if pages are a coninuous range */
+ bool no_selection_gap = true;
+ int last_selection = -1;
+ int n_selected = 0;
+
+ bool any_has_crop = false;
+
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if ((book_page != null) && book_page.is_selected)
+ {
+ n_selected ++;
+ if (book_page.is_deleted)
+ marked_deleted_pages++;
+ else
+ marked_nondeleted_pages++;
+
+ if (book_page.is_filtered)
+ any_filtered = true;
+ if (!book_page.has_auto_rotate)
+ any_no_auto_rotate = true;
+ if (book_page.depth > max_depth)
+ max_depth = book_page.depth;
+ if (book_page.n_channels > max_channels)
+ max_channels = book_page.n_channels;
+ if (i != last_selection + 1 && last_selection != -1)
+ no_selection_gap = false;
+ if (book_page.has_crop)
+ any_has_crop = true;
+ last_selection = i;
+ }
+ }
+ if (last_selection == -1)
+ no_selection_gap = false;
+
+ /* reverse order only active if range selected */
+ reverse_order_action.set_enabled (no_selection_gap && n_selected > 1);
+
+ /* Enable deskew text page button only if page not auto-rotated */
+ deskew_text_page_action.set_enabled (any_no_auto_rotate);
+
+ /* only allow detect empty pages if there are pages not marked deleted */
+ detect_empty_pages_action.set_enabled (marked_deleted_pages < n_selected);
+
+ /* Enable color filters only if one page has more depth / colors than the target filter */
+ convert_to_grayscale_action.set_enabled (max_channels > 1);
+ convert_to_2bit_grayscale_action.set_enabled (max_depth > 2 || max_channels > 1);
+ convert_to_line_art_action.set_enabled (max_depth > 1 || max_channels > 1);
+ automatic_color_filter_action.set_enabled (max_channels > 1);
+
+ /* Enable restore button only if there are original pixels to restore */
+ restore_page_action.set_enabled (any_filtered);
+
+ bool most_deleted = marked_deleted_pages > marked_nondeleted_pages;
+
+ if (most_deleted)
+ {
+ delete_button.add_css_class ("suggested-action");
+ delete_button.set_icon_name ("trash-arrow-back-symbolic");
+ delete_button.set_tooltip_text (_("Undelete Selected Pages"));
+ }
+ else
+ {
+ delete_button.remove_css_class ("suggested-action");
+ delete_button.set_icon_name ("edit-delete-symbolic");
+ delete_button.set_tooltip_text (_("Delete Selected Pages"));
+ }
+
+ if (any_has_crop)
+ crop_actions.set_rotate_enabled (true);
+ else
+ crop_actions.set_rotate_enabled (false);
+
+ if (any_has_crop)
+ {
+ crop_split_button.remove_css_class ("flat");
+ crop_split_button.add_css_class ("raised");
+ }
+ else
+ {
+ crop_split_button.remove_css_class ("raised");
+ crop_split_button.add_css_class ("flat");
+ }
+ }
+
+ if (undo_action != null)
+ undo_action.set_enabled (undo_actions.length > 0);
+ if (redo_action != null)
+ redo_action.set_enabled (redo_actions.length > 0);
+
+ updating_page_menu = false;
+ }
+
+ private void page_selected_cb (BookView view, Page? page)
+ {
+ if (page == null)
+ {
+ update_page_menu ();
+ return;
+ }
+
+ /* Disconnect from previous page's signals */
+ if (current_selected_page != null)
+ {
+ current_selected_page.filter_changed.disconnect (page_filter_changed_cb);
+ }
+
+ /* Connect to new page's signals */
+ page.filter_changed.connect (page_filter_changed_cb);
+ current_selected_page = page;
+
+ update_page_menu ();
+
+ crop_actions.update_current_crop (page.crop_name);
+ }
+
+ private void page_filter_changed_cb ()
+ {
+ /* Update button states when page filters change (e.g., after restore_image or rotate) */
+ update_page_menu ();
+ }
+
+ private string make_cache_tmp_dir () throws Error
+ {
+ var cache_dir = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "tmp");
+ DirUtils.create_with_parents (cache_dir, 0700);
+ string full = Path.build_filename (cache_dir, n_tmp_folders.to_string ());
+ DirUtils.create_with_parents (full, 0700);
+ n_tmp_folders ++;
+ return full;
+ }
+
+ private void cleanup_tmp_dir ()
+ {
+ try
+ {
+ var cache_tmp_dir = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "tmp");
+ var tmp_folder = File.new_for_path (cache_tmp_dir);
+
+ if (!tmp_folder.query_exists ())
+ return;
+
+ /* Remove all subdirectories */
+ var enumerator = tmp_folder.enumerate_children ("standard::name,standard::type", FileQueryInfoFlags.NONE);
+ FileInfo info;
+ while ((info = enumerator.next_file ()) != null)
+ {
+ if (info.get_file_type () == FileType.DIRECTORY)
+ {
+ var subdir = tmp_folder.get_child (info.get_name ());
+ try
+ {
+ remove_directory_recursive (subdir);
+ }
+ catch (Error e)
+ {
+ warning ("Failed to remove temporary directory: %s", e.message);
+ }
+ }
+ }
+ }
+ catch (Error e)
+ {
+ warning ("Error cleaning up temporary directory: %s", e.message);
+ }
+ }
+
+ private void remove_directory_recursive (File dir) throws Error
+ {
+ var enumerator = dir.enumerate_children ("standard::name,standard::type", FileQueryInfoFlags.NONE);
+ FileInfo info;
+ while ((info = enumerator.next_file ()) != null)
+ {
+ var child = dir.get_child (info.get_name ());
+ if (info.get_file_type () == FileType.DIRECTORY)
+ remove_directory_recursive (child);
+ else
+ child.delete ();
+ }
+ dir.delete ();
+ }
+
+ private async void show_page_cb (BookView view, Page page)
+ {
+ File file;
+ try
+ {
+ var dir = make_cache_tmp_dir ();
+ file = File.new_for_path (Path.build_filename (dir, "scan.png"));
+
+ yield book.save_async ("image/png", 0, file, null, null, true);
+ }
+ catch (Error e)
+ {
+ show_error_dialog (_("Unable to save image for preview"), e.message);
+ return;
+ }
+ try
+ {
+ var launcher = new Gtk.FileLauncher (file);
+ yield launcher.launch (this, null);
+ }
+ catch (Error e)
+ {
+ return;
+ }
+ }
+
+ private void show_page_menu_cb (BookView view, Gtk.Widget from, double x, double y)
+ {
+ paste_image_action.set_enabled (clipboard_has_image ());
+
+ /* Input point in the 'from' widget's coordinate space */
+ Graphene.Point p = Graphene.Point () {
+ x = (float) x,
+ y = (float) y
+ };
+
+ /* Output point in 'this' widget's coordinate space */
+ Graphene.Point out_p;
+
+ if (from.compute_point (this, p, out out_p)) {
+ Gdk.Rectangle rect = {
+ x: (int) out_p.x,
+ y: (int) out_p.y,
+ w: 1,
+ h: 1
+ };
+
+ page_menu.set_pointing_to (rect);
+ page_menu.popup ();
+ }
+ }
+
+ private void set_crop (string? crop_name)
+ {
+ if (updating_page_menu)
+ return;
+
+ if (crop_name == "none" || crop_name == null)
+ crop_name = null;
+
+ var timestamp = GLib.get_monotonic_time ();
+ bool any_has_crop = false;
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+
+ if (page.is_selected == false)
+ continue;
+
+ var page_index = (int)book.get_page_index (page);
+
+ if (crop_name == null)
+ {
+ add_undo_action (UndoAction.ActionType.CROP, page_index, 0, new int[] {page.crop_x, page.crop_y, page.crop_width, page.crop_height}, page.crop_name, timestamp);
+ page.set_no_crop ();
+ }
+ else if (crop_name == "custom")
+ {
+ any_has_crop = true;
+ /* this can take long due to get_border */
+ try
+ {
+ crop_operation_pool.add (
+ new CropOperationTask (
+ page,
+ page_index,
+ this,
+ timestamp
+ )
+ );
+ }
+ catch (Error e)
+ {
+ warning ("Failed to start crop operation: %s", e.message);
+ }
+ }
+ else
+ {
+ if (page.set_named_crop (crop_name))
+ {
+ add_undo_action (UndoAction.ActionType.CROP, page_index, 1, new int[] {page.crop_x, page.crop_y, page.crop_width, page.crop_height}, page.crop_name, timestamp);
+ }
+ }
+ if (page.has_crop)
+ any_has_crop = true;
+ }
+
+ if (any_has_crop)
+ crop_split_button.add_css_class ("raised");
+ else
+ crop_split_button.remove_css_class ("raised");
+ crop_actions.update_current_crop (book_view.selected_page.crop_name);
+ }
+
+ public void crop_set_action_cb (SimpleAction action, Variant? value)
+ {
+ set_crop (value.get_string ());
+ }
+
+ public void crop_rotate_action_cb ()
+ {
+ var page = book_view.selected_page;
+ if (page == null)
+ return;
+
+ var timestamp = GLib.get_monotonic_time ();
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if (book_page.is_selected && book_page.has_crop && book_page.crop_width < book_page.height && book_page.crop_height < book_page.width)
+ {
+ add_undo_action (UndoAction.ActionType.CROP, i, 0, new int[] {book_page.crop_x, book_page.crop_y, book_page.crop_width, book_page.crop_height}, book_page.crop_name, timestamp);
+ book_page.rotate_crop ();
+ add_undo_action (UndoAction.ActionType.CROP, i, 1, new int[] {book_page.crop_x, book_page.crop_y, book_page.crop_width, book_page.crop_height}, book_page.crop_name, timestamp);
+ }
+ }
+ }
+
+ private void reorder_document_cb ()
+ {
+ var dialog = new ReorderPagesDialog ();
+ dialog.set_transient_for (this);
+
+ /* Button for combining sides in reordering dialog */
+ dialog.combine_sides.clicked.connect (() =>
+ {
+ uint target_location = 1;
+ /* start in the middle and move the pages */
+ for (int i = (int)((book.n_pages + 1) / 2); i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ move_page_with_undo (page, target_location);
+ target_location += 2;
+ }
+ dialog.close ();
+ });
+
+ /* Button for combining sides in reverse order in reordering dialog */
+ dialog.combine_sides_rev.clicked.connect (() =>
+ {
+ uint target_location = 1;
+ for (int i = (int)((book.n_pages + 1) / 2); i < book.n_pages; i++)
+ {
+ /* just keep moving the last page */
+ var page = book.get_page (-1);
+ move_page_with_undo (page, target_location);
+ target_location += 2;
+ }
+ dialog.close ();
+ });
+
+ /* Button for reversing in reordering dialog */
+ dialog.reverse.clicked.connect (() =>
+ {
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page != null) {
+ move_page_with_undo (page, 0);
+ }
+ }
+ dialog.close ();
+ });
+
+ /* Button for keeping the ordering, but flip every second upside down */
+ dialog.flip_odd.clicked.connect (() =>
+ {
+ select_odd_pages_cb ();
+ flip_page_cb ();
+ dialog.close ();
+ });
+
+ /* Button for keeping the ordering, but flip every second upside down */
+ dialog.flip_even.clicked.connect (() =>
+ {
+ select_even_pages_cb ();
+ flip_page_cb ();
+ dialog.close ();
+ });
+
+ dialog.present ();
+ }
+
+ private void new_document_cb ()
+ {
+ new_document ();
+ }
+
+ public void save_document_cb ()
+ {
+ save_document_async.begin ();
+ }
+
+ private void draw_page (Gtk.PrintOperation operation,
+ Gtk.PrintContext print_context,
+ int page_number)
+ {
+ var context = print_context.get_cairo_context ();
+
+ /* map number to non-deleted page index */
+ int nr_to_page_index = 0;
+ var displayed_pages = 0;
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ if (!book.get_page (i).is_deleted)
+ displayed_pages++;
+ if (displayed_pages - 1 == page_number)
+ {
+ nr_to_page_index = i;
+ break;
+ }
+ }
+
+ var page = book.get_page (nr_to_page_index);
+
+ /* Rotate to same aspect */
+ bool is_landscape = false;
+ if (print_context.get_width () > print_context.get_height ())
+ is_landscape = true;
+ if (page.is_landscape != is_landscape)
+ {
+ context.translate (print_context.get_width (), 0);
+ context.rotate (Math.PI_2);
+ }
+
+ context.scale (print_context.get_dpi_x () / page.dpi,
+ print_context.get_dpi_y () / page.dpi);
+
+ var image = page.get_image (true, true);
+
+ Gdk.cairo_set_source_pixbuf (context, image, 0, 0);
+ context.paint ();
+ }
+
+ private void email_document_cb ()
+ {
+ email_document_async.begin ();
+ }
+
+ private void export_selection_cb ()
+ {
+ save_document_async.begin (true);
+ }
+
+ private async void email_document_async ()
+ {
+ try
+ {
+ var dir = make_cache_tmp_dir ();
+ string mime_type = "application/pdf";
+ string filename = "scan.pdf";
+ var file = File.new_for_path (Path.build_filename (dir, filename));
+ yield book.save_async (mime_type, settings.get_int ("jpeg-quality"), file,
+ null, null);
+ yield book.postprocess_async (mime_type, file, settings.get_boolean ("postproc-enabled"),
+ settings.get_string ("postproc-script"), settings.get_string ("postproc-arguments"),
+ settings.get_boolean ("postproc-keep-original"));
+ var command_line = "xdg-email";
+ command_line += " --attach %s".printf (file.get_path ());
+
+ Process.spawn_command_line_async (command_line);
+ }
+ catch (Error e)
+ {
+ warning ("Unable to email document: %s", e.message);
+ }
+ }
+ private void open_folder_cb (SimpleAction action, Variant? parameter)
+ requires (parameter != null)
+ {
+ string uri = parameter.get_string ();
+ var file = File.new_for_uri (uri);
+
+ var launcher = new Gtk.FileLauncher (file);
+ launcher.open_containing_folder.begin (this, null, (obj, res) => {
+ try {
+ launcher.open_containing_folder.end (res);
+ } catch (Error e) {
+ warning ("Failed to open containing folder: %s", e.message);
+ }
+ });
+ }
+
+
+ private void print_document ()
+ {
+ var print = new Gtk.PrintOperation ();
+ int n_non_deleted = 0;
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ if (!book.get_page (i).is_deleted)
+ n_non_deleted++;
+ }
+ print.n_pages = n_non_deleted;
+ print.draw_page.connect (draw_page);
+
+ try
+ {
+ print.run (Gtk.PrintOperationAction.PRINT_DIALOG, this);
+ }
+ catch (Error e)
+ {
+ warning ("Error printing: %s", e.message);
+ }
+
+ print.draw_page.disconnect (draw_page);
+ }
+
+ private void print_document_cb ()
+ {
+ print_document ();
+ }
+
+ private void launch_help ()
+ {
+ var launcher = new Gtk.UriLauncher ("help:simple-scan");
+ launcher.launch.begin (this, null);
+ }
+
+ private void help_cb ()
+ {
+ launch_help ();
+ }
+
+ private void show_about ()
+ {
+ string[] authors = { "Robert Ancell " };
+
+ var about = new Adw.AboutDialog ()
+ {
+ developers = authors,
+ translator_credits = _("translator-credits"),
+ copyright = "Copyright © 2009-2018 Canonical Ltd.",
+ license_type = Gtk.License.GPL_3_0,
+ application_name = _("Document Scanner"),
+ application_icon = "org.gnome.SimpleScan",
+ version = VERSION,
+ website = "https://gitlab.gnome.org/GNOME/simple-scan",
+ issue_url = "https://gitlab.gnome.org/GNOME/simple-scan/-/issues/",
+ };
+
+ about.present (this);
+ }
+
+ private void about_cb ()
+ {
+ show_about ();
+ }
+
+ private void on_quit ()
+ {
+ prompt_to_save_async.begin (/* Text in dialog warning when a document is about to be lost */
+ _("Save document before quitting?"),
+ /* Text in dialog warning when a document is about to be lost */
+ _("_Quit without Saving"), (obj, res) =>
+ {
+ if (!prompt_to_save_async.end(res))
+ return;
+
+ destroy ();
+
+ if (save_state_timeout != 0)
+ save_state (true);
+
+ autosave_manager.cleanup ();
+ });
+ }
+
+ private void quit_cb ()
+ {
+ on_quit ();
+ }
+
+ private void edit_button_cb ()
+ {
+ edit_button.activate ();
+ }
+
+ private void toggle_view_cb ()
+ {
+ book_view.toggle_grid ();
+ }
+
+ private void deskew_page_cb ()
+ {
+ book_view.deskew ();
+ }
+
+ private void zoom_in_cb ()
+ {
+ if (book_view != null)
+ book_view.zoom_in ();
+ }
+
+ private void zoom_out_cb ()
+ {
+ if (book_view != null)
+ book_view.zoom_out ();
+ }
+
+ public override void size_allocate (int width, int height, int baseline)
+ {
+ base.size_allocate (width, height, baseline);
+
+ if (!window_is_maximized && !window_is_fullscreen)
+ {
+ window_width = this.get_width();
+ window_height = this.get_height();
+ save_state ();
+ }
+ }
+
+ public override void unmap ()
+ {
+ window_is_maximized = is_maximized ();
+ window_is_fullscreen = is_fullscreen ();
+ save_state ();
+
+ base.unmap ();
+ }
+
+
+ [GtkCallback]
+ private bool window_close_request_cb (Gtk.Window window)
+ {
+ on_quit ();
+ return true; /* Let us quit on our own terms */
+ }
+
+ private void page_added_cb (Book book, Page page)
+ {
+ update_page_menu ();
+ }
+
+ private void reordered_cb (Book book)
+ {
+ update_page_menu ();
+ }
+
+ private void page_removed_cb (Book book, Page page)
+ {
+ update_page_menu ();
+ update_info_label ();
+ }
+
+ private void book_changed_cb (Book book)
+ {
+ save_button.sensitive = true;
+ book_needs_saving = true;
+ copy_to_clipboard_action.set_enabled (true);
+ }
+
+ public void update_info_label (int scans = -1)
+ {
+ if (scans > 0) {
+ scan_count = scans;
+ window_title.subtitle = _("%d pages (scanning %d...)").printf ((int) book.n_pages, scan_count);
+ }
+ else if (scan_count > 1)
+ {
+ window_title.subtitle = _("%d pages (%d last scan)").printf ((int) book.n_pages, scan_count);
+ }
+ else if (book.n_pages == 0)
+ {
+ /* Show the current scan settings instead of a page count when the book is empty */
+ window_title.subtitle = get_scan_settings_summary ();
+ }
+ else {
+ window_title.subtitle = _("%d pages").printf ((int) book.n_pages);
+ }
+ }
+
+ private string get_scan_settings_summary ()
+ {
+ var page_size_label = get_page_size_label (settings.get_string ("page-size"));
+ var scan_side_label = get_scan_side_label ((ScanSide) settings.get_enum ("page-side"));
+ /* Scan settings summary shown below the window title when there are no pages.
+ * The order is page size, scan side(s) and resolution (dpi = dots per inch). */
+ return _("%s · %s · %d dpi").printf (page_size_label, scan_side_label, dpi);
+ }
+
+ private string get_page_size_label (string size_id)
+ {
+ switch (size_id)
+ {
+ case "Automatic":
+ return _("Automatic");
+ case "Custom":
+ return _("Custom");
+ case "letter":
+ return _("Letter");
+ case "legal":
+ return _("Legal");
+ case "4x6":
+ return _("4×6");
+ default:
+ /* A6, A5, A4, A3 and any other identifier are shown as-is */
+ return size_id;
+ }
+ }
+
+ private string get_scan_side_label (ScanSide side)
+ {
+ switch (side)
+ {
+ case ScanSide.FRONT:
+ return _("Front");
+ case ScanSide.BACK:
+ return _("Back");
+ default:
+ case ScanSide.BOTH:
+ return _("Both Sides");
+ }
+ }
+
+ private void load ()
+ {
+ preferences_dialog = new PreferencesDialog (settings);
+
+ Gtk.Window.set_default_icon_name ("org.gnome.SimpleScan");
+
+ var app = Application.get_default () as Gtk.Application;
+
+ crop_actions = new CropActions (this);
+
+ app.add_action_entries (action_entries, this);
+
+ save_document_action = (GLib.SimpleAction) app.lookup_action("save");
+ new_document_action = (GLib.SimpleAction) app.lookup_action("new");
+ scan_type_action = (GLib.SimpleAction) app.lookup_action("scan_type");
+ dpi_action = (GLib.SimpleAction) app.lookup_action("dpi");
+ load_dpi_from_settings ();
+ paper_size_action = (GLib.SimpleAction) app.lookup_action("paper_size");
+ load_paper_size_from_settings ();
+ scan_side_action = (GLib.SimpleAction) app.lookup_action("scan_side");
+ load_scan_side_from_settings ();
+
+ delete_page_action = (GLib.SimpleAction) app.lookup_action("delete_page");
+ toggle_deleted_display_action = (GLib.SimpleAction) app.lookup_action("toggle_deleted_display");
+ restore_page_action = (GLib.SimpleAction) app.lookup_action("restore_image");
+ deskew_text_page_action = (GLib.SimpleAction) app.lookup_action("deskew_text_page");
+ detect_empty_pages_action = (GLib.SimpleAction) app.lookup_action("detect_empty_pages");
+ convert_to_grayscale_action = (GLib.SimpleAction) app.lookup_action("convert_to_grayscale");
+ convert_to_2bit_grayscale_action = (GLib.SimpleAction) app.lookup_action("convert_to_2bit_grayscale");
+ convert_to_line_art_action = (GLib.SimpleAction) app.lookup_action("convert_to_line_art");
+ automatic_color_filter_action = (GLib.SimpleAction) app.lookup_action("automatic_color_filter");
+ page_move_left_action = (GLib.SimpleAction) app.lookup_action("move_left");
+ page_move_right_action = (GLib.SimpleAction) app.lookup_action("move_right");
+ copy_to_clipboard_action = (GLib.SimpleAction) app.lookup_action("copy_page");
+ paste_image_action = (GLib.SimpleAction) app.lookup_action("paste_image");
+ paste_crop_action = (GLib.SimpleAction) app.lookup_action("paste_crop");
+ reverse_order_action = (GLib.SimpleAction) app.lookup_action("reverse_order");
+ rotate_left_action = (GLib.SimpleAction) app.lookup_action("rotate_left");
+ rotate_right_action = (GLib.SimpleAction) app.lookup_action("rotate_right");
+ flip_page_action = (GLib.SimpleAction) app.lookup_action("flip_page");
+ crop_page_action = (GLib.SimpleAction) app.lookup_action("crop_page");
+ split_page_action = (GLib.SimpleAction) app.lookup_action("split_page");
+ undo_action = (GLib.SimpleAction) app.lookup_action("undo");
+ redo_action = (GLib.SimpleAction) app.lookup_action("redo");
+
+ // Initialize undo/redo actions as disabled
+ undo_action.set_enabled (false);
+ redo_action.set_enabled (false);
+
+ // --- Register accelerators ---
+ app.set_accels_for_action ("app.new", ACCEL_NEW_DOCUMENT);
+ app.set_accels_for_action ("app.scan_single", ACCEL_SCAN_SINGLE);
+ app.set_accels_for_action ("app.scan_adf", ACCEL_SCAN_ADF);
+ app.set_accels_for_action ("app.scan_batch", ACCEL_SCAN_BATCH);
+ app.set_accels_for_action ("app.scan_side::front", ACCEL_SCAN_SIDE_FRONT);
+ app.set_accels_for_action ("app.scan_side::back", ACCEL_SCAN_SIDE_BACK);
+ app.set_accels_for_action ("app.scan_side::both", ACCEL_SCAN_SIDE_BOTH);
+ app.set_accels_for_action ("app.scan_start", ACCEL_SCAN_START);
+ app.set_accels_for_action ("app.scan_stop", ACCEL_SCAN_STOP);
+ app.set_accels_for_action ("app.rotate_left", ACCEL_ROTATE_LEFT);
+ app.set_accels_for_action ("app.rotate_right", ACCEL_ROTATE_RIGHT);
+ app.set_accels_for_action ("app.flip_page", ACCEL_FLIP_PAGE);
+ app.set_accels_for_action ("app.deskew_text_page", ACCEL_DESKEW_TEXT_PAGE);
+ app.set_accels_for_action ("app.detect_empty_pages", ACCEL_DETECT_EMPTY_PAGES);
+ app.set_accels_for_action ("app.split_page", ACCEL_SPLIT_PAGE);
+ app.set_accels_for_action ("app.convert_to_line_art", ACCEL_CONVERT_TO_LINE_ART);
+ app.set_accels_for_action ("app.convert_to_2bit_grayscale", ACCEL_CONVERT_TO_2BIT_GRAYSCALE);
+ app.set_accels_for_action ("app.convert_to_grayscale", ACCEL_CONVERT_TO_GRAYSCALE);
+ app.set_accels_for_action ("app.automatic_color_filter", ACCEL_CONVERT_AUTOMATIC);
+ app.set_accels_for_action ("app.restore_image", ACCEL_RESTORE_IMAGE);
+ app.set_accels_for_action ("app.move_left", ACCEL_MOVE_LEFT);
+ app.set_accels_for_action ("app.move_right", ACCEL_MOVE_RIGHT);
+ app.set_accels_for_action ("app.crop_page", ACCEL_CROP_PAGE);
+ app.set_accels_for_action ("app.copy_page", ACCEL_COPY_PAGE);
+ app.set_accels_for_action ("app.paste_image", ACCEL_PASTE_IMAGE);
+ app.set_accels_for_action ("app.paste_crop", ACCEL_PASTE_CROP);
+ app.set_accels_for_action ("app.delete_page", ACCEL_DELETE_PAGE);
+ app.set_accels_for_action ("app.toggle_deleted_display", ACCEL_TOGGLE_DELETED_DISPLAY);
+ app.set_accels_for_action ("app.select_all", ACCEL_SELECT_ALL);
+ app.set_accels_for_action ("app.select_even_pages", ACCEL_SELECT_EVEN_PAGES);
+ app.set_accels_for_action ("app.select_odd_pages", ACCEL_SELECT_ODD_PAGES);
+ app.set_accels_for_action ("app.reverse_order", ACCEL_REVERSE_ORDER);
+ app.set_accels_for_action ("app.save", ACCEL_SAVE);
+ app.set_accels_for_action ("app.email", ACCEL_EMAIL);
+ app.set_accels_for_action ("app.print", ACCEL_PRINT);
+ app.set_accels_for_action ("app.help", ACCEL_HELP);
+ app.set_accels_for_action ("app.quit", ACCEL_QUIT);
+ app.set_accels_for_action ("app.preferences", ACCEL_PREFERENCES);
+ app.set_accels_for_action ("app.shortcuts", ACCEL_SHORTCUTS);
+ app.set_accels_for_action ("app.zoom_in", ACCEL_ZOOM_IN);
+ app.set_accels_for_action ("app.zoom_out", ACCEL_ZOOM_OUT);
+ app.set_accels_for_action ("app.toggle_view", ACCEL_TOGGLE_VIEW);
+ app.set_accels_for_action ("app.edit_page", ACCEL_EDIT_PAGE);
+ app.set_accels_for_action ("app.deskew_page", ACCEL_DESKEW_PAGE);
+ app.set_accels_for_action ("app.undo", ACCEL_UNDO);
+ app.set_accels_for_action ("app.redo", ACCEL_REDO);
+
+
+ var gear_menu = new Menu ();
+ var section = new Menu ();
+ gear_menu.append_section (null, section);
+ section.append (_("E_xport selection…"), "app.export_selection");
+ section.append (_("_Email…"), "app.email");
+ section.append (_("Pri_nt…"), "app.print");
+ section.append (C_("menu", "_Reorder Pages…"), "app.reorder");
+ section = new Menu ();
+ gear_menu.append_section (null, section);
+ section.append (_("_Preferences"), "app.preferences");
+ section.append (_("_Keyboard Shortcuts"), "app.shortcuts");
+ section.append (_("_Help"), "app.help");
+ section.append (_("_About Document Scanner"), "app.about");
+ menu_button.set_menu_model (gear_menu);
+
+ app.add_window (this);
+
+ book_view = new BookView (book);
+ book_view.vexpand = true;
+
+ main_vbox.prepend (book_view);
+
+ book_view.page_selected.connect (page_selected_cb);
+ book_view.show_page.connect (show_page_cb);
+ book_view.show_menu.connect (show_page_menu_cb);
+ book_view.move_page_with_undo.connect (book_view_move_page_with_undo_cb);
+ book_view.scan_progress_changed.connect (scan_progress_changed_cb);
+ book_view.request_deskew_help.connect (show_deskew_help_toast);
+ book_view.visible = true;
+
+ /* connect the progress arc drawing function */
+ scan_progress_arc.set_snapshot_func ((snapshot, width, height) => {
+ if (!scanning_)
+ return;
+
+ /* display time to next scan */
+ if (time_to_wait > 0)
+ scan_progress = time_to_wait;
+
+ float center_x = (float) width / 2.0f;
+ float center_y = (float) height / 2.0f;
+ float radius = (float) ((width < height ? width : height) / 2.0 - 1);
+
+ /* Set up colors - use the accent color from the theme */
+ var stroke_color = Adw.StyleManager.get_default ().accent_color.to_rgba ();
+ /* if scan_button is blue (suggested action), use white */
+ if (scan_button.has_css_class ("suggested-action"))
+ stroke_color = { 1.0f, 1.0f, 1.0f, 1.0f };
+
+ /* Build an arc path: start at top (-π/2), sweep clockwise by 2π·progress.
+ Gsk.PathBuilder.svg_arc_to is the easiest portable arc primitive. */
+ var builder = new Gsk.PathBuilder ();
+ float start_angle = -(float) Math.PI / 2.0f;
+ float sweep = (float) Math.fmin (2.0 * Math.PI * scan_progress, 2.0 * Math.PI);
+ float end_angle = start_angle + sweep;
+
+ float start_x = center_x + radius * (float) Math.cos (start_angle);
+ float start_y = center_y + radius * (float) Math.sin (start_angle);
+ float end_x = center_x + radius * (float) Math.cos (end_angle);
+ float end_y = center_y + radius * (float) Math.sin (end_angle);
+
+ builder.move_to (start_x, start_y);
+
+ if (Math.fabs (sweep) > Math.PI * 0.01)
+ {
+ bool large_arc = sweep > Math.PI;
+ builder.svg_arc_to (radius, radius, 0.0f, large_arc, true, end_x, end_y);
+ }
+
+ var path = builder.to_path ();
+ var stroke = new Gsk.Stroke (2.0f);
+ snapshot.append_stroke (path, stroke, stroke_color);
+ });
+
+ /* Load previous state */
+ load_state ();
+
+ /* Restore window size */
+ debug ("Restoring window to %dx%d pixels", window_width, window_height);
+ set_default_size (window_width, window_height);
+ if (window_is_maximized)
+ {
+ debug ("Restoring window to maximized");
+ maximize ();
+ }
+ if (window_is_fullscreen)
+ {
+ debug ("Restoring window to fullscreen");
+ fullscreen ();
+ }
+ }
+
+ private string state_filename
+ {
+ owned get { return Path.build_filename (Environment.get_user_config_dir (), "simple-scan", "state"); }
+ }
+
+ private void load_state ()
+ {
+ debug ("Loading state from %s", state_filename);
+
+ var f = new KeyFile ();
+ try
+ {
+ f.load_from_file (state_filename, KeyFileFlags.NONE);
+ }
+ catch (Error e)
+ {
+ if (!(e is FileError.NOENT))
+ warning ("Failed to load state: %s", e.message);
+ }
+ window_width = state_get_integer (f, "window", "width", 600);
+ if (window_width <= 0)
+ window_width = 600;
+ window_height = state_get_integer (f, "window", "height", 400);
+ if (window_height <= 0)
+ window_height = 400;
+ window_is_maximized = state_get_boolean (f, "window", "is-maximized");
+ window_is_fullscreen = state_get_boolean (f, "window", "is-fullscreen");
+ scan_type = Scanner.type_from_string(state_get_string (f, "scanner", "scan-type"));
+ set_scan_type (scan_type);
+ }
+
+ private string state_get_string (KeyFile f, string group_name, string key, string default = "")
+ {
+ try
+ {
+ return f.get_string (group_name, key);
+ }
+ catch
+ {
+ return default;
+ }
+ }
+
+ private int state_get_integer (KeyFile f, string group_name, string key, int default = 0)
+ {
+ try
+ {
+ return f.get_integer (group_name, key);
+ }
+ catch
+ {
+ return default;
+ }
+ }
+
+ private bool state_get_boolean (KeyFile f, string group_name, string key, bool default = false)
+ {
+ try
+ {
+ return f.get_boolean (group_name, key);
+ }
+ catch
+ {
+ return default;
+ }
+ }
+
+ private static string STATE_DIR = Path.build_filename (Environment.get_user_config_dir (), "simple-scan", null);
+ private void save_state (bool force = false)
+ {
+ if (!force)
+ {
+ if (save_state_timeout != 0)
+ Source.remove (save_state_timeout);
+ save_state_timeout = Timeout.add (100, () =>
+ {
+ save_state (true);
+ save_state_timeout = 0;
+ return false;
+ });
+ return;
+ }
+
+ debug ("Saving state to %s", state_filename);
+
+ var f = new KeyFile ();
+ f.set_integer ("window", "width", window_width);
+ f.set_integer ("window", "height", window_height);
+ f.set_boolean ("window", "is-maximized", window_is_maximized);
+ f.set_boolean ("window", "is-fullscreen", window_is_fullscreen);
+ f.set_string ("scanner", "scan-type", Scanner.type_to_string(scan_type));
+ try
+ {
+ DirUtils.create_with_parents (STATE_DIR, 0700);
+ FileUtils.set_contents (state_filename, f.to_data ());
+ }
+ catch (Error e)
+ {
+ warning ("Failed to write state: %s", e.message);
+ }
+ }
+
+ public void start ()
+ {
+ visible = true;
+ autosave_manager = new AutosaveManager ();
+ autosave_manager.book = book;
+
+ if (autosave_manager.exists ())
+ {
+ prompt_to_load_autosaved_book.begin ((obj, res) => {
+ bool restore = prompt_to_load_autosaved_book.end (res);
+
+ if (restore)
+ {
+ autosave_manager.load ();
+ }
+
+ if (book.n_pages == 0)
+ book_needs_saving = false;
+ else
+ {
+ stack.set_visible_child_name ("document");
+ book_view.select_first_page ();
+ book_needs_saving = true;
+ book_changed_cb (book);
+ update_info_label ();
+ }
+ });
+ }
+ }
+
+ private void rotate_page_left_with_undo (Page page, int page_index, int64 timestamp = 0)
+ {
+ page.rotate_left ();
+ add_undo_action (UndoAction.ActionType.ROTATE, page_index, 3, {}, null, timestamp);
+ }
+
+ private void rotate_page_right_with_undo (Page page, int page_index, int64 timestamp = 0)
+ {
+ page.rotate_right ();
+ add_undo_action (UndoAction.ActionType.ROTATE, page_index, 1, {}, null, timestamp);
+ }
+
+ public void move_page_with_undo (Page page, uint location, int64 timestamp = 0)
+ {
+ int old_location = (int) book.get_page_index (page);
+ book.move_page (page, location);
+ add_undo_action (UndoAction.ActionType.MOVE, (int) location, old_location, {}, null, timestamp);
+ }
+
+ private void add_undo_action (UndoAction.ActionType action_type, int page_index, int action_value, int[] crop_data = {}, string? crop_name = null, int64 timestamp = 0)
+ {
+ /* Clear redo stack when a new action is performed */
+ redo_actions.set_size (0);
+ if (timestamp == 0)
+ timestamp = GLib.get_monotonic_time ();
+ undo_actions.append_val (new UndoAction (action_type, timestamp, page_index, action_value, crop_data, crop_name));
+ undo_action.set_enabled (true);
+ redo_action.set_enabled (false);
+ }
+
+ private void undo_cb ()
+ {
+ if (undo_actions.length == 0)
+ return;
+
+ var action = undo_actions.remove_index (undo_actions.length - 1);
+ var first_timestamp = action.timestamp;
+
+ /* undo all changes with the same timestamp */
+ while (true)
+ {
+ if (action.action_type == UndoAction.ActionType.ROTATE)
+ {
+ var page = book.get_page (action.page_index);
+ if (page != null)
+ {
+ /* Undo by applying the inverse of the original right-rotation count. */
+ int undo_rotations = (4 - action.action_value) % 4;
+ for (int i = 0; i < undo_rotations; i++)
+ page.rotate_right ();
+ }
+ }
+ else if (action.action_type == UndoAction.ActionType.MOVE)
+ {
+ var page = book.get_page ((int) action.page_index);
+ if (page != null)
+ {
+ /* Move page back to its original location */
+ book.move_page (page, (uint) action.action_value);
+ }
+ }
+ else if (action.action_type == UndoAction.ActionType.DELETE)
+ {
+ var page = book.get_page ((int) action.page_index);
+ if (page != null)
+ {
+ /* Toggle delete */
+ book.delete_page (page);
+ }
+ }
+ else if (action.action_type == UndoAction.ActionType.CROP)
+ {
+ var page = book.get_page ((int) action.page_index);
+ if (page != null)
+ {
+ /* reset crop */
+ if (action.action_value == 0 || action.action_value == 2)
+ {
+ if (action.crop_name != "custom" && action.crop_name != null)
+ {
+ page.set_named_crop (action.crop_name);
+ }
+ else
+ {
+ page.set_custom_crop (action.crop_width, action.crop_height);
+ }
+ page.move_crop (action.crop_x, action.crop_y);
+ }
+ /* remove crop */
+ if (action.action_value == 1)
+ {
+ page.set_no_crop ();
+ }
+
+ }
+ }
+ else if (action.action_type == UndoAction.ActionType.SPLIT_PAGE) {
+ var page = book.get_page (action.page_index);
+ book.permanently_delete_page(page);
+ }
+
+ redo_actions.append_val (action);
+
+ if (undo_actions.length == 0)
+ {
+ undo_action.set_enabled (false);
+ break;
+ }
+ else
+ {
+ action = undo_actions.index (undo_actions.length - 1);
+ if (first_timestamp == action.timestamp) {
+ action = undo_actions.remove_index (undo_actions.length - 1);
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ }
+ redo_action.set_enabled (redo_actions.length != 0);
+ update_page_menu ();
+ }
+
+ private void redo_cb ()
+ {
+ if (redo_actions.length == 0)
+ return;
+
+ var action = redo_actions.remove_index (redo_actions.length - 1);
+ var first_timestamp = action.timestamp;
+
+ /* undo all changes with the same timestamp */
+ while (true)
+ {
+ if (action.action_type == UndoAction.ActionType.ROTATE)
+ {
+ var page = book.get_page (action.page_index);
+ if (page != null)
+ {
+ /* Redo by applying the original right rotation count. */
+ for (int i = 0; i < action.action_value; i++)
+ page.rotate_right ();
+ }
+ }
+ else if (action.action_type == UndoAction.ActionType.MOVE)
+ {
+ var page = book.get_page ((int) action.action_value);
+ if (page != null)
+ {
+ book.move_page (page, (uint) action.page_index);
+ }
+ }
+ else if (action.action_type == UndoAction.ActionType.DELETE)
+ {
+ var page = book.get_page ((int) action.page_index);
+ if (page != null)
+ {
+ /* Toggle delete */
+ book.delete_page (page);
+ }
+ }
+ else if (action.action_type == UndoAction.ActionType.CROP)
+ {
+ var page = book.get_page ((int) action.page_index);
+ if (page != null)
+ {
+ /* reset crop */
+ if (action.action_value == 1 || action.action_value == 2)
+ {
+ if (action.crop_name != "custom" && action.crop_name != null)
+ {
+ page.set_named_crop (action.crop_name);
+ }
+ else
+ {
+ page.set_custom_crop (action.crop_width, action.crop_height);
+ }
+ page.move_crop (action.crop_x, action.crop_y);
+ }
+ /* remove crop */
+ if (action.action_value == 0)
+ {
+ page.set_no_crop ();
+ }
+
+ }
+ }
+ else if (action.action_type == UndoAction.ActionType.SPLIT_PAGE)
+ {
+ var page = book.get_page (action.page_index - 1);
+ var new_page = page.copy ();
+ book.append_page (new_page);
+ new_page.scan_finished ();
+ book.move_page (new_page, (uint)(action.page_index + 1));
+ }
+
+ undo_actions.append_val (action);
+ if (redo_actions.length == 0)
+ {
+ redo_action.set_enabled (false);
+ break;
+ }
+ else
+ {
+ action = redo_actions.index (redo_actions.length - 1);
+ if (first_timestamp == action.timestamp) {
+ action = redo_actions.remove_index (redo_actions.length - 1);
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ undo_action.set_enabled (true);
+ update_page_menu ();
+ }
+
+ private void dismiss_deskew_info_cb ()
+ {
+ settings.set_boolean ("deskew-help-toast-shown", true);
+ }
+
+ private void show_deskew_help_toast ()
+ {
+ if (settings.get_boolean ("deskew-help-toast-shown"))
+ return;
+
+ var toast = new Adw.Toast (_("Deskew the page by drawing a line that marks what should be horizontal, using two clicks.")) {
+ button_label = _("Got It"),
+ action_name = "app.dismiss-deskew-info"
+ };
+
+ toast_overlay.add_toast (toast);
+ }
+
+ private void dismiss_filter_info_cb ()
+ {
+ settings.set_boolean ("filter-undo-toast-shown", true);
+ }
+
+ private void show_filter_undo_toast ()
+ {
+ if (settings.get_boolean ("filter-undo-toast-shown"))
+ return;
+
+ var toast = new Adw.Toast (_("To undo filters or deskew use Restore Original Image (%s).").printf (ACCEL_RESTORE_IMAGE[0])) {
+ button_label = _("Got It"),
+ action_name = "app.dismiss-filter-info"
+ };
+
+ toast_overlay.add_toast (toast);
+ }
+}
+
+private class CancellableProgressBar : Gtk.Box
+{
+ private Gtk.ProgressBar bar;
+ private Gtk.Button? button;
+
+ public CancellableProgressBar (string? text, Cancellable? cancellable)
+ {
+ this.orientation = Gtk.Orientation.HORIZONTAL;
+
+ bar = new Gtk.ProgressBar ();
+ bar.visible = true;
+ bar.set_text (text);
+ bar.set_show_text (true);
+ prepend (bar);
+
+ if (cancellable != null)
+ {
+ button = new Gtk.Button.from_icon_name ("process-stop-symbolic");
+ button.add_css_class ("destructive-action");
+ button.add_css_class ("circular");
+ button.set_margin_end (6);
+ button.sensitive = true;
+ button.visible = true;
+ button.clicked.connect (() =>
+ {
+ set_visible (false);
+ cancellable.cancel ();
+ });
+ prepend (button);
+ }
+ }
+
+ public void set_fraction (double fraction)
+ {
+ bar.set_fraction (fraction);
+ }
+
+ public void remove_with_delay (uint delay, Gtk.Box parent)
+ {
+ button.set_sensitive (false);
+
+ Timeout.add (delay, () =>
+ {
+ parent.remove (this);
+ return false;
+ });
+ }
+}
+
+private class SimpleProgressBar : Gtk.Box
+{
+ private Gtk.ProgressBar bar;
+
+ public SimpleProgressBar (string? text)
+ {
+ this.orientation = Gtk.Orientation.HORIZONTAL;
+
+ bar = new Gtk.ProgressBar ();
+ bar.visible = true;
+ bar.set_text (text);
+ bar.set_show_text (true);
+ prepend (bar);
+ }
+
+ public void add_fraction (double to_add)
+ {
+ bar.set_fraction (bar.get_fraction () + to_add);
+ }
+
+ public void remove_with_delay (uint delay, Gtk.Box parent)
+ {
+ Timeout.add (delay, () =>
+ {
+ if (this.get_parent () == parent)
+ parent.remove (this);
+ return false;
+ });
+ }
+}
+
+private class CropActions
+{
+ private GLib.SimpleActionGroup group;
+
+ private GLib.SimpleAction crop_set;
+ private GLib.SimpleAction crop_rotate;
+
+ private GLib.ActionEntry[] crop_entries =
+ {
+ { "set", AppWindow.crop_set_action_cb, "s", "'none'" },
+ { "rotate", AppWindow.crop_rotate_action_cb },
+ };
+
+ public CropActions (AppWindow window)
+ {
+ group = new GLib.SimpleActionGroup ();
+ group.add_action_entries (crop_entries, window);
+
+ crop_set = (GLib.SimpleAction) group.lookup_action ("set");
+ crop_rotate = (GLib.SimpleAction) group.lookup_action ("rotate");
+
+ window.insert_action_group ("crop", group);
+ }
+
+ public void set_rotate_enabled (bool value)
+ {
+ crop_rotate.set_enabled (value);
+ }
+
+ public void update_current_crop (string? crop_name)
+ {
+ crop_rotate.set_enabled (crop_name != null);
+
+ if (crop_name == null)
+ crop_set.set_state ("none");
+ else
+ crop_set.set_state (crop_name);
+ }
+}
diff --git a/src/authorize-dialog.vala b/src/authorize-dialog.vala
index ab0237a4c37fae46d3bc7621e1b72c060dda3347..3ca36619607b9944f73c88b705b5613f8a866f75 100644
--- a/src/authorize-dialog.vala
+++ b/src/authorize-dialog.vala
@@ -1,96 +1,96 @@
-/*
- * Copyright (C) 2009-2017 Canonical Ltd.
- * Author: Robert Ancell ,
- * Eduard Gotwig
- *
- * 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.
- */
-
-[GtkTemplate (ui = "/org/gnome/SimpleScan/ui/authorize-dialog.ui")]
-private class AuthorizeDialog : Gtk.Window
-{
- [GtkChild]
- private unowned Adw.PreferencesGroup preferences_group;
- [GtkChild]
- private unowned Adw.EntryRow username_entry;
- [GtkChild]
- private unowned Adw.PasswordEntryRow password_entry;
-
- public signal void authorized (AuthorizeDialogResponse res);
-
- public AuthorizeDialog (Gtk.Window parent, string title)
- {
- preferences_group.set_title (title);
- set_transient_for (parent);
- }
-
- public string get_username ()
- {
- return username_entry.text;
- }
-
- public string get_password ()
- {
- return password_entry.text;
- }
-
- [GtkCallback]
- private void authorize_button_cb ()
- {
- authorized (AuthorizeDialogResponse.new_authorized (get_username (), get_password ()));
- }
-
- [GtkCallback]
- private void cancel_button_cb ()
- {
- authorized (AuthorizeDialogResponse.new_canceled ());
- }
-
- public async AuthorizeDialogResponse open()
- {
- SourceFunc callback = open.callback;
-
- AuthorizeDialogResponse response = {};
-
- authorized.connect ((res) =>
- {
- response = res;
- callback ();
- });
-
- present ();
- yield;
- close ();
-
- return response;
- }
-}
-
-public struct AuthorizeDialogResponse
-{
- public string username;
- public string password;
- public bool success;
-
- public static AuthorizeDialogResponse new_canceled ()
- {
- return AuthorizeDialogResponse ()
- {
- success = false,
- };
- }
-
- public static AuthorizeDialogResponse new_authorized (string username, string password)
- {
- return AuthorizeDialogResponse ()
- {
- username = username,
- password = password,
- success = true,
- };
- }
-}
+/*
+ * Copyright (C) 2009-2017 Canonical Ltd.
+ * Author: Robert Ancell ,
+ * Eduard Gotwig
+ *
+ * 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.
+ */
+
+[GtkTemplate (ui = "/org/gnome/SimpleScan/ui/authorize-dialog.ui")]
+private class AuthorizeDialog : Gtk.Window
+{
+ [GtkChild]
+ private unowned Adw.PreferencesGroup preferences_group;
+ [GtkChild]
+ private unowned Adw.EntryRow username_entry;
+ [GtkChild]
+ private unowned Adw.PasswordEntryRow password_entry;
+
+ public signal void authorized (AuthorizeDialogResponse res);
+
+ public AuthorizeDialog (Gtk.Window parent, string title)
+ {
+ preferences_group.set_title (title);
+ set_transient_for (parent);
+ }
+
+ public string get_username ()
+ {
+ return username_entry.text;
+ }
+
+ public string get_password ()
+ {
+ return password_entry.text;
+ }
+
+ [GtkCallback]
+ private void authorize_button_cb ()
+ {
+ authorized (AuthorizeDialogResponse.new_authorized (get_username (), get_password ()));
+ }
+
+ [GtkCallback]
+ private void cancel_button_cb ()
+ {
+ authorized (AuthorizeDialogResponse.new_canceled ());
+ }
+
+ public async AuthorizeDialogResponse open()
+ {
+ SourceFunc callback = open.callback;
+
+ AuthorizeDialogResponse response = {};
+
+ authorized.connect ((res) =>
+ {
+ response = res;
+ callback ();
+ });
+
+ present ();
+ yield;
+ close ();
+
+ return response;
+ }
+}
+
+public struct AuthorizeDialogResponse
+{
+ public string username;
+ public string password;
+ public bool success;
+
+ public static AuthorizeDialogResponse new_canceled ()
+ {
+ return AuthorizeDialogResponse ()
+ {
+ success = false,
+ };
+ }
+
+ public static AuthorizeDialogResponse new_authorized (string username, string password)
+ {
+ return AuthorizeDialogResponse ()
+ {
+ username = username,
+ password = password,
+ success = true,
+ };
+ }
+}
diff --git a/src/autosave-manager.vala b/src/autosave-manager.vala
index 31fd3280be890084f99672a392f7b3d7d1cd5fc4..f4db50b730e9343172c770fa46611e0d9d692902 100644
--- a/src/autosave-manager.vala
+++ b/src/autosave-manager.vala
@@ -1,375 +1,450 @@
-/*
- * Copyright (C) 2011 Timo Kluck
- * Authors: Timo Kluck
- * 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 class AutosaveManager
-{
- private static string AUTOSAVE_DIR = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "autosaves");
- private static string AUTOSAVE_FILENAME = "autosave.book";
- private static string AUTOSAVE_PATH = Path.build_filename (AUTOSAVE_DIR, AUTOSAVE_FILENAME);
-
- private uint update_timeout = 0;
-
- private HashTable page_filenames;
-
- private Book book_ = null;
- public Book book
- {
- get
- {
- return book_;
- }
- set
- {
- if (book_ != null)
- {
- for (var i = 0; i < book_.n_pages; i++)
- {
- var page = book_.get_page (i);
- on_page_removed (page);
- }
- book_.page_added.disconnect (on_page_added);
- book_.page_removed.disconnect (on_page_removed);
- book_.reordered.disconnect (on_changed);
- book_.cleared.disconnect (on_cleared);
- }
- book_ = value;
- book_.page_added.connect (on_page_added);
- book_.page_removed.connect (on_page_removed);
- book_.reordered.connect (on_changed);
- book_.cleared.connect (on_cleared);
- for (var i = 0; i < book_.n_pages; i++)
- {
- var page = book_.get_page (i);
- on_page_added (page);
- }
- }
- }
-
- public AutosaveManager ()
- {
- page_filenames = new HashTable (direct_hash, direct_equal);
- }
-
- public bool exists ()
- {
- var file = File.new_for_path (AUTOSAVE_PATH);
- return file.query_exists ();
- }
-
- public void load ()
- {
- debug ("Loading autosave information");
-
- book.clear ();
- page_filenames.remove_all ();
-
- var file = new KeyFile ();
- try
- {
- file.load_from_file (AUTOSAVE_PATH, KeyFileFlags.NONE);
- }
- catch (Error e)
- {
- if (!(e is FileError.NOENT))
- warning ("Could not load autosave information; not restoring any autosaves: %s", e.message);
- return;
- }
- var pages = get_value (file, "simple-scan", "pages");
- foreach (var page_name in pages.split (" "))
- {
- debug ("Loading automatically saved page %s", page_name);
-
- var scan_width = get_integer (file, page_name, "scan-width");
- var scan_height = get_integer (file, page_name, "scan-height");
- var rowstride = get_integer (file, page_name, "rowstride");
- var n_channels = get_integer (file, page_name, "n-channels");
- var depth = get_integer (file, page_name, "depth");
- var dpi = get_integer (file, page_name, "dpi");
- var scan_direction_name = get_value (file, page_name, "scan-direction");
- ScanDirection scan_direction = ScanDirection.TOP_TO_BOTTOM;
- switch (scan_direction_name)
- {
- case "TOP_TO_BOTTOM":
- scan_direction = ScanDirection.TOP_TO_BOTTOM;
- break;
- case "LEFT_TO_RIGHT":
- scan_direction = ScanDirection.LEFT_TO_RIGHT;
- break;
- case "BOTTOM_TO_TOP":
- scan_direction = ScanDirection.BOTTOM_TO_TOP;
- break;
- case "RIGHT_TO_LEFT":
- scan_direction = ScanDirection.RIGHT_TO_LEFT;
- break;
- }
- var color_profile = get_value (file, page_name, "color-profile");
- if (color_profile == "")
- color_profile = null;
- var pixels_filename = get_value (file, page_name, "pixels-filename");
- var has_crop = get_boolean (file, page_name, "has-crop");
- var crop_name = get_value (file, page_name, "crop-name");
-
- if (crop_name == "")
- {
- // If it has no crop name but has crop it probably means that it is a custom crop
- if (has_crop)
- crop_name = "custom";
- else
- crop_name = null;
- }
-
- var crop_x = get_integer (file, page_name, "crop-x");
- var crop_y = get_integer (file, page_name, "crop-y");
- var crop_width = get_integer (file, page_name, "crop-width");
- var crop_height = get_integer (file, page_name, "crop-height");
-
- uchar[]? pixels = null;
- if (pixels_filename != "")
- {
- var path = Path.build_filename (AUTOSAVE_DIR, pixels_filename);
- var f = File.new_for_path (path);
- try
- {
- f.load_contents (null, out pixels, null);
- }
- catch (Error e)
- {
- warning ("Failed to load pixel information");
- continue;
- }
- }
-
- var page = new Page.from_data (scan_width,
- scan_height,
- rowstride,
- n_channels,
- depth,
- dpi,
- scan_direction,
- color_profile,
- pixels,
- has_crop,
- crop_name,
- crop_x,
- crop_y,
- crop_width,
- crop_height);
- page_filenames.insert (page, pixels_filename);
- book.append_page (page);
- }
- }
-
- private string get_value (KeyFile file, string group_name, string key, string default = "")
- {
- try
- {
- return file.get_value (group_name, key);
- }
- catch (Error e)
- {
- return default;
- }
- }
-
- private int get_integer (KeyFile file, string group_name, string key, int default = 0)
- {
- try
- {
- return file.get_integer (group_name, key);
- }
- catch (Error e)
- {
- return default;
- }
- }
-
- private bool get_boolean (KeyFile file, string group_name, string key, bool default = false)
- {
- try
- {
- return file.get_boolean (group_name, key);
- }
- catch (Error e)
- {
- return default;
- }
- }
-
- public void cleanup ()
- {
- debug ("Deleting autosave records");
-
- if (update_timeout > 0)
- Source.remove (update_timeout);
- update_timeout = 0;
-
- Dir dir;
- try
- {
- dir = Dir.open (AUTOSAVE_DIR);
- }
- catch (Error e)
- {
- warning ("Failed to delete autosaves: %s", e.message);
- return;
- }
-
- while (true)
- {
- var filename = dir.read_name ();
- if (filename == null)
- break;
- var path = Path.build_filename (AUTOSAVE_DIR, filename);
- FileUtils.unlink (path);
- }
- }
-
- public void on_page_added (Page page)
- {
- page.scan_finished.connect (on_scan_finished);
- page.crop_changed.connect (on_changed);
- }
-
- public void on_page_removed (Page page)
- {
- page.scan_finished.disconnect (on_scan_finished);
- page.crop_changed.disconnect (on_changed);
-
- var filename = page_filenames.lookup (page);
- if (filename != null)
- FileUtils.unlink (filename);
- page_filenames.remove (page);
- }
-
- public void on_scan_finished (Page page)
- {
- save_pixels (page);
- save (false);
- }
-
- public void on_changed ()
- {
- save ();
- }
-
- public void on_cleared ()
- {
- page_filenames.remove_all ();
- save ();
- }
-
- private void save (bool do_timeout = true)
- {
- if (update_timeout == 0 && do_timeout)
- debug ("Waiting to autosave...");
-
- /* Cancel existing timeout */
- if (update_timeout > 0)
- Source.remove (update_timeout);
- update_timeout = 0;
-
- if (do_timeout)
- {
- update_timeout = Timeout.add (100, () =>
- {
- real_save ();
- update_timeout = 0;
- return false;
- });
- }
- else
- real_save();
- }
-
- private void real_save ()
- {
- debug ("Autosaving book information");
-
- var file = new KeyFile ();
- var page_names = "";
- for (var i = 0; i < book.n_pages; i++)
- {
- var page = book.get_page (i);
-
- /* Skip empty pages */
- if (!page.has_data)
- continue;
-
- var page_name = "page-%d".printf (i);
- if (page_names != "")
- page_names += " ";
- page_names += page_name;
-
- debug ("Autosaving page %s", page_name);
-
- file.set_integer (page_name, "scan-width", page.scan_width);
- file.set_integer (page_name, "scan-height", page.scan_height);
- file.set_integer (page_name, "rowstride", page.rowstride);
- file.set_integer (page_name, "n-channels", page.n_channels);
- file.set_integer (page_name, "depth", page.depth);
- file.set_integer (page_name, "dpi", page.dpi);
- switch (page.scan_direction)
- {
- case ScanDirection.TOP_TO_BOTTOM:
- file.set_value (page_name, "scan-direction", "TOP_TO_BOTTOM");
- break;
- case ScanDirection.LEFT_TO_RIGHT:
- file.set_value (page_name, "scan-direction", "LEFT_TO_RIGHT");
- break;
- case ScanDirection.BOTTOM_TO_TOP:
- file.set_value (page_name, "scan-direction", "BOTTOM_TO_TOP");
- break;
- case ScanDirection.RIGHT_TO_LEFT:
- file.set_value (page_name, "scan-direction", "RIGHT_TO_LEFT");
- break;
- }
- file.set_value (page_name, "color-profile", page.color_profile ?? "");
- file.set_value (page_name, "pixels-filename", page_filenames.lookup (page) ?? "");
- file.set_boolean (page_name, "has-crop", page.has_crop);
- file.set_value (page_name, "crop-name", page.crop_name ?? "");
- file.set_integer (page_name, "crop-x", page.crop_x);
- file.set_integer (page_name, "crop-y", page.crop_y);
- file.set_integer (page_name, "crop-width", page.crop_width);
- file.set_integer (page_name, "crop-height", page.crop_height);
- }
- file.set_value ("simple-scan", "pages", page_names);
-
- try
- {
- DirUtils.create_with_parents (AUTOSAVE_DIR, 0700);
- FileUtils.set_contents (AUTOSAVE_PATH, file.to_data ());
- }
- catch (Error e)
- {
- warning ("Failed to write autosave: %s", e.message);
- }
- }
-
- private void save_pixels (Page page)
- {
- var filename = "%u.pixels".printf (direct_hash (page));
- var path = Path.build_filename (AUTOSAVE_DIR, filename);
- page_filenames.insert (page, filename);
-
- debug ("Autosaving page pixels to %s", path);
-
- var file = File.new_for_path (path);
- try
- {
- file.replace_contents (page.get_pixels (), null, false, FileCreateFlags.NONE, null);
- }
- catch (Error e)
- {
- warning ("Failed to autosave page contents: %s", e.message);
- }
- }
-}
+/*
+ * Copyright (C) 2011 Timo Kluck
+ * Authors: Timo Kluck
+ * 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 class AutosaveManager
+{
+ private static string AUTOSAVE_DIR = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "autosaves");
+ private static string AUTOSAVE_FILENAME = "autosave.book";
+ private static string AUTOSAVE_PATH = Path.build_filename (AUTOSAVE_DIR, AUTOSAVE_FILENAME);
+
+ private uint update_timeout = 0;
+ private ThreadPool load_page_pixels_pool;
+
+ private Book book_ = null;
+ public Book book
+ {
+ get
+ {
+ return book_;
+ }
+ set
+ {
+ if (book_ != null)
+ {
+ for (var i = 0; i < book_.n_pages; i++)
+ {
+ var page = book_.get_page (i);
+ on_page_removed (page);
+ }
+ book_.page_added.disconnect (on_page_added);
+ book_.page_removed.disconnect (on_page_removed);
+ book_.reordered.disconnect (on_changed);
+ book_.cleared.disconnect (on_cleared);
+ }
+ book_ = value;
+ book_.page_added.connect (on_page_added);
+ book_.page_removed.connect (on_page_removed);
+ book_.reordered.connect (on_changed);
+ book_.cleared.connect (on_cleared);
+ for (var i = 0; i < book_.n_pages; i++)
+ {
+ var page = book_.get_page (i);
+ on_page_added (page);
+ }
+ }
+ }
+
+ public AutosaveManager ()
+ {
+ try
+ {
+ load_page_pixels_pool = new ThreadPool.with_owned_data (
+ (task) => { load_page_pixels_worker (task); },
+ int.max((int) GLib.get_num_processors () - 1, 1),
+ true
+ );
+ }
+ catch (Error e)
+ {
+ warning ("Failed to create thread pool: %s\n", e.message);
+ }
+
+ }
+
+ public bool exists ()
+ {
+ var file = File.new_for_path (AUTOSAVE_PATH);
+ return file.query_exists ();
+ }
+
+ public void load ()
+ {
+ debug ("Loading autosave information");
+
+ book.clear ();
+
+ var file = new KeyFile ();
+ try
+ {
+ file.load_from_file (AUTOSAVE_PATH, KeyFileFlags.NONE);
+ }
+ catch (Error e)
+ {
+ if (!(e is FileError.NOENT))
+ warning ("Could not load autosave information; not restoring any autosaves: %s", e.message);
+ return;
+ }
+ var pages = get_value (file, "simple-scan", "pages");
+ foreach (var page_name in pages.split (" "))
+ {
+ debug ("Loading automatically saved page %s", page_name);
+
+ var scan_width = get_integer (file, page_name, "scan-width");
+ var scan_height = get_integer (file, page_name, "scan-height");
+ var page_size = get_value (file, page_name, "page-size");
+ var rowstride = get_integer (file, page_name, "rowstride");
+ var n_channels = get_integer (file, page_name, "n-channels");
+ var depth = get_integer (file, page_name, "depth");
+ var dpi = get_integer (file, page_name, "dpi");
+ var scan_direction_name = get_value (file, page_name, "scan-direction");
+ ScanDirection scan_direction = scan_direction_from_string (scan_direction_name);
+ var color_profile = get_value (file, page_name, "color-profile");
+ if (color_profile == "")
+ color_profile = null;
+ var raw_backup_path = get_value (file, page_name, "raw-backup-path");
+ var fullres_pixels_path = get_value (file, page_name, "fullres-pixels-path");
+ var original_depth = get_integer (file, page_name, "original-depth");
+ var original_color_profile = get_value (file, page_name, "original-color-profile");
+ if (original_color_profile == "")
+ original_color_profile = null;
+ var original_rowstride = get_integer (file, page_name, "original-rowstride");
+ var original_n_channels = get_integer (file, page_name, "original-n-channels");
+ var is_filtered = get_boolean (file, page_name, "is-filtered");
+ var has_crop = get_boolean (file, page_name, "has-crop");
+ var has_auto_rotate = get_boolean (file, page_name, "has-auto-rotate");
+ var is_deleted = get_boolean (file, page_name, "is-deleted");
+ var crop_name = get_value (file, page_name, "crop-name");
+
+ if (crop_name == "")
+ {
+ // If it has no crop name but has crop it probably means that it is a custom crop
+ if (has_crop)
+ crop_name = "custom";
+ else
+ crop_name = null;
+ }
+
+ var crop_x = get_integer (file, page_name, "crop-x");
+ var crop_y = get_integer (file, page_name, "crop-y");
+ var crop_width = get_integer (file, page_name, "crop-width");
+ var crop_height = get_integer (file, page_name, "crop-height");
+
+ var page = new Page.from_data (scan_width,
+ scan_height,
+ page_size,
+ rowstride,
+ n_channels,
+ depth,
+ dpi,
+ scan_direction,
+ color_profile,
+ null,
+ raw_backup_path,
+ fullres_pixels_path,
+ 0,
+ 0,
+ 0,
+ has_auto_rotate,
+ has_crop,
+ crop_name,
+ crop_x,
+ crop_y,
+ crop_width,
+ crop_height,
+ is_deleted,
+ original_depth,
+ original_color_profile,
+ original_rowstride,
+ original_n_channels,
+ is_filtered
+ );
+
+ book.append_page (page);
+ try
+ {
+ load_page_pixels_pool.add (new PageUpdateTask (page));
+ }
+ catch (Error e)
+ {
+ warning ("Failed to add page update task to thread pool: %s\n", e.message);
+ }
+
+ }
+ }
+
+ private void load_page_pixels_worker (PageUpdateTask? task)
+ {
+ /* first update just loads full resolution page */
+ task.page.update_pixels (true, false);
+ /* ... second scales it down */
+ task.page.update_pixels (false, true);
+ }
+
+ private class PageUpdateTask
+ {
+ public Page page;
+
+ public PageUpdateTask (Page page)
+ {
+ this.page = page;
+ }
+ }
+
+ private static string scan_direction_to_string (ScanDirection direction)
+ {
+ switch (direction)
+ {
+ case ScanDirection.LEFT_TO_RIGHT:
+ return "LEFT_TO_RIGHT";
+ case ScanDirection.BOTTOM_TO_TOP:
+ return "BOTTOM_TO_TOP";
+ case ScanDirection.RIGHT_TO_LEFT:
+ return "RIGHT_TO_LEFT";
+ default:
+ return "TOP_TO_BOTTOM";
+ }
+ }
+
+ private static ScanDirection scan_direction_from_string (string name)
+ {
+ switch (name)
+ {
+ case "LEFT_TO_RIGHT":
+ return ScanDirection.LEFT_TO_RIGHT;
+ case "BOTTOM_TO_TOP":
+ return ScanDirection.BOTTOM_TO_TOP;
+ case "RIGHT_TO_LEFT":
+ return ScanDirection.RIGHT_TO_LEFT;
+ default:
+ return ScanDirection.TOP_TO_BOTTOM;
+ }
+ }
+
+ private string get_value (KeyFile file, string group_name, string key, string default = "")
+ {
+ try
+ {
+ return file.get_value (group_name, key);
+ }
+ catch (Error e)
+ {
+ return default;
+ }
+ }
+
+ private int get_integer (KeyFile file, string group_name, string key, int default = 0)
+ {
+ try
+ {
+ return file.get_integer (group_name, key);
+ }
+ catch (Error e)
+ {
+ return default;
+ }
+ }
+
+ private bool get_boolean (KeyFile file, string group_name, string key, bool default = false)
+ {
+ try
+ {
+ return file.get_boolean (group_name, key);
+ }
+ catch (Error e)
+ {
+ return default;
+ }
+ }
+
+ public void cleanup ()
+ {
+ debug ("Deleting autosave records");
+
+ if (update_timeout > 0)
+ Source.remove (update_timeout);
+ update_timeout = 0;
+
+ Dir dir;
+ try
+ {
+ dir = Dir.open (AUTOSAVE_DIR);
+ }
+ catch (Error e)
+ {
+ warning ("Failed to delete autosaves: %s", e.message);
+ return;
+ }
+
+ while (true)
+ {
+ var filename = dir.read_name ();
+ if (filename == null)
+ break;
+ var path = Path.build_filename (AUTOSAVE_DIR, filename);
+ FileUtils.unlink (path);
+ }
+ }
+
+ public void on_page_added (Page page)
+ {
+ page.scan_finished.connect (on_scan_finished);
+ page.filter_changed.connect (on_filter_changed);
+ page.crop_changed.connect (on_changed);
+ page.is_deleted_changed.connect (on_changed);
+ }
+
+ public void on_page_removed (Page page)
+ {
+ page.scan_finished.disconnect (on_scan_finished);
+ page.filter_changed.disconnect (on_filter_changed);
+ page.crop_changed.disconnect (on_changed);
+
+ bool fullres_pixels_copied = false;
+ bool raw_backup_copied = false;
+
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if (book_page != null)
+ {
+ if (page.fullres_pixels_path == book_page.fullres_pixels_path)
+ fullres_pixels_copied = true;
+ if (page.raw_backup_path == book_page.raw_backup_path)
+ raw_backup_copied = true;
+ }
+ }
+
+ if (page.fullres_pixels_path != "" && !fullres_pixels_copied)
+ FileUtils.unlink (page.fullres_pixels_path);
+
+ if (page.raw_backup_path != "" && !raw_backup_copied)
+ FileUtils.unlink (page.raw_backup_path);
+ }
+
+ public void on_scan_finished (Page page)
+ {
+ save (false);
+ }
+
+ public void on_filter_changed (Page page)
+ {
+ save (false);
+ }
+
+ public void on_changed ()
+ {
+ save ();
+ }
+
+ public void on_cleared ()
+ {
+ save ();
+ }
+
+ private void save (bool do_timeout = true)
+ {
+ Idle.add (() => {
+ schedule_save (do_timeout);
+ return false;
+ });
+ }
+
+ private void schedule_save (bool do_timeout = true)
+ {
+ if (update_timeout == 0 && do_timeout)
+ debug ("Waiting to autosave...");
+
+ /* Cancel existing timeout */
+ if (update_timeout > 0)
+ Source.remove (update_timeout);
+ update_timeout = 0;
+
+ if (do_timeout)
+ {
+ update_timeout = Timeout.add (100, () =>
+ {
+ real_save ();
+ update_timeout = 0;
+ return false;
+ });
+ }
+ else
+ {
+ update_timeout = Idle.add (() =>
+ {
+ real_save ();
+ update_timeout = 0;
+ return false;
+ });
+ }
+ }
+
+ private void real_save ()
+ {
+ debug ("Autosaving book information");
+
+ var file = new KeyFile ();
+ var page_names = "";
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+
+ /* Skip empty pages */
+ if (!page.has_data)
+ continue;
+
+ var page_name = "page-%d".printf (i);
+ if (page_names != "")
+ page_names += " ";
+ page_names += page_name;
+
+ debug ("Autosaving page %s", page_name);
+
+ file.set_integer (page_name, "scan-width", page.scan_width);
+ file.set_integer (page_name, "scan-height", page.scan_height);
+ file.set_string (page_name, "page-size", page.page_size);
+ file.set_integer (page_name, "rowstride", page.rowstride);
+ file.set_integer (page_name, "n-channels", page.n_channels);
+ file.set_integer (page_name, "depth", page.depth);
+ file.set_integer (page_name, "dpi", page.dpi);
+ file.set_value (page_name, "scan-direction", scan_direction_to_string (page.scan_direction));
+ file.set_value (page_name, "color-profile", page.color_profile ?? "");
+ file.set_value (page_name, "raw-backup-path", page.raw_backup_path);
+ file.set_value (page_name, "fullres-pixels-path", page.fullres_pixels_path);
+ file.set_integer (page_name, "original-depth", page.original_depth);
+ file.set_value (page_name, "original-color-profile", page.original_color_profile ?? "");
+ file.set_integer (page_name, "original-rowstride", page.original_rowstride);
+ file.set_integer (page_name, "original-n-channels", page.original_n_channels);
+ file.set_boolean (page_name, "is-filtered", page.is_filtered);
+ file.set_boolean (page_name, "has-auto-rotate", page.has_auto_rotate);
+ file.set_boolean (page_name, "has-crop", page.has_crop);
+ file.set_boolean (page_name, "is-deleted", page.is_deleted);
+ file.set_value (page_name, "crop-name", page.crop_name ?? "");
+ file.set_integer (page_name, "crop-x", page.crop_x);
+ file.set_integer (page_name, "crop-y", page.crop_y);
+ file.set_integer (page_name, "crop-width", page.crop_width);
+ file.set_integer (page_name, "crop-height", page.crop_height);
+ }
+ file.set_value ("simple-scan", "pages", page_names);
+
+ try
+ {
+ DirUtils.create_with_parents (AUTOSAVE_DIR, 0700);
+ FileUtils.set_contents (AUTOSAVE_PATH, file.to_data ());
+ }
+ catch (Error e)
+ {
+ warning ("Failed to write autosave: %s", e.message);
+ }
+ }
+
+}
diff --git a/src/book-view.vala b/src/book-view.vala
index 59ddc9dcc4e588fbf33ac2a731b44ff1e6967804..24fbaa97be8997cdf2e79fb5540d924a2bf26244 100644
--- a/src/book-view.vala
+++ b/src/book-view.vala
@@ -1,642 +1,2133 @@
-/*
- * 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.
- */
-
-// FIXME: When scrolling, copy existing render sideways?
-// FIXME: Only render pages that change and only the part that changed
-
-public class BookView : Gtk.Box
-{
- /* Book being rendered */
- public Book book { get; private set; }
- private HashTable page_data;
-
- /* True if the view needs to be laid out again */
- private bool need_layout;
- private bool laying_out;
- private bool show_selected_page;
-
- /* Currently selected page */
- private PageView? selected_page_view = null;
- public Page? selected_page
- {
- get
- {
- if (selected_page_view != null)
- return selected_page_view.page;
- else
- return null;
- }
- set
- {
- if (selected_page == value)
- return;
-
- if (value != null)
- select_page_view (page_data.lookup (value));
- else
- select_page_view (null);
- }
- }
-
- /* Widget being rendered to */
- private Gtk.ScrolledWindow scrolled_window;
- private Gtk.DrawingArea drawing_area;
-
- private new string cursor;
-
- private Gtk.EventControllerMotion motion_controller;
- private Gtk.EventControllerScroll cursor_scroll_controller;
- private Gtk.EventControllerKey key_controller;
- private Gtk.GestureClick primary_click_gesture;
- private Gtk.GestureClick secondary_click_gesture;
- private Gtk.EventControllerFocus focus_controller;
-
-
- public signal void page_selected (Page? page);
- public signal void show_page (Page page);
- public signal void show_menu (Gtk.Widget from, double x, double y);
-
- public int x_offset
- {
- get { return (int) scrolled_window.get_hadjustment ().get_value (); }
- set { scrolled_window.get_hadjustment ().set_value ((double) value); }
- }
-
- public BookView (Book book)
- {
- GLib.Object (orientation: Gtk.Orientation.VERTICAL);
- this.book = book;
-
- /* Load existing pages */
- for (var i = 0; i < book.n_pages; i++)
- {
- Page page = book.get_page (i);
- add_cb (book, page);
- }
-
- selected_page = book.get_page (0);
-
- /* Watch for new pages */
- book.page_added.connect (add_cb);
- book.page_removed.connect (remove_cb);
- book.reordered.connect (reorder_cb);
- book.cleared.connect (clear_cb);
-
- need_layout = true;
- page_data = new HashTable (direct_hash, direct_equal);
- cursor = "default";
-
- drawing_area = new Gtk.DrawingArea ();
- drawing_area.set_size_request (200, 100);
- drawing_area.can_focus = true;
- drawing_area.focusable = true;
- drawing_area.vexpand = true;
- drawing_area.set_draw_func(draw_cb);
-
- // Use GtkScrolledWindow for automatic scrollbar management
- scrolled_window = new Gtk.ScrolledWindow();
- scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER); // Horizontal only
- scrolled_window.set_child(drawing_area);
- scrolled_window.hexpand = true;
- scrolled_window.vexpand = true;
-
- append(scrolled_window);
-
- drawing_area.resize.connect (drawing_area_resize_cb);
-
- motion_controller = new Gtk.EventControllerMotion ();
- motion_controller.motion.connect (motion_cb);
- drawing_area.add_controller(motion_controller);
-
- cursor_scroll_controller = new Gtk.EventControllerScroll (
- Gtk.EventControllerScrollFlags.BOTH_AXES
- | Gtk.EventControllerScrollFlags.DISCRETE
- );
- cursor_scroll_controller.scroll.connect (cursor_scroll_cb);
- drawing_area.add_controller(cursor_scroll_controller);
-
- key_controller = new Gtk.EventControllerKey ();
- key_controller.key_pressed.connect (key_cb);
- drawing_area.add_controller(key_controller);
-
- primary_click_gesture = new Gtk.GestureClick ();
- primary_click_gesture.button = Gdk.BUTTON_PRIMARY;
- primary_click_gesture.pressed.connect (primary_pressed_cb);
- primary_click_gesture.released.connect (primary_released_cb);
- drawing_area.add_controller(primary_click_gesture);
-
- secondary_click_gesture = new Gtk.GestureClick ();
- secondary_click_gesture.button = Gdk.BUTTON_SECONDARY;
- secondary_click_gesture.pressed.connect (secondary_pressed_cb);
- secondary_click_gesture.released.connect (secondary_released_cb);
- drawing_area.add_controller(secondary_click_gesture);
-
- focus_controller = new Gtk.EventControllerFocus ();
- focus_controller.enter.connect_after (focus_cb);
- focus_controller.leave.connect_after (focus_cb);
- drawing_area.add_controller(focus_controller);
-
- scrolled_window.get_hadjustment ().value_changed.connect (scroll_cb);
-
- drawing_area.visible = true;
- }
-
- ~BookView ()
- {
- book.page_added.disconnect (add_cb);
- book.page_removed.disconnect (remove_cb);
- book.reordered.disconnect (reorder_cb);
- book.cleared.disconnect (clear_cb);
- drawing_area.resize.disconnect (drawing_area_resize_cb);
- motion_controller.motion.disconnect (motion_cb);
- cursor_scroll_controller.scroll.disconnect (cursor_scroll_cb);
- key_controller.key_pressed.disconnect (key_cb);
- primary_click_gesture.pressed.disconnect (primary_pressed_cb);
- primary_click_gesture.released.disconnect (primary_released_cb);
- secondary_click_gesture.pressed.disconnect (secondary_pressed_cb);
- secondary_click_gesture.released.disconnect (secondary_released_cb);
- focus_controller.enter.disconnect (focus_cb);
- focus_controller.leave.disconnect (focus_cb);
- scrolled_window.get_hadjustment ().value_changed.disconnect (scroll_cb);
- }
-
- private PageView get_nth_page (int n)
- {
- Page page = book.get_page (n);
- return page_data.lookup (page);
- }
-
- private PageView get_next_page (PageView page)
- {
- for (var i = 0; ; i++)
- {
- var p = book.get_page (i);
- if (p == null)
- break;
- if (p == page.page)
- {
- p = book.get_page (i + 1);
- if (p != null)
- return page_data.lookup (p);
- }
- }
-
- return page;
- }
-
- private PageView get_prev_page (PageView page)
- {
- var prev_page = page;
- for (var i = 0; ; i++)
- {
- var p = book.get_page (i);
- if (p == null)
- break;
- if (p == page.page)
- return prev_page;
- prev_page = page_data.lookup (p);
- }
-
- return page;
- }
-
- private void page_view_changed_cb (PageView page)
- {
- redraw ();
- }
-
- private void page_view_size_changed_cb (PageView page)
- {
- need_layout = true;
- redraw ();
- }
-
- private void add_cb (Book book, Page page)
- {
- var page_view = new PageView (page);
- page_view.changed.connect (page_view_changed_cb);
- page_view.size_changed.connect (page_view_size_changed_cb);
- page_data.insert (page, page_view);
- need_layout = true;
- redraw ();
- }
-
- private void set_selected_page_view (PageView? page)
- {
- /* Deselect existing page if changed */
- if (selected_page_view != null && page != selected_page_view)
- selected_page_view.selected = true;
-
- selected_page_view = page;
- redraw();
- if (selected_page_view == null)
- return;
-
- /* Select new page if widget has focus */
- if (!drawing_area.has_focus)
- selected_page_view.selected = false;
- else
- selected_page_view.selected = true;
- }
-
- private void show_page_view (PageView? page)
- {
- if (page == null)
- return;
-
- int allocation_width = drawing_area.get_allocated_width();
- var left_edge = page.x_offset;
- var right_edge = page.x_offset + page.width;
-
- if (left_edge - x_offset < 0)
- x_offset = left_edge;
- else if (right_edge - x_offset > allocation_width)
- x_offset = right_edge - allocation_width;
- }
-
- private void select_page_view (PageView? page)
- {
- Page? p = null;
-
- if (selected_page_view == page)
- return;
-
- set_selected_page_view (page);
-
- if (need_layout)
- show_selected_page = true;
- else
- show_page_view (page);
-
- if (page != null)
- p = page.page;
- page_selected (p);
- }
-
- private void remove_cb (Book book, Page page)
- {
- PageView new_selection = selected_page_view;
-
- /* Select previous page or next if removing the selected page */
- if (page == selected_page)
- {
- new_selection = get_prev_page (selected_page_view);
- if (new_selection == selected_page_view)
- new_selection = get_next_page (selected_page_view);
- selected_page_view = null;
- }
-
- var page_view = page_data.lookup (page);
- page_view.changed.disconnect (page_view_changed_cb);
- page_view.size_changed.disconnect (page_view_size_changed_cb);
- page_data.remove (page);
-
- select_page_view (new_selection);
-
- need_layout = true;
- redraw ();
- }
-
- private void reorder_cb (Book book)
- {
- need_layout = true;
- redraw ();
- }
-
- private void clear_cb (Book book)
- {
- page_data.remove_all ();
- selected_page_view = null;
- page_selected (null);
- need_layout = true;
- redraw ();
- }
-
- public void drawing_area_resize_cb ()
- {
- need_layout = true;
- layout ();
- }
-
- private void layout_into (int width, int height, out int book_width, out int book_height)
- {
- var pages = new List ();
- for (var i = 0; i < book.n_pages; i++)
- pages.append (get_nth_page (i));
-
- /* Get maximum page resolution */
- int max_dpi = 0;
- foreach (var page in pages)
- {
- var p = page.page;
- if (p.dpi > max_dpi)
- max_dpi = p.dpi;
- }
-
- /* Get area required to fit all pages */
- int max_width = 0, max_height = 0;
- foreach (var page in pages)
- {
- var p = page.page;
- var w = p.width;
- var h = p.height;
-
- /* Scale to the same DPI */
- w = (int) ((double)w * max_dpi / p.dpi + 0.5);
- h = (int) ((double)h * max_dpi / p.dpi + 0.5);
-
- if (w > max_width)
- max_width = w;
- if (h > max_height)
- max_height = h;
- }
-
- var aspect = (double)width / height;
- var max_aspect = (double)max_width / max_height;
-
- /* Get total dimensions of all pages */
- int spacing = 12;
- book_width = 0;
- book_height = 0;
- foreach (var page in pages)
- {
- var p = page.page;
-
- /* NOTE: Using double to avoid overflow for large images */
- if (max_aspect > aspect)
- {
- /* Set width scaled on DPI and maximum width */
- int w = (int) ((double)p.width * max_dpi * width / (p.dpi * max_width));
- page.width = w;
- }
- else
- {
- /* Set height scaled on DPI and maximum height */
- int h = (int) ((double)p.height * max_dpi * height / (p.dpi * max_height));
- page.height = h;
- }
-
- var h = page.height;
- if (h > book_height)
- book_height = h;
- book_width += page.width + spacing;
- }
- if (pages != null)
- book_width -= spacing;
-
- int x_offset_local = 0;
- foreach (var page in pages)
- {
- /* Layout pages left to right */
- page.x_offset = x_offset_local;
- x_offset_local += page.width + spacing;
-
- /* Centre page vertically */
- page.y_offset = (height - page.height) / 2;
- }
- }
-
- private void layout ()
- {
- if (!need_layout)
- return;
-
- laying_out = true;
-
- int width = drawing_area.get_allocated_width();
- int height = this.get_allocated_height();
-
- int book_width, book_height;
- layout_into (width, height, out book_width, out book_height);
-
- drawing_area.set_size_request(book_width, -1);
- drawing_area.queue_resize();
- if (show_selected_page)
- show_page_view (selected_page_view);
-
- need_layout = false;
- show_selected_page = false;
- laying_out = false;
- }
-
- public void draw_cb (Gtk.DrawingArea drawing_area, Cairo.Context context, int width, int height)
- {
- layout ();
-
- var scroll_x = (double) x_offset;
- var scroll_right = scroll_x + scrolled_window.get_allocated_width ();
-
- var pages = new List ();
- for (var i = 0; i < book.n_pages; i++)
- pages.append (get_nth_page (i));
-
- var ruler_color = get_style_context ().get_color ();
- Gdk.RGBA ruler_color_selected = {};
- ruler_color_selected.parse("#3584e4"); /* Gnome Blue 3 */
-
- /* Render each page */
- foreach (var page in pages)
- {
- var left_edge = page.x_offset;
- var right_edge = page.x_offset + page.width;
-
- /* Page not visible, don't render */
- if (right_edge < scroll_x || left_edge > scroll_right)
- continue;
-
- context.save ();
- page.render (context, page == selected_page_view ? ruler_color_selected : ruler_color);
- context.restore ();
-
- if (page.selected)
- drawing_area.get_style_context ().render_focus (context,
- page.x_offset,
- page.y_offset,
- page.width,
- page.height);
- }
- }
-
- private PageView? get_page_at (int x, int y, out int x_, out int y_)
- {
- x_ = y_ = 0;
- for (var i = 0; i < book.n_pages; i++)
- {
- var page = get_nth_page (i);
- var left = page.x_offset;
- var right = left + page.width;
- var top = page.y_offset;
- var bottom = top + page.height;
- if (x >= left && x <= right && y >= top && y <= bottom)
- {
- x_ = x - left;
- y_ = y - top;
- return page;
- }
- }
-
- return null;
- }
-
- private void primary_pressed_cb (Gtk.GestureClick controler, int n_press, double x, double y)
- {
- button_cb(controler, Gdk.BUTTON_PRIMARY, true, n_press, x, y);
- }
-
- private void primary_released_cb (Gtk.GestureClick controler, int n_press, double x, double y)
- {
- button_cb(controler, Gdk.BUTTON_PRIMARY, false, n_press, x, y);
- }
-
- private void secondary_pressed_cb (Gtk.GestureClick controler, int n_press, double x, double y)
- {
- button_cb(controler, Gdk.BUTTON_SECONDARY, true, n_press, x, y);
- }
-
- private void secondary_released_cb (Gtk.GestureClick controler, int n_press, double x, double y)
- {
- button_cb(controler, Gdk.BUTTON_SECONDARY, false, n_press, x, y);
- }
-
- private void button_cb (Gtk.GestureClick controler, int button, bool press, int n_press, double dx, double dy)
- {
- layout ();
-
- drawing_area.grab_focus ();
-
- int x = 0, y = 0;
- if (press)
- select_page_view (get_page_at ((int) dx, (int) dy, out x, out y));
-
- if (selected_page_view == null)
- return;
-
- /* Modify page */
- if (button == Gdk.BUTTON_PRIMARY)
- {
- if (press)
- selected_page_view.button_press (x, y);
- else if (press && n_press == 2)
- show_page (selected_page);
- else if (!press)
- selected_page_view.button_release (x, y);
- }
-
- /* Show pop-up menu on right click */
- if (button == Gdk.BUTTON_SECONDARY)
- show_menu (drawing_area, dx, dy);
- }
-
- private new void set_cursor (string cursor)
- {
- if (this.cursor == cursor)
- return;
- this.cursor = cursor;
-
- Gdk.Cursor c = new Gdk.Cursor.from_name (cursor, null);
- drawing_area.set_cursor (c);
- }
-
- private void motion_cb (Gtk.EventControllerMotion controler, double dx, double dy)
- {
- string cursor = "default";
-
- int event_x = (int) dx;
- int event_y = (int) dy;
-
- var event_state = controler.get_current_event_state();
-
- /* Dragging */
- if (selected_page_view != null && (event_state & Gdk.ModifierType.BUTTON1_MASK) != 0)
- {
- var x = (int) (event_x - selected_page_view.x_offset);
- var y = (int) (event_y - selected_page_view.y_offset);
- selected_page_view.motion (x, y);
- cursor = selected_page_view.cursor;
- }
- else
- {
- int x, y;
- var over_page = get_page_at ((int) (event_x), (int) event_y, out x, out y);
- if (over_page != null)
- {
- over_page.motion (x, y);
- cursor = over_page.cursor;
- }
- }
-
- set_cursor (cursor);
- }
-
- private bool key_cb (Gtk.EventControllerKey controler, uint keyval, uint keycode, Gdk.ModifierType state)
- {
- switch (keyval)
- {
- case Gdk.Key.Home:
- selected_page = book.get_page (0);
- return true;
- case Gdk.Key.Left:
- select_page_view (get_prev_page (selected_page_view));
- return true;
- case Gdk.Key.Right:
- select_page_view (get_next_page (selected_page_view));
- return true;
- case Gdk.Key.End:
- selected_page = book.get_page ((int) book.n_pages - 1);
- return true;
-
- default:
- return false;
- }
- }
-
- private void scroll_cb (Gtk.Adjustment adjustment)
- {
- if (!laying_out)
- redraw ();
- }
-
- private void focus_cb (Gtk.EventControllerFocus controler)
- {
- set_selected_page_view (selected_page_view);
- }
-
- private bool cursor_scroll_cb (Gtk.EventControllerScroll controller, double dx, double dy)
- {
- if (dx == 0 && dy == 0) {
- return false;
- }
- else if (dy >= 0 && dx >= 0) {
- // Down and/or right
- select_next_page();
- }
- else if (dy <= 0 && dx <= 0) {
- // Up and/or left
- select_prev_page();
- }
- return true;
- }
-
- public void redraw ()
- {
- drawing_area.queue_draw ();
- }
-
- public void select_next_page ()
- {
- select_page_view (get_next_page (selected_page_view));
- }
-
- public void select_prev_page ()
- {
- select_page_view (get_prev_page (selected_page_view));
- }
-}
+/*
+ * 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 class BookView : Gtk.Box
+{
+ /* Book being rendered */
+ public Book book { get; private set; }
+ private HashTable page_data;
+
+ /* True if the view needs to be laid out again */
+ private bool need_layout;
+ private bool laying_out;
+ private bool show_selected_page;
+
+ /* Currently selected page */
+ private PageView? selected_page_view = null;
+ private PageView? drag_page_view = null;
+ private bool drag_in_progress = false;
+ private bool drag_pending = false;
+ private int drag_start_x = 0;
+ private int drag_start_y = 0;
+ private int drag_cursor_x = 0;
+ private int drag_cursor_y = 0;
+ private PageView? drag_target_page = null;
+ private bool drag_target_left = false;
+ private bool drag_delete = false;
+ private const int drag_start_threshold = 48;
+ private Gdk.Texture? delete_cursor = null;
+
+ /* Eraser mode state */
+ private bool eraser_mode = false;
+ private bool eraser_active = false; /* true while LMB is held in eraser mode */
+ private int eraser_size_screen = 24; /* edge length of the eraser square in screen px */
+ private int eraser_cursor_x = 0; /* last cursor position in widget coords (for overlay) */
+ private int eraser_cursor_y = 0;
+ private Gdk.Texture? eraser_cursor_texture = null;
+ private Gtk.Button? eraser_button = null;
+
+ /* Deskew mode state */
+ private bool deskew_mode = false;
+ private bool deskew_have_first_point = false; /* true after the first click */
+ private double deskew_x0 = 0; /* first click position (widget coords) */
+ private double deskew_y0 = 0;
+ private double deskew_cursor_x = 0; /* current cursor position (widget coords) */
+ private double deskew_cursor_y = 0;
+ private Gtk.Button? deskew_button = null;
+
+ /* Box holding the per-page editing buttons (eraser, deskew). Only shown
+ in single-page view. */
+ private Gtk.Box? edit_button_box = null;
+
+ public Page? selected_page
+ {
+ get
+ {
+ if (selected_page_view != null)
+ return selected_page_view.page;
+ else
+ return null;
+ }
+ set
+ {
+ if (selected_page == value)
+ return;
+
+ if (value != null)
+ select_page_view (page_data.lookup (value));
+ else
+ select_page_view (null);
+ }
+ }
+
+ /* Widget being rendered to */
+ private Gtk.ScrolledWindow scrolled_window;
+ private SnapshotArea drawing_area;
+
+ private Gtk.Overlay overlay;
+
+ private Gtk.Box zoom_button_box;
+ private Gtk.Button toggle_grid_button;
+ public bool show_grid {public get; private set; default = true;}
+ public bool show_deleted_pages { public get; private set; default = true;}
+ private Gtk.Button zoom_in_button;
+ private Gtk.Button zoom_out_button;
+
+ private Gtk.Box move_button_box;
+ private Gtk.Button move_left_button;
+ private Gtk.Button move_right_button;
+
+ private new string cursor;
+
+ /* Grid layout properties */
+ private int page_label_height = 20; /* Height for page number label */
+ private int page_spacing = 12; /* Spacing between pages */
+ private double zoom_factor = 1.0; /* Zoom factor for page height */
+ private double zoom_step = Math.sqrt (2.0); /* Use sqrt2 if changing zoom so twice scroll doubles zoom factor */
+
+ private Gtk.EventControllerMotion motion_controller;
+ private Gtk.EventControllerScroll cursor_scroll_controller;
+ private Gtk.EventControllerKey key_controller;
+ private Gtk.GestureClick primary_click_gesture;
+ private Gtk.GestureClick secondary_click_gesture;
+ private Gtk.EventControllerFocus focus_controller;
+
+
+ public signal void page_selected (Page? page);
+ public signal void show_page (Page page);
+ public signal void show_menu (Gtk.Widget from, double x, double y);
+ public signal void scan_progress_changed (double progress);
+ public signal void move_page_with_undo (Page page, uint location);
+
+ /* Emitted when the user enters deskew mode, so the window can show a
+ one-time help toast explaining how the tool works. */
+ public signal void request_deskew_help ();
+
+ public int y_offset
+ {
+ get { return (int) scrolled_window.get_vadjustment ().get_value (); }
+ set { scrolled_window.get_vadjustment ().set_value ((double) value); }
+ }
+
+ public int x_offset
+ {
+ get { return (int) scrolled_window.get_hadjustment ().get_value (); }
+ set { scrolled_window.get_hadjustment ().set_value ((double) value); }
+ }
+
+ public BookView (Book book)
+ {
+ GLib.Object (orientation: Gtk.Orientation.VERTICAL);
+ this.book = book;
+
+ /* Load existing pages */
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ Page page = book.get_page (i);
+ add_cb (book, page);
+ }
+
+ selected_page = book.get_page (0);
+
+ /* Watch for new pages */
+ book.page_added.connect (add_cb);
+ book.page_removed.connect (remove_cb);
+ book.reordered.connect (reorder_cb);
+ book.cleared.connect (clear_cb);
+
+ need_layout = true;
+ page_data = new HashTable (direct_hash, direct_equal);
+ cursor = "default";
+
+ drawing_area = new SnapshotArea ();
+ drawing_area.set_size_request (200, 100);
+ drawing_area.can_focus = true;
+ drawing_area.focusable = true;
+ drawing_area.can_target = true;
+ drawing_area.set_snapshot_func (snapshot_cb);
+
+ // Use GtkScrolledWindow for automatic scrollbar management
+ scrolled_window = new Gtk.ScrolledWindow();
+ scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+ scrolled_window.set_child(drawing_area);
+ scrolled_window.hexpand = true;
+ scrolled_window.vexpand = true;
+
+ /* That needs to be added here and not for drawing area
+ - otherwise the events are not always captured */
+ cursor_scroll_controller = new Gtk.EventControllerScroll (
+ Gtk.EventControllerScrollFlags.BOTH_AXES
+ | Gtk.EventControllerScrollFlags.DISCRETE
+ );
+ cursor_scroll_controller.set_propagation_phase (Gtk.PropagationPhase.CAPTURE);
+ cursor_scroll_controller.scroll.connect (cursor_scroll_cb);
+ scrolled_window.add_controller(cursor_scroll_controller);
+
+ motion_controller = new Gtk.EventControllerMotion ();
+ motion_controller.motion.connect (motion_cb);
+ drawing_area.add_controller(motion_controller);
+
+ key_controller = new Gtk.EventControllerKey ();
+ key_controller.key_pressed.connect (key_cb);
+ drawing_area.add_controller(key_controller);
+
+ primary_click_gesture = new Gtk.GestureClick ();
+ primary_click_gesture.button = Gdk.BUTTON_PRIMARY;
+ primary_click_gesture.pressed.connect (primary_pressed_cb);
+ primary_click_gesture.released.connect (primary_released_cb);
+ drawing_area.add_controller(primary_click_gesture);
+
+ secondary_click_gesture = new Gtk.GestureClick ();
+ secondary_click_gesture.button = Gdk.BUTTON_SECONDARY;
+ secondary_click_gesture.pressed.connect (secondary_pressed_cb);
+ secondary_click_gesture.released.connect (secondary_released_cb);
+ drawing_area.add_controller(secondary_click_gesture);
+
+ focus_controller = new Gtk.EventControllerFocus ();
+ focus_controller.enter.connect_after (focus_cb);
+ focus_controller.leave.connect_after (focus_cb);
+ drawing_area.add_controller(focus_controller);
+
+ scrolled_window.get_vadjustment ().value_changed.connect (scroll_cb);
+ scrolled_window.get_hadjustment ().value_changed.connect (scroll_cb);
+
+ drawing_area.visible = true;
+
+ /* adding zoom buttons */
+ overlay = new Gtk.Overlay ();
+ overlay.set_child (scrolled_window);
+ overlay.hexpand = true;
+ overlay.vexpand = true;
+
+ zoom_button_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
+ zoom_button_box.valign = Gtk.Align.END;
+ zoom_button_box.halign = Gtk.Align.END;
+ zoom_button_box.margin_end = 24;
+ zoom_button_box.margin_bottom = 24;
+
+ toggle_grid_button = new Gtk.Button ();
+ toggle_grid_button.icon_name = "view-paged-symbolic";
+ toggle_grid_button.tooltip_text = _("Single Page View");
+ toggle_grid_button.add_css_class ("circular");
+ toggle_grid_button.add_css_class ("osd");
+ toggle_grid_button.margin_bottom = 6;
+ toggle_grid_button.clicked.connect (() => toggle_grid ());
+ zoom_button_box.append (toggle_grid_button);
+
+ zoom_in_button = new Gtk.Button ();
+ zoom_in_button.icon_name = "zoom-in-symbolic";
+ zoom_in_button.tooltip_text = _("Zoom In");
+ zoom_in_button.add_css_class ("circular");
+ zoom_in_button.add_css_class ("osd");
+ zoom_in_button.clicked.connect (() => zoom_in ());
+ zoom_button_box.append (zoom_in_button);
+
+ zoom_out_button = new Gtk.Button ();
+ zoom_out_button.icon_name = "zoom-out-symbolic";
+ zoom_out_button.tooltip_text = _("Zoom Out");
+ zoom_out_button.add_css_class ("circular");
+ zoom_out_button.add_css_class ("osd");
+ zoom_out_button.clicked.connect (() => zoom_out ());
+ zoom_button_box.append (zoom_out_button);
+
+ /* Per-page editing buttons (eraser, deskew), stacked vertically at the
+ top-right. Shown only in single-page view (managed alongside
+ move_button_box.visible). */
+ edit_button_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
+ edit_button_box.valign = Gtk.Align.START;
+ edit_button_box.halign = Gtk.Align.END;
+ edit_button_box.margin_top = 24;
+ edit_button_box.margin_end = 24;
+ edit_button_box.visible = false; /* only visible in single-page view */
+
+ eraser_button = new Gtk.Button ();
+ eraser_button.icon_name = "eraser-symbolic";
+ eraser_button.tooltip_text = _("Toggle Eraser");
+ eraser_button.add_css_class ("circular");
+ eraser_button.add_css_class ("osd");
+ eraser_button.clicked.connect (() => toggle_eraser ());
+ edit_button_box.append (eraser_button);
+
+ deskew_button = new Gtk.Button ();
+ deskew_button.icon_name = "object-rotate-right-symbolic";
+ deskew_button.tooltip_text = _("Deskew");
+ deskew_button.add_css_class ("circular");
+ deskew_button.add_css_class ("osd");
+ deskew_button.clicked.connect (() => toggle_deskew ());
+ edit_button_box.append (deskew_button);
+
+ overlay.add_overlay (edit_button_box);
+
+ move_button_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
+ move_button_box.valign = Gtk.Align.END;
+ move_button_box.halign = Gtk.Align.CENTER;
+ move_button_box.margin_bottom = 24;
+
+ move_left_button = new Gtk.Button ();
+ move_left_button.icon_name = "go-previous-symbolic";
+ move_left_button.tooltip_text = _("Move Left");
+ move_left_button.add_css_class ("circular");
+ move_left_button.add_css_class ("osd");
+ move_left_button.clicked.connect (() => select_prev_page ());
+ move_button_box.append (move_left_button);
+
+ move_right_button = new Gtk.Button ();
+ move_right_button.icon_name = "go-next-symbolic";
+ move_right_button.tooltip_text = _("Move Right");
+ move_right_button.add_css_class ("circular");
+ move_right_button.add_css_class ("osd");
+ move_right_button.clicked.connect (() => select_next_page ());
+ move_button_box.append (move_right_button);
+
+ overlay.add_overlay (move_button_box);
+ overlay.add_overlay (zoom_button_box);
+ append(overlay);
+
+ }
+
+ ~BookView ()
+ {
+ book.page_added.disconnect (add_cb);
+ book.page_removed.disconnect (remove_cb);
+ book.reordered.disconnect (reorder_cb);
+ book.cleared.disconnect (clear_cb);
+ motion_controller.motion.disconnect (motion_cb);
+ cursor_scroll_controller.scroll.disconnect (cursor_scroll_cb);
+ key_controller.key_pressed.disconnect (key_cb);
+ primary_click_gesture.pressed.disconnect (primary_pressed_cb);
+ primary_click_gesture.released.disconnect (primary_released_cb);
+ secondary_click_gesture.pressed.disconnect (secondary_pressed_cb);
+ secondary_click_gesture.released.disconnect (secondary_released_cb);
+ focus_controller.enter.disconnect (focus_cb);
+ focus_controller.leave.disconnect (focus_cb);
+ scrolled_window.get_vadjustment ().value_changed.disconnect (scroll_cb);
+ scrolled_window.get_hadjustment ().value_changed.disconnect (scroll_cb);
+ }
+
+ private PageView get_nth_page (int n)
+ {
+ Page page = book.get_page (n);
+ return page_data.lookup (page);
+ }
+
+ private PageView get_next_page (PageView page)
+ {
+ for (var i = 0; ; i++)
+ {
+ var p = book.get_page (i);
+ if (p == null)
+ break;
+ if (p == page.page)
+ {
+ for (var j = i + 1; ; j++)
+ {
+ p = book.get_page (j);
+ if (p != null && !(!show_deleted_pages && p.is_deleted))
+ {
+ return page_data.lookup (p);
+ }
+ else if (p == null)
+ return page;
+ }
+ }
+ }
+
+ return page;
+ }
+
+ private PageView get_prev_page (PageView page)
+ {
+ for (var i = 0; ; i++)
+ {
+ var p = book.get_page (i);
+ if (p == null)
+ break;
+ if (p == page.page)
+ {
+ if (i == 0)
+ return page;
+ for (var j = i - 1; ; j--)
+ {
+ p = book.get_page (j);
+ if (p != null && !(!show_deleted_pages && p.is_deleted))
+ {
+ return page_data.lookup (p);
+ }
+ else if (p == null)
+ return page;
+ if (i == 0)
+ return page;
+ }
+
+ }
+ }
+
+ return page;
+ }
+
+ // find the page view that is above the given page
+ private PageView get_above_page (PageView page)
+ {
+
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var p = get_nth_page (i);
+ if (p == null)
+ break;
+ if (
+ (p.y_offset >= page.y_offset - 2 * page.height) &&
+ (p.x_offset + p.width >= page.x_offset)) {
+ return p;
+ }
+ }
+
+ return page;
+ }
+
+ private PageView get_below_page (PageView page)
+ {
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var p = get_nth_page (i);
+ if (p == null)
+ break;
+ if ((p.y_offset > page.y_offset) && ((p.x_offset + p.width >= page.x_offset))) {
+ return p;
+ }
+ }
+
+ return page;
+ }
+
+ public void toggle_deleted_display ()
+ {
+ show_deleted_pages = !show_deleted_pages;
+ need_layout = true;
+ redraw ();
+ }
+
+ private void page_view_changed_cb (PageView page)
+ {
+ redraw ();
+ }
+
+ private void page_view_size_changed_cb (PageView page)
+ {
+ need_layout = true;
+ redraw ();
+ }
+
+ private void page_scan_progress_changed_cb (double progress)
+ {
+ scan_progress_changed (progress);
+ }
+
+ private void add_cb (Book book, Page page)
+ {
+ var page_view = new PageView (page);
+ page_view.changed.connect (page_view_changed_cb);
+ page_view.size_changed.connect (page_view_size_changed_cb);
+ page_view.scan_progress_changed.connect (page_scan_progress_changed_cb);
+ page_data.insert (page, page_view);
+ page_view.page_pixels_changed_cb (page_view.page);
+ need_layout = true;
+ redraw ();
+ }
+
+ private void set_selected_page_view (PageView? page)
+ {
+ /* Deselect existing page if changed */
+ if (selected_page_view != null && page != selected_page_view)
+ selected_page_view.selected = true;
+
+ selected_page_view = page;
+
+ /* unhide the newly selected page */
+ if (!show_grid)
+ need_layout = true;
+
+ redraw();
+ if (selected_page_view == null)
+ return;
+
+ /* Select new page if widget has focus */
+ if (!drawing_area.has_focus)
+ selected_page_view.selected = false;
+ else
+ selected_page_view.selected = true;
+ }
+
+ private void show_page_view (PageView? page)
+ {
+ if (page == null || !show_grid)
+ return;
+
+ int visible_height = scrolled_window.get_height();
+ var top_edge = page.y_offset;
+ var bottom_edge = page.y_offset + page.height + page_label_height;
+
+ /* if within the page (zoomed page) */
+ if ((top_edge < y_offset) && (bottom_edge > y_offset + visible_height))
+ return;
+ /* otherwise adjust offset */
+ else if (top_edge < y_offset) {
+ y_offset = top_edge;
+ }
+ else if (bottom_edge > y_offset + visible_height)
+ y_offset = bottom_edge - visible_height;
+ }
+
+ public int get_n_selected_pages ()
+ {
+ var n_selected = 0;
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if (book_page.is_selected)
+ n_selected++;
+ }
+ return n_selected;
+ }
+
+ public void select_first_page ()
+ {
+ var page = get_nth_page (0);
+ if (page == null)
+ return;
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ book_page.is_selected = i == 0;
+ }
+ set_selected_page_view (page);
+ show_page_view (page);
+ page_selected (page.page);
+ }
+
+ public void select_nth_page (int n)
+ {
+ var page = get_nth_page (n);
+ if (page == null) {
+ set_selected_page_view (null);
+ page_selected (null);
+ return;
+ }
+
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ book_page.is_selected = i == n;
+ }
+ set_selected_page_view (page);
+ show_page_view (page);
+ page_selected (page.page);
+ }
+
+ private void select_page_view (PageView? page)
+ {
+ Page? p = null;
+
+ if (selected_page_view == page)
+ return;
+
+ /* In single-page view only the displayed page is held at full
+ resolution. When switching to a different page, scale the
+ previously-displayed page back down so its texture is freed. */
+ var previously_selected = selected_page_view;
+
+ set_selected_page_view (page);
+
+ if (!show_grid)
+ {
+ if (previously_selected != null && previously_selected != page)
+ previously_selected.page.update_pixels (false, true);
+ if (page != null)
+ page.page.update_pixels (true, true);
+ }
+
+ if (need_layout)
+ show_selected_page = true;
+ else
+ show_page_view (page);
+
+ if (page != null)
+ p = page.page;
+ page_selected (p);
+ }
+
+ private void remove_cb (Book book, Page page)
+ {
+ PageView? new_selection = selected_page_view;
+
+ /* select next page if removing the selected page */
+ if (page == selected_page)
+ {
+ /* the last page is used if there is no next page */
+ new_selection = get_nth_page (-1);
+
+ for (var i = 0; i < book.n_pages; i++) {
+ var p = get_nth_page (i);
+ /* row below current selection */
+ if (p.y_offset > selected_page_view.y_offset)
+ {
+ new_selection = p;
+ break;
+ }
+ else if ((p.x_offset > selected_page_view.x_offset) && (p.y_offset == selected_page_view.y_offset))
+ {
+ new_selection = p;
+ break;
+ }
+ }
+ selected_page_view = null;
+ }
+
+ var page_view = page_data.lookup (page);
+ page_view.changed.disconnect (page_view_changed_cb);
+ page_view.size_changed.disconnect (page_view_size_changed_cb);
+ page_view.scan_progress_changed.disconnect (page_scan_progress_changed_cb);
+ page_data.remove (page);
+
+ select_page_view (new_selection);
+
+ need_layout = true;
+ redraw ();
+ }
+
+ private void reorder_cb (Book book)
+ {
+ need_layout = true;
+ redraw ();
+ }
+
+ private void clear_cb (Book book)
+ {
+ page_data.remove_all ();
+ selected_page_view = null;
+ page_selected (null);
+ need_layout = true;
+ redraw ();
+ }
+
+ public void app_window_resize_cb ()
+ {
+ need_layout = true;
+ layout ();
+ }
+
+ private void layout_into (int width, int height, out int book_width, out int book_height)
+ {
+ int min_page_height = 48;
+ int preview_page_height = 512;
+
+ if (book.n_pages == 0)
+ {
+ book_width = width;
+ book_height = height;
+ return;
+ }
+
+ /* Make sure a page is always selected */
+ if (selected_page_view == null)
+ {
+ select_first_page ();
+ }
+
+ /* make sure the selected_page_view page is properly set to selected
+ if not, deselect all other pages and select the one */
+ if (!selected_page_view.page.is_selected)
+ {
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = get_nth_page (i);
+ if (page != selected_page_view)
+ page.page.is_selected = false;
+ }
+ selected_page_view.page.is_selected = true;
+ page_selected (selected_page_view.page);
+ }
+
+ /* in single page mode, deselect other pages and make sure only the displayed page is scaled up */
+ if (!show_grid)
+ {
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page_view = get_nth_page (i);
+ if (page_view != selected_page_view)
+ {
+ page_view.page.is_selected = false;
+ page_view.page.update_pixels (false, true);
+ }
+ else
+ {
+ page_view.page.update_pixels (true, true);
+ /* make sure the texture is in high resolution */
+ var texture_width = page_view.get_texture_width();
+ if (texture_width < page_view.page.scan_width) {
+ page_view.page.pixels_changed ();
+ }
+ }
+ }
+ }
+
+ int available_height = height - 2 * page_spacing; // Subtract top and bottom margins
+ int available_width = width - 2 * page_spacing;
+
+ int calculated_height = available_height / 2 - page_label_height - page_spacing;
+
+ if (!show_grid)
+ calculated_height = available_height;
+
+ /* make sure we have a minimum height */
+ if (calculated_height < min_page_height)
+ calculated_height = min_page_height;
+
+ /* apply zoom */
+ calculated_height = (int) (calculated_height * zoom_factor);
+
+ /* make sure we have a minimum height */
+ while (calculated_height < min_page_height)
+ {
+ zoom_factor *= zoom_step;
+ calculated_height = (int)(calculated_height * zoom_step);
+ }
+
+ /* in grid view keep pages within one row and below the preview size */
+ if (show_grid)
+ {
+ while (calculated_height > (available_height - page_spacing - page_label_height))
+ {
+ zoom_factor /= zoom_step;
+ calculated_height = (int)(calculated_height / zoom_step);
+ }
+ while (calculated_height > preview_page_height)
+ {
+ zoom_factor /= zoom_step;
+ calculated_height = (int)(calculated_height / zoom_step);
+ }
+ }
+
+ /* and in single page view fill at least 1 / 2 */
+ if (!show_grid)
+ {
+ while (calculated_height < 0.5 * (available_height - page_spacing - page_label_height))
+ {
+ zoom_factor *= zoom_step;
+ calculated_height = (int)(calculated_height * zoom_step);
+ }
+ }
+
+ /* Calculate page widths with this height */
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = get_nth_page (i);
+ var p = page.page;
+ double aspect = (double)p.width / p.height;
+ int scaled_width = (int)(calculated_height * aspect + 0.5);
+ page.width = scaled_width;
+ page.height = calculated_height;
+ }
+
+ /* Layout pages */
+ int current_row = 0;
+ int row_width = page_spacing;
+ int y_offset_local = page_spacing;
+
+ /* check if the row is too narrow for one of the pages */
+ var max_row_width = width;
+
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = get_nth_page (i);
+
+ /* page view mode */
+ if (!show_grid)
+ {
+ if (page == selected_page_view)
+ {
+ /* center page */
+ page.x_offset = (int) Math.fmax(0, (width - page.width) / 2);
+ page.y_offset = (int) Math.fmax(0, (height - (page.height + 2 * page_spacing + page_label_height) ) / 2);
+ max_row_width = page.width;
+ }
+ else
+ {
+ /* hide page */
+ page.y_offset = -2 * page.height;
+ }
+
+ continue;
+ }
+
+ /* move deleted pages out of view */
+ if (!show_deleted_pages && page.page.is_deleted)
+ {
+ page.y_offset = -2 * page.height;
+ continue;
+ }
+
+ row_width += page.width + page_spacing;
+ /* Move to next row if needed */
+ if (i > 0 && row_width > available_width)
+ {
+ y_offset_local += calculated_height + page_label_height + page_spacing;
+ current_row++;
+
+ row_width = page.width + 2 * page_spacing;
+ }
+ /* check if the row is too narrow for one of the pages
+ so that a new row does not solve the display problem*/
+ if (page.width + 2 * page_spacing > max_row_width) {
+ max_row_width = page.width + 2 * page_spacing;
+ }
+
+ /* left align */
+ page.x_offset = row_width - page.width - page_spacing;
+ page.y_offset = y_offset_local;
+ }
+
+ book_width = max_row_width;
+ book_height = (current_row + 1) * (calculated_height + page_label_height + page_spacing);
+ if (!show_grid)
+ book_height -= page_spacing;
+ }
+
+ private void layout ()
+ {
+ if (!need_layout)
+ return;
+
+ GLib.Idle.add(() => {
+ /* The move buttons navigate between pages in single-page view,
+ so they are only useful when there is more than one page. */
+ move_button_box.visible = !show_grid && book.n_pages >= 2;
+ edit_button_box.visible = !show_grid;
+ if (show_grid && eraser_mode)
+ set_eraser_mode (false);
+ if (show_grid && deskew_mode)
+ set_deskew_mode (false);
+ return false;
+ });
+
+ laying_out = true;
+
+ int width = this.get_width();
+ int height = this.get_height();
+
+ int book_width, book_height;
+ layout_into (width, height, out book_width, out book_height);
+
+ drawing_area.set_size_request(book_width, book_height);
+ drawing_area.queue_resize();
+ if (show_selected_page)
+ show_page_view (selected_page_view);
+
+ need_layout = false;
+ show_selected_page = false;
+ laying_out = false;
+ }
+
+ private void update_drag_target (int x, int y)
+ {
+ drag_target_page = null;
+ drag_delete = false;
+
+ /* if outside the window, delete page */
+ if (x < 0
+ || y < y_offset
+ || x > scrolled_window.get_width ()
+ || y > y_offset + scrolled_window.get_height ()
+ )
+ {
+ drag_delete = true;
+ return;
+ }
+
+ /* get target page */
+ int half_spacing = page_spacing / 2;
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = get_nth_page (i);
+ if (page == drag_page_view)
+ continue;
+ if (y < page.y_offset || y > page.y_offset + page.height)
+ continue;
+ if (x < page.x_offset - half_spacing || x > page.x_offset + page.width + half_spacing)
+ continue;
+
+ drag_target_page = page;
+ drag_target_left = x <= page.x_offset + page.width / 2;
+ return;
+ }
+ }
+
+ private void draw_drag_preview (Gtk.Snapshot snapshot, int width, int height)
+ {
+ if (drag_page_view == null)
+ return;
+
+ int preview_height = 96;
+ int preview_width = 96;
+ if (drag_page_view.width < drag_page_view.height)
+ {
+ preview_width = (int) ((double) drag_page_view.width * preview_height / drag_page_view.height + 0.5);
+ if (preview_width < 24)
+ preview_width = 24;
+ }
+ else
+ {
+ preview_height = (int) ((double) drag_page_view.height * preview_width / drag_page_view.width + 0.5);
+ if (preview_height < 24)
+ preview_height = 24;
+ }
+
+ int preview_x = drag_cursor_x + 12;
+ int preview_y = drag_cursor_y + 12;
+
+ drag_page_view.render_preview (snapshot, preview_x, preview_y, preview_width, preview_height);
+ }
+
+ /**
+ * Build a rounded-rectangle Gsk.RoundedRect with all four corners equal.
+ */
+ private static Gsk.RoundedRect rounded_rect (double x, double y, double w, double h, double radius)
+ {
+ var bounds = Graphene.Rect ();
+ bounds.init ((float) x, (float) y, (float) w, (float) h);
+ var rr = Gsk.RoundedRect ();
+ rr.init_from_rect (bounds, (float) radius);
+ return rr;
+ }
+
+ /**
+ * Append a 1-px-wide rounded-rect outline using the GSK-native border node.
+ * This is the high-performance equivalent of stroking a path: GSK can
+ * render and cache it without allocating any Gsk.Path / Gsk.PathBuilder
+ * objects, which is critical when the outline is drawn for many pages
+ * on every redraw (scrolling).
+ */
+ private static void append_rounded_outline (Gtk.Snapshot snapshot, Gdk.RGBA color,
+ double x, double y, double w, double h,
+ double radius, double thickness)
+ {
+ var rr = rounded_rect (x, y, w, h, radius);
+ float[] widths = { (float) thickness, (float) thickness, (float) thickness, (float) thickness };
+ Gdk.RGBA[] colors = { color, color, color, color };
+ snapshot.append_border (rr, widths, colors);
+ }
+
+ /**
+ * Append a label centered above (label_y) at (label_x).
+ */
+ private void append_page_label (Gtk.Snapshot snapshot, int page_index, double label_x, double label_y)
+ {
+ var pango_ctx = drawing_area.get_pango_context ();
+ var pango_layout = new Pango.Layout (pango_ctx);
+ pango_layout.set_text (page_index.to_string (), -1);
+ int text_width;
+ pango_layout.get_pixel_size (out text_width, null);
+
+ Gdk.RGBA grey = { 0.5f, 0.5f, 0.5f, 1.0f };
+ snapshot.save ();
+ snapshot.translate (Graphene.Point () {
+ x = (float) (label_x - text_width / 2.0),
+ y = (float) label_y
+ });
+ snapshot.append_layout (pango_layout, grey);
+ snapshot.restore ();
+ }
+
+ public void snapshot_cb (Gtk.Snapshot snapshot, int width, int height)
+ {
+ layout ();
+
+ var scroll_y = (double) y_offset;
+ var scroll_bottom = scroll_y + scrolled_window.get_height ();
+
+ Gdk.RGBA selected_bg = { 0.871f, 0.867f, 0.855f, 1.0f };
+ Gdk.RGBA selected_border = { 0.604f, 0.600f, 0.588f, 1.0f };
+ Gdk.RGBA drag_color = Adw.StyleManager.get_default ().accent_color.to_rgba ();
+
+ var page_index = 0;
+ /* Render each page */
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = get_nth_page (i);
+ page_index++;
+
+ /* hide other pages in single page view */
+ if (!show_grid && selected_page_view != page)
+ continue;
+
+ /* hide deleted pages if toggled hide */
+ if (!show_deleted_pages && page.page.is_deleted)
+ continue;
+
+ var top_edge = page.y_offset;
+ var bottom_edge = page.y_offset + page.height + page_label_height;
+
+ /* Page not visible, don't render */
+ if (bottom_edge < scroll_y || top_edge > scroll_bottom)
+ continue;
+
+ /* background "shadow" */
+ {
+ int spacing_width = 9;
+ double x = page.x_offset + spacing_width;
+ double y = page.y_offset + spacing_width;
+ double w = page.width - 2 * spacing_width;
+ double h = page.height - 2 * spacing_width;
+
+ Gdk.RGBA shadow_color = { 0.78f, 0.77f, 0.76f, 0.95f };
+ var shadow_outline = rounded_rect (x, y, w, h, 0.0);
+ snapshot.append_outset_shadow (shadow_outline,
+ shadow_color,
+ 0.0f, /* dx */
+ 0.0f, /* dy */
+ 2.0f, /* spread */
+ 6.0f); /* blur_radius */
+ }
+
+ /* selected pages with gray background */
+ if (page.page.is_selected)
+ {
+ double x = page.x_offset, y = page.y_offset;
+ double w = page.width;
+ double h = page.height + page_label_height;
+ var rr = rounded_rect (x, y, w, h, 8.0);
+
+ snapshot.push_rounded_clip (rr);
+ var bg_rect = Graphene.Rect ();
+ bg_rect.init ((float) x, (float) y, (float) w, (float) h);
+ snapshot.append_color (selected_bg, bg_rect);
+ snapshot.pop ();
+
+ /* "selected" page for copy and so on - add a subtle border */
+ if (page == selected_page_view)
+ {
+ append_rounded_outline (snapshot, selected_border, x, y, w, h, 8.0, 1.0);
+ }
+ }
+
+ page.render (snapshot, page == selected_page_view, show_grid);
+
+ /* Render page number below the page */
+ var label_y = page.y_offset + page.height;
+ var label_x = page.x_offset + page.width / 2.0;
+ append_page_label (snapshot, page_index, label_x, label_y);
+ }
+
+ if (drag_in_progress && drag_target_page != null)
+ {
+ var previous_page = get_prev_page (selected_page_view);
+ var next_page = get_next_page (selected_page_view);
+
+ int x = drag_target_left ? drag_target_page.x_offset
+ : drag_target_page.x_offset + drag_target_page.width;
+ x = drag_target_left ? x - page_spacing / 2 : x + page_spacing / 2;
+
+ /* do not display bar for previous and next page */
+ if (
+ !(drag_target_left && (drag_target_page == next_page)) &&
+ !(!drag_target_left && (drag_target_page == previous_page))
+ )
+ {
+ /* 3px-thick vertical bar */
+ var bar = Graphene.Rect ();
+ bar.init ((float) (x - 1),
+ (float) (drag_target_page.y_offset + page_spacing),
+ 3.0f,
+ (float) (drag_target_page.height - 2 * page_spacing));
+ snapshot.append_color (drag_color, bar);
+ }
+ }
+
+ if (drag_in_progress)
+ draw_drag_preview (snapshot, width, height);
+
+ /* Eraser cursor preview: a transparent square with black + white outlines
+ at the last known cursor position. The cursor texture itself moves with
+ the system pointer; this overlay keeps the visual on the canvas
+ consistent (it's drawn at the same screen position as the cursor). */
+ if (eraser_mode && selected_page_view != null)
+ draw_eraser_overlay (snapshot);
+
+ /* Deskew guide line: drawn from the first click to the current cursor
+ position so the user can see the reference they are marking. */
+ if (deskew_mode && deskew_have_first_point && selected_page_view != null)
+ draw_deskew_overlay (snapshot);
+ }
+
+ private void draw_deskew_overlay (Gtk.Snapshot snapshot)
+ {
+ Gdk.RGBA white = { 1.0f, 1.0f, 1.0f, 1.0f };
+ /* get system color */
+ var system_color = Adw.StyleManager.get_default ().accent_color.to_rgba ();
+
+ var builder = new Gsk.PathBuilder ();
+ builder.move_to ((float) deskew_x0, (float) deskew_y0);
+ builder.line_to ((float) deskew_cursor_x, (float) deskew_cursor_y);
+ var path = builder.to_path ();
+
+ /* White casing underneath for contrast, thinner coloured line on top. */
+ var outer = new Gsk.Stroke (4.0f);
+ snapshot.append_stroke (path, outer, white);
+ var inner = new Gsk.Stroke (2.0f);
+ snapshot.append_stroke (path, inner, system_color);
+ }
+
+ private void draw_eraser_overlay (Gtk.Snapshot snapshot)
+ {
+ Gdk.RGBA white = { 1.0f, 1.0f, 1.0f, 1.0f };
+ Gdk.RGBA black = { 0.0f, 0.0f, 0.0f, 1.0f };
+
+ float n = (float) eraser_size_screen;
+ float ox = (float) eraser_cursor_x - n / 2.0f;
+ float oy = (float) eraser_cursor_y - n / 2.0f;
+
+ /* Outer white outline (1 px) */
+ var outer_bounds = Graphene.Rect ();
+ outer_bounds.init (ox, oy, n, n);
+ var outer = Gsk.RoundedRect ();
+ outer.init_from_rect (outer_bounds, 0);
+ float[] outer_widths = { 1.0f, 1.0f, 1.0f, 1.0f };
+ Gdk.RGBA[] outer_cols = { white, white, white, white };
+ snapshot.append_border (outer, outer_widths, outer_cols);
+
+ /* Inner black border (1 px), inset by 1 px so it sits visually inside. */
+ var inner_bounds = Graphene.Rect ();
+ inner_bounds.init (ox + 1, oy + 1, n - 2, n - 2);
+ var inner = Gsk.RoundedRect ();
+ inner.init_from_rect (inner_bounds, 0);
+ float[] inner_widths = { 1.0f, 1.0f, 1.0f, 1.0f };
+ Gdk.RGBA[] inner_cols = { black, black, black, black };
+ snapshot.append_border (inner, inner_widths, inner_cols);
+ }
+
+ private PageView? get_page_at (int x, int y, out int x_, out int y_)
+ {
+ x_ = y_ = 0;
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var page = get_nth_page (i);
+ var left = page.x_offset;
+ var right = left + page.width;
+ var top = page.y_offset;
+ var bottom = top + page.height + page_label_height;
+ if (x >= left && x <= right && y >= top && y <= bottom)
+ {
+ x_ = x - left;
+ y_ = y - top;
+ return page;
+ }
+ }
+
+ return null;
+ }
+
+ private void primary_pressed_cb (Gtk.GestureClick controler, int n_press, double x, double y)
+ {
+ button_cb(controler, Gdk.BUTTON_PRIMARY, true, n_press, x, y);
+ }
+
+ private void primary_released_cb (Gtk.GestureClick controler, int n_press, double x, double y)
+ {
+ button_cb(controler, Gdk.BUTTON_PRIMARY, false, n_press, x, y);
+ }
+
+ private void secondary_pressed_cb (Gtk.GestureClick controler, int n_press, double x, double y)
+ {
+ button_cb(controler, Gdk.BUTTON_SECONDARY, true, n_press, x, y);
+ }
+
+ private void secondary_released_cb (Gtk.GestureClick controler, int n_press, double x, double y)
+ {
+ button_cb(controler, Gdk.BUTTON_SECONDARY, false, n_press, x, y);
+ }
+
+ private void button_cb (Gtk.GestureClick controler, int button, bool press, int n_press, double dx, double dy)
+ {
+ layout ();
+
+ drawing_area.grab_focus ();
+ /* we need to do that otherwise grab_focus scrolls up left ... */
+ y_offset = y_offset;
+ x_offset = x_offset;
+
+ /* Eraser mode: primary button starts/stops a stroke. */
+ if (eraser_mode && button == Gdk.BUTTON_PRIMARY)
+ {
+ int ex = (int) dx;
+ int ey = (int) dy;
+ if (press)
+ {
+ eraser_active = true;
+ eraser_cursor_x = ex;
+ eraser_cursor_y = ey;
+ eraser_apply (ex, ey, false);
+ redraw ();
+ }
+ else
+ {
+ if (eraser_active)
+ {
+ /* Final apply with save_to_disk=true. */
+ eraser_apply (ex, ey, true);
+ }
+ eraser_active = false;
+ redraw ();
+ }
+ return;
+ }
+
+ /* Deskew mode: two primary clicks define a line that should become
+ horizontal; the second click rotates the page. A secondary click
+ cancels the operation. */
+ if (deskew_mode)
+ {
+ if (button == Gdk.BUTTON_SECONDARY && press)
+ {
+ set_deskew_mode (false);
+ return;
+ }
+ if (button == Gdk.BUTTON_PRIMARY && press && selected_page_view != null)
+ {
+ if (!deskew_have_first_point)
+ {
+ deskew_x0 = dx;
+ deskew_y0 = dy;
+ deskew_cursor_x = dx;
+ deskew_cursor_y = dy;
+ deskew_have_first_point = true;
+ redraw ();
+ }
+ else
+ {
+ deskew_apply (dx, dy);
+ set_deskew_mode (false);
+ }
+ }
+ return;
+ }
+
+ int x = 0, y = 0;
+ var page_view = get_page_at ((int) dx, (int) dy, out x, out y);
+
+ var event = controler.get_current_event ();
+ var state = event.get_modifier_state ();
+
+ var page = page_view != null ? page_view.page : null;
+
+ if (press && button == Gdk.BUTTON_PRIMARY && page_view != null && show_grid)
+ {
+ if ((state & Gdk.ModifierType.CONTROL_MASK) != 0)
+ {
+ page.is_selected = !page.is_selected;
+ /* we deselected the current page view - find the first! */
+ if (!page.is_selected && selected_page_view.page == page)
+ {
+ var found_another_selected_page = false;
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if (book_page.is_selected) {
+ select_page_view (page_data.lookup (book_page));
+ page_selected (book_page);
+ found_another_selected_page = true;
+ break;
+ }
+ }
+ if (!found_another_selected_page)
+ {
+ select_page_view (null);
+ page_selected (null);
+ }
+ }
+ }
+ else if ((state & Gdk.ModifierType.SHIFT_MASK) != 0)
+ {
+ /* select range from selected to clicked page */
+ if (selected_page_view != null)
+ {
+ var found_selected = 0;
+ var block_end_found = false;
+ /* for "extending" the selection to the right (keep selected up to selected view) */
+ int not_selected_up_to = 0;
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if ((book_page == page) || (book_page == selected_page_view.page))
+ found_selected++;
+ if (found_selected == 1 || book_page == page_view.page || book_page == selected_page_view.page)
+ {
+ book_page.is_selected = true;
+ }
+ else if (found_selected == 0)
+ {
+ if (!book_page.is_selected && i != not_selected_up_to)
+ {
+ for (int j = i; j >= not_selected_up_to; j--)
+ {
+ var book_page2 = book.get_page (j);
+ book_page2.is_selected = false;
+ }
+ }
+ not_selected_up_to = i;
+ }
+ /* allow "extending" the selection to the left */
+ else if (found_selected > 1) {
+ if (!book_page.is_selected)
+ block_end_found = true;
+ if (block_end_found)
+ book_page.is_selected = false;
+ else
+ book_page.is_selected = true;
+ }
+
+ }
+ }
+ }
+ }
+
+ if (button == Gdk.BUTTON_PRIMARY && !press && n_press == 1 && (state & Gdk.ModifierType.SHIFT_MASK) == 0 && (state & Gdk.ModifierType.CONTROL_MASK) == 0 && !drag_in_progress && page != null)
+ {
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if (book_page != page)
+ book_page.is_selected = false;
+ }
+ page.is_selected = true;
+
+ if (page.is_selected)
+ {
+ select_page_view (page_view);
+ }
+ page_selected (selected_page_view.page);
+ }
+
+ /* Modify page */
+ if (button == Gdk.BUTTON_PRIMARY && page_view != null)
+ {
+ if (press && page_view.page.is_selected)
+ {
+ bool started_crop = false;
+ if (selected_page_view != null)
+ started_crop = selected_page_view.button_press (x, y);
+
+ if (!started_crop)
+ {
+ drag_page_view = selected_page_view;
+ drag_pending = drag_page_view != null;
+ drag_in_progress = false;
+ drag_start_x = (int) dx;
+ drag_start_y = (int) dy;
+ drag_cursor_x = drag_start_x;
+ drag_cursor_y = drag_start_y;
+ drag_target_page = null;
+ }
+ }
+ else if (!press && n_press == 2)
+ {
+ /* make sure only one page is selected */
+ for (var i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if (book_page != page)
+ book_page.is_selected = false;
+ }
+
+ show_page (selected_page);
+ }
+ else if (!press)
+ {
+ if (drag_in_progress && drag_page_view != null && show_grid)
+ {
+ if (drag_delete)
+ {
+ book.delete_page(drag_page_view.page);
+ redraw ();
+ }
+ else if (drag_target_page != null)
+ {
+ var source_index = book.get_page_index (drag_page_view.page);
+ var target_index = book.get_page_index (drag_target_page.page);
+ var location = drag_target_left ? target_index : target_index + 1;
+ if (location > source_index)
+ location -= 1;
+ if (drag_page_view.page != drag_target_page.page)
+ {
+ var start_selection = (int) book.n_pages;
+ var end_selection = 0;
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if ((book_page != null) && book_page.is_selected)
+ {
+ if (i < start_selection)
+ start_selection = i;
+ end_selection = i;
+ }
+ }
+ if (start_selection == end_selection)
+ move_page_with_undo(selected_page_view.page, location);
+ else if (location < start_selection)
+ {
+ /* if we move pages from the end in front of the target, the pages shift +1 */
+ var selection_correction = 0;
+
+ for (int i = end_selection; i > (int) location; i--)
+ {
+ var book_page = book.get_page (i + selection_correction);
+ if ((book_page != null) && book_page.is_selected)
+ {
+ move_page_with_undo(book_page, location);
+ selection_correction++;
+ }
+ }
+ }
+ else if (location > end_selection)
+ {
+ /* if we move pages behind the target, the target shifts -1 */
+ var selection_correction = 0;
+
+ for (int i = end_selection; i >= start_selection; i--)
+ {
+ var book_page = book.get_page (i);
+ if ((book_page != null) && book_page.is_selected)
+ {
+ move_page_with_undo(book_page, location - selection_correction);
+ selection_correction++;
+ }
+ }
+ }
+
+ }
+ }
+ }
+
+ if (selected_page_view != null)
+ selected_page_view.button_release (x, y);
+ }
+
+ }
+
+ if (!press)
+ {
+ drag_in_progress = false;
+ drag_pending = false;
+ drag_page_view = null;
+ drag_target_page = null;
+ set_cursor ("default");
+ }
+
+ /* Show pop-up menu on right click (after release!) */
+ if (button == Gdk.BUTTON_SECONDARY && !press)
+ {
+ if (page_view != null)
+ {
+ /* just make selected page view but don't move selection */
+ if (page_view.page.is_selected)
+ {
+ selected_page_view = page_view;
+ page_selected (selected_page_view.page);
+ }
+ /* was not selected before, make only selected */
+ else
+ {
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ book_page.is_selected = false;
+ }
+ page_view.page.is_selected = true;
+ select_page_view (page_view);
+ page_selected (page_view.page);
+ }
+ }
+ show_menu (drawing_area, dx, dy);
+ }
+ }
+
+ /* there's no default cursor for some actions, create them
+ from the icons */
+ private Gdk.Texture create_cursor_from_icon (string icon_name)
+ {
+ int cursor_symbol_size = 20;
+
+ /* black & white icon */
+ Gdk.RGBA black = { 0.0f, 0.0f, 0.0f, 1.0f };
+ Gdk.RGBA white_color = { 1.0f, 1.0f, 1.0f, 0.875f };
+ Gdk.RGBA shadow_color = { 0.0f, 0.0f, 0.0f, 0.25f };
+ Gdk.RGBA error_color = { 1.0f, 0.0f, 0.0f, 1.0f };
+ Gdk.RGBA warning_color = { 1.0f, 1.0f, 0.0f, 1.0f };
+ Gdk.RGBA success_color = { 0.0f, 1.0f, 0.0f, 1.0f };
+ Gdk.RGBA[] colors = { black, error_color, warning_color, success_color };
+ Gdk.RGBA[] white_colors = { white_color, error_color, warning_color, success_color };
+ Gdk.RGBA[] shadow_colors = { shadow_color, error_color, warning_color, success_color };
+
+ var display = Gdk.Display.get_default();
+ var renderer = drawing_area.get_native().get_renderer();
+ var icon_theme = Gtk.IconTheme.get_for_display(display);
+
+ var snapshot = new Gtk.Snapshot();
+ var icon_paintable = icon_theme.lookup_icon(icon_name, null, cursor_symbol_size, 1, Gtk.TextDirection.NONE, 0);
+
+ /* render shadow below */
+ snapshot.save();
+ snapshot.translate(Graphene.Point() {
+ x = 0, y = 2
+ });
+ ((Gtk.SymbolicPaintable)icon_paintable).snapshot_symbolic(snapshot, cursor_symbol_size, cursor_symbol_size, shadow_colors);
+ snapshot.restore();
+
+ /* Render white border */
+ for (int dx = -1; dx <= 1; dx += 2) {
+ for (int dy = -1; dy <= 1; dy += 2) {
+ snapshot.save();
+ snapshot.translate(Graphene.Point() {
+ x = dx, y = dy
+ });
+ ((Gtk.SymbolicPaintable)icon_paintable).snapshot_symbolic(snapshot, cursor_symbol_size, cursor_symbol_size, white_colors);
+ snapshot.restore();
+ }
+ }
+
+ /* Render black icon on top */
+ ((Gtk.SymbolicPaintable)icon_paintable).snapshot_symbolic(snapshot, cursor_symbol_size, cursor_symbol_size, colors);
+ var icon_node = snapshot.to_node();
+ return renderer.render_texture(icon_node, null);
+ }
+
+ private void init_cursor_symbols ()
+ {
+ if (delete_cursor == null) {
+ delete_cursor = create_cursor_from_icon("edit-delete-symbolic");
+ }
+ }
+
+ private new void set_cursor (string cursor)
+ {
+ init_cursor_symbols ();
+ if (this.cursor == cursor)
+ return;
+ this.cursor = cursor;
+
+ Gdk.Cursor c;
+ if (cursor == "delete")
+ {
+ c = new Gdk.Cursor.from_texture(delete_cursor, 12, 12, null);
+ }
+ else if (cursor == "eraser")
+ {
+ init_eraser_cursor ();
+ int hot = eraser_size_screen / 2;
+ c = new Gdk.Cursor.from_texture (eraser_cursor_texture, hot, hot, null);
+ }
+ else
+ {
+ c = new Gdk.Cursor.from_name (cursor, null);
+ }
+
+ drawing_area.set_cursor (c);
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Eraser mode */
+ /* ------------------------------------------------------------------ */
+
+ /**
+ * Build the cursor texture used in eraser mode: a transparent square
+ * with a black border surrounded by a thin white outline so it stays
+ * visible on both light and dark backgrounds.
+ */
+ private void init_eraser_cursor ()
+ {
+ if (eraser_cursor_texture != null)
+ return;
+
+ int n = eraser_size_screen;
+ var renderer = drawing_area.get_native ().get_renderer ();
+ var snapshot = new Gtk.Snapshot ();
+
+ Gdk.RGBA white = { 1.0f, 1.0f, 1.0f, 1.0f };
+ Gdk.RGBA black = { 0.0f, 0.0f, 0.0f, 1.0f };
+
+ /* Outer white outline (1 px) */
+ var outer_bounds = Graphene.Rect ();
+ outer_bounds.init (0, 0, (float) n, (float) n);
+ var outer = Gsk.RoundedRect ();
+ outer.init_from_rect (outer_bounds, 0);
+ float[] outer_widths = { 1.0f, 1.0f, 1.0f, 1.0f };
+ Gdk.RGBA[] outer_cols = { white, white, white, white };
+ snapshot.append_border (outer, outer_widths, outer_cols);
+
+ /* Inner black border (1 px), inset by 1 px */
+ var inner_bounds = Graphene.Rect ();
+ inner_bounds.init (1, 1, (float) (n - 2), (float) (n - 2));
+ var inner = Gsk.RoundedRect ();
+ inner.init_from_rect (inner_bounds, 0);
+ float[] inner_widths = { 1.0f, 1.0f, 1.0f, 1.0f };
+ Gdk.RGBA[] inner_cols = { black, black, black, black };
+ snapshot.append_border (inner, inner_widths, inner_cols);
+
+ var node = snapshot.to_node ();
+ if (node != null)
+ {
+ var bounds = Graphene.Rect ();
+ bounds.init (0, 0, (float) n, (float) n);
+ eraser_cursor_texture = renderer.render_texture (node, bounds);
+ }
+ }
+
+ /** Toggle the eraser button (called from the button click handler). */
+ private void toggle_eraser ()
+ {
+ set_eraser_mode (!eraser_mode);
+ }
+
+ /** Programmatically enter or leave eraser mode. */
+ private void set_eraser_mode (bool active)
+ {
+ if (eraser_mode == active)
+ return;
+
+ /* The two edit modes are mutually exclusive. */
+ if (active && deskew_mode)
+ set_deskew_mode (false);
+
+ eraser_mode = active;
+ eraser_active = false;
+
+ if (eraser_button != null)
+ {
+ eraser_button.icon_name = eraser_mode
+ ? "pointer-primary-click-symbolic"
+ : "eraser-symbolic";
+ eraser_button.tooltip_text = eraser_mode
+ ? _("Exit Eraser Mode")
+ : _("Toggle Eraser");
+ }
+
+ set_cursor (eraser_mode ? "eraser" : "default");
+ redraw ();
+ }
+
+ /* Erase the area in Page.pixels */
+ private bool eraser_apply (int event_x, int event_y, bool last)
+ {
+ if (selected_page_view == null)
+ return false;
+
+ /* Translate widget coords into page-image coords (inside the
+ border + ruler frame). */
+ int local_x = event_x - selected_page_view.x_offset
+ - selected_page_view.get_image_origin_x ();
+ int local_y = event_y - selected_page_view.y_offset
+ - selected_page_view.get_image_origin_y ();
+
+ int half = eraser_size_screen / 2;
+ int sx0 = local_x - half;
+ int sy0 = local_y - half;
+ int sx1 = sx0 + eraser_size_screen;
+ int sy1 = sy0 + eraser_size_screen;
+
+ /* Clip to the visible image rectangle. */
+ int pw = selected_page_view.get_preview_width ();
+ int ph = selected_page_view.get_preview_height ();
+ if (sx1 <= 0 || sy1 <= 0 || sx0 >= pw || sy0 >= ph)
+ return false;
+
+ sx0 = int.max (0, sx0);
+ sy0 = int.max (0, sy0);
+ sx1 = int.min (pw, sx1);
+ sy1 = int.min (ph, sy1);
+
+ /* Map the clipped screen rect back into page (rotated) coordinates.
+ Use floor for the top-left and ceil for the bottom-right so the
+ erased area always covers every pixel under the cursor square. */
+ var page = selected_page_view.page;
+ int px0 = (int) Math.floor ((double) sx0 * page.width / pw);
+ int py0 = (int) Math.floor ((double) sy0 * page.height / ph);
+ int px1 = (int) Math.ceil ((double) sx1 * page.width / pw);
+ int py1 = (int) Math.ceil ((double) sy1 * page.height / ph);
+ if (px1 <= px0 || py1 <= py0)
+ return false;
+
+ /* Make sure we are at full resolution before mutating pixels. */
+ page.update_pixels (true);
+ page.erase_rect (px0, py0, px1 - px0, py1 - py0, last);
+ return true;
+ }
+
+ private void toggle_deskew ()
+ {
+ set_deskew_mode (!deskew_mode);
+ }
+
+ public void deskew ()
+ {
+ if (show_grid)
+ return;
+ toggle_deskew ();
+ }
+
+ private void set_deskew_mode (bool active)
+ {
+ if (deskew_mode == active)
+ return;
+
+ /* Make sure only one mode is active at a time. */
+ if (active && eraser_mode)
+ set_eraser_mode (false);
+
+ deskew_mode = active;
+ deskew_have_first_point = false;
+
+ if (deskew_button != null)
+ {
+ deskew_button.icon_name = deskew_mode
+ ? "pointer-primary-click-symbolic"
+ : "object-rotate-right-symbolic";
+ deskew_button.tooltip_text = deskew_mode
+ ? _("Exit Deskew Mode")
+ : _("Deskew");
+ }
+
+ set_cursor (deskew_mode ? "crosshair" : "default");
+
+ /* Explain how the tool works the first time it is used. */
+ if (deskew_mode)
+ request_deskew_help ();
+
+ redraw ();
+ }
+
+ /* Rotate the image so that the drawn line is horizontal afterwards. */
+ private void deskew_apply (double x1, double y1)
+ {
+ if (selected_page_view == null)
+ return;
+
+ double ddx = x1 - deskew_x0;
+ double ddy = y1 - deskew_y0;
+
+ /* Ignore a degenerate (too short) line to avoid noisy rotations. */
+ if (ddx * ddx + ddy * ddy < 25.0)
+ return;
+
+ double line_angle = Math.atan2 (ddy, ddx) * 180.0 / Math.PI;
+
+ /* Normalise into [-90, 90] so we rotate by the smallest amount and
+ never flip the page upside down. */
+ if (line_angle > 90.0)
+ line_angle -= 180.0;
+ else if (line_angle < -90.0)
+ line_angle += 180.0;
+
+ if (Math.fabs (line_angle) < 0.01)
+ return;
+
+ var page = selected_page_view.page;
+
+ /* Make sure we are at full resolution before mutating pixels, then
+ rotate so the marked line becomes horizontal. rotate() rotates the
+ image clockwise for a positive angle, so we negate the line angle. */
+ page.update_pixels (true);
+ page.rotate (-line_angle, true);
+
+ /* rotate() does not emit pixels_changed itself; trigger a texture
+ rebuild and mark the book as changed. */
+ page.pixels_changed ();
+ redraw ();
+ }
+
+ private void motion_cb (Gtk.EventControllerMotion controler, double dx, double dy)
+ {
+ string cursor = "default";
+
+ int event_x = (int) dx;
+ int event_y = (int) dy;
+
+ var event_state = controler.get_current_event_state();
+
+ /* Deskew mode: track the cursor so the guide line follows it. */
+ if (deskew_mode)
+ {
+ deskew_cursor_x = event_x;
+ deskew_cursor_y = event_y;
+ set_cursor ("crosshair");
+ if (deskew_have_first_point)
+ redraw ();
+ return;
+ }
+
+ /* Eraser mode short-circuits the normal hover/drag logic. */
+ if (eraser_mode)
+ {
+ eraser_cursor_x = event_x;
+ eraser_cursor_y = event_y;
+ if (eraser_active && (event_state & Gdk.ModifierType.BUTTON1_MASK) != 0)
+ eraser_apply (event_x, event_y, false);
+ set_cursor ("eraser");
+ redraw ();
+ return;
+ }
+
+ /* Dragging */
+ if (selected_page_view != null && (event_state & Gdk.ModifierType.BUTTON1_MASK) != 0)
+ {
+ if (drag_pending && !drag_in_progress)
+ {
+ int drag_dx = event_x - drag_start_x;
+ if (drag_dx < 0)
+ drag_dx = -drag_dx;
+ int drag_dy = event_y - drag_start_y;
+ if (drag_dy < 0)
+ drag_dy = -drag_dy;
+
+ if (drag_dx >= drag_start_threshold || drag_dy >= drag_start_threshold && show_grid)
+ {
+ drag_in_progress = true;
+
+ drag_cursor_x = event_x;
+ drag_cursor_y = event_y;
+ update_drag_target (event_x, event_y);
+ cursor = "grabbing";
+ redraw ();
+ }
+ else
+ {
+ cursor = selected_page_view.cursor;
+ }
+ }
+
+ /* for drag & drop, do not deselect page you drag */
+ if (!selected_page_view.page.is_selected && show_grid)
+ {
+ selected_page_view.page.is_selected = true;
+ redraw ();
+ }
+
+ if (drag_in_progress && show_grid)
+ {
+ drag_cursor_x = event_x;
+ drag_cursor_y = event_y;
+
+ /* scroll if at top or bottom */
+
+ /* distance to bottom / top triggering scroll */
+ int drag_scroll_margin = 95;
+
+ if (
+ (event_y + drag_scroll_margin > y_offset + scrolled_window.get_height ())
+ && (event_y < y_offset + scrolled_window.get_height ())
+ )
+ {
+ if (y_offset + 5 <= drawing_area.get_height () - scrolled_window.get_height ())
+ {
+ y_offset += 16;
+ }
+ }
+ else if (
+ (event_y - drag_scroll_margin < y_offset)
+ && (event_y > y_offset)
+ )
+ {
+ if (y_offset >= 16)
+ {
+ y_offset -= 16;
+ }
+ }
+
+ /* determine what the grab action is */
+ update_drag_target (event_x, event_y);
+ if (drag_delete)
+ cursor = "delete";
+ else
+ cursor = "grabbing";
+ redraw ();
+ }
+ else
+ {
+ var x = (int) (event_x - selected_page_view.x_offset);
+ var y = (int) (event_y - selected_page_view.y_offset);
+ selected_page_view.motion (x, y);
+ cursor = selected_page_view.cursor;
+ }
+ }
+ else
+ {
+ int x, y;
+ var over_page = get_page_at ((int) (event_x), (int) event_y, out x, out y);
+ if (over_page != null)
+ {
+ over_page.motion (x, y);
+ cursor = over_page.cursor;
+ }
+ }
+
+ set_cursor (cursor);
+ }
+
+ private bool key_cb (Gtk.EventControllerKey controler, uint keyval, uint keycode, Gdk.ModifierType state)
+ {
+ PageView target_page_view = null;
+
+ switch (keyval)
+ {
+ case Gdk.Key.Home:
+ target_page_view = page_data.lookup (book.get_page (0));
+ break;
+ case Gdk.Key.Left:
+ target_page_view = get_prev_page (selected_page_view);
+ break;
+ case Gdk.Key.Right:
+ target_page_view = get_next_page (selected_page_view);
+ break;
+ case Gdk.Key.Page_Up:
+ target_page_view = get_above_page (selected_page_view);
+ break;
+ case Gdk.Key.Page_Down:
+ target_page_view = get_below_page (selected_page_view);
+ break;
+ case Gdk.Key.End:
+ target_page_view = page_data.lookup (book.get_page ((int) book.n_pages - 1));
+ break;
+ case Gdk.Key.Menu:
+ if (selected_page_view != null) {
+ show_menu (drawing_area, (double) selected_page_view.x_offset + 1.0, (double) selected_page_view.y_offset + 1.0);
+ } else {
+ show_menu (drawing_area, 0.0, 0.0);
+ }
+ return true;
+ case Gdk.Key.F10:
+ if ((state & Gdk.ModifierType.SHIFT_MASK) != 0) {
+ if (selected_page_view != null) {
+ show_menu (drawing_area, (double) selected_page_view.x_offset + 1.0, (double) selected_page_view.y_offset + 1.0);
+ } else {
+ show_menu (drawing_area, 0.0, 0.0);
+ }
+ return true;
+ }
+ return false;
+ default:
+ return false;
+ }
+
+ if (target_page_view == null)
+ return true;
+
+ /* set page to selected */
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ book_page.is_selected = false;
+ }
+ target_page_view.page.is_selected = true;
+ select_page_view (target_page_view);
+ return true;
+ }
+
+ private void scroll_cb (Gtk.Adjustment adjustment)
+ {
+ if (!laying_out)
+ redraw ();
+ }
+
+ private void focus_cb (Gtk.EventControllerFocus controler)
+ {
+ set_selected_page_view (selected_page_view);
+ }
+
+ private bool cursor_scroll_cb (Gtk.EventControllerScroll controller, double dx, double dy)
+ {
+ if (dy == 0) {
+ return false;
+ }
+
+ /* Check if Ctrl is pressed for zooming */
+ var event_state = controller.get_current_event_state();
+ if ((event_state & Gdk.ModifierType.CONTROL_MASK) != 0)
+ {
+ if (dy > 0)
+ zoom_out ();
+ else if (dy < 0)
+ zoom_in ();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void set_toggle_grid_button_sensitive (bool sensitive)
+ {
+ toggle_grid_button.set_sensitive (sensitive);
+ }
+
+ public void toggle_grid ()
+ {
+ /* Leaving/entering the grid must stop any in-progress deskew. */
+ set_deskew_mode (false);
+
+ show_grid = !show_grid;
+ toggle_grid_button.set_icon_name (show_grid ? "view-paged-symbolic" : "view-grid-symbolic");
+ toggle_grid_button.set_tooltip_text (show_grid ? _("Single Page View") : _("Grid View"));
+
+ /* Make sure a page is always selected */
+ if (selected_page_view == null)
+ {
+ select_first_page ();
+ }
+
+ /* set only zoomed page to selected */
+ if (!show_grid)
+ {
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ if (selected_page_view.page == book_page)
+ {
+ book_page.is_selected = true;
+ book_page.update_pixels (true, true);
+ }
+ else
+ {
+ book_page.is_selected = false;
+ book_page.update_pixels (false, true);
+ }
+ }
+ select_page_view (selected_page_view);
+ }
+ else
+ {
+ /* Grid view: every page must be scaled down (including the previously
+ selected one) so we don't keep a large texture around for any of
+ them. */
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ book_page.update_pixels (false, true);
+ }
+ }
+
+ need_layout = true;
+ redraw ();
+ }
+
+ public void zoom_and_keep_focus (bool zoom_in) {
+
+ /* zooming changes page coordinates, so cancel any in-progress deskew */
+ set_deskew_mode (false);
+
+ /* zoom limits */
+ if ((zoom_factor >= 256.0 && zoom_in) || (zoom_factor <= 0.25 && !zoom_in))
+ return;
+
+ /* no need to keep focus in grid mode */
+ if (show_grid)
+ {
+ var zoom_change = zoom_in ? zoom_step : 1 / zoom_step;
+ zoom_factor *= zoom_change;
+
+ need_layout = true;
+ redraw ();
+ return;
+ }
+
+ Gtk.Adjustment hadjustment = scrolled_window.get_hadjustment ();
+ Gtk.Adjustment vadjustment = scrolled_window.get_vadjustment ();
+
+ double old_hadj_value = hadjustment.get_value ();
+ double old_vadj_value = vadjustment.get_value ();
+
+ double h_page_size = hadjustment.get_page_size ();
+ double v_page_size = vadjustment.get_page_size ();
+
+ double current_center_x = old_hadj_value + (h_page_size / 2.0);
+ double current_center_y = old_vadj_value + (v_page_size / 2.0);
+
+ var zoom_change = zoom_in ? zoom_step : 1 / zoom_step;
+ zoom_factor *= zoom_change;
+
+ need_layout = true;
+ redraw ();
+
+ double updated_h_page = hadjustment.get_page_size ();
+ double updated_v_page = vadjustment.get_page_size ();
+
+ double new_hadj_value = (current_center_x * zoom_change) - (updated_h_page / 2.0);
+ double new_vadj_value = (current_center_y * zoom_change) - (updated_v_page / 2.0);
+
+ new_hadj_value = new_hadj_value.clamp (hadjustment.get_lower (), hadjustment.get_upper () - h_page_size);
+ new_vadj_value = new_vadj_value.clamp (vadjustment.get_lower (), vadjustment.get_upper () - v_page_size);
+
+ hadjustment.set_value (new_hadj_value);
+ vadjustment.set_value (new_vadj_value);
+ }
+
+ public void zoom_in ()
+ {
+ zoom_and_keep_focus (true);
+ }
+
+ public void zoom_out ()
+ {
+ zoom_and_keep_focus (false);
+ }
+
+ public void redraw ()
+ {
+ drawing_area.queue_draw ();
+ }
+
+ public void select_next_page ()
+ {
+ /* changing page invalidates any in-progress deskew */
+ set_deskew_mode (false);
+
+ var target_page_view = get_next_page (selected_page_view);
+ /* set page to selected */
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ book_page.is_selected = false;
+ }
+ target_page_view.page.is_selected = true;
+ select_page_view (target_page_view);
+ }
+
+ public void select_prev_page ()
+ {
+ /* changing page invalidates any in-progress deskew */
+ set_deskew_mode (false);
+
+ var target_page_view = get_prev_page (selected_page_view);
+ /* set page to selected */
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var book_page = book.get_page (i);
+ book_page.is_selected = false;
+ }
+ target_page_view.page.is_selected = true;
+ select_page_view (target_page_view);
+ }
+}
diff --git a/src/book.vala b/src/book.vala
index f1ca089a53c083fa964ac0b95e6f938e4548672e..fc3dbc7341530f6ba0ede25328ee0a9e104fe2cc 100644
--- a/src/book.vala
+++ b/src/book.vala
@@ -1,1012 +1,1026 @@
-/*
- * 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 delegate void ProgressionCallback (double fraction);
-
-// Assumes first page has index 0
-public enum FlipEverySecond {
- Even = 1,
- Odd = 0,
-}
-
-public class Book : Object
-{
- private List pages;
-
- public uint n_pages { get { return pages.length (); } }
-
- public signal void page_added (Page page);
- public signal void page_removed (Page page);
- public signal void reordered ();
- public signal void cleared ();
- public signal void changed ();
-
- public Book ()
- {
- pages = new List ();
- }
-
- ~Book ()
- {
- foreach (var page in pages)
- {
- page.pixels_changed.disconnect (page_changed_cb);
- page.crop_changed.disconnect (page_changed_cb);
- }
- }
-
- public void clear ()
- {
- foreach (var page in pages)
- {
- page.pixels_changed.disconnect (page_changed_cb);
- page.crop_changed.disconnect (page_changed_cb);
- }
- pages = null;
- cleared ();
- }
-
- private void page_changed_cb (Page page)
- {
- changed ();
- }
-
- public void append_page (Page page)
- {
- page.pixels_changed.connect (page_changed_cb);
- page.crop_changed.connect (page_changed_cb);
-
- pages.append (page);
- page_added (page);
- changed ();
- }
-
- public void move_page (Page page, uint location)
- {
- pages.remove (page);
- pages.insert (page, (int) location);
- reordered ();
- changed ();
- }
-
- public void reverse ()
- {
- var new_pages = new List ();
- foreach (var page in pages)
- new_pages.prepend (page);
- pages = (owned) new_pages;
-
- reordered ();
- changed ();
- }
-
- public void combine_sides ()
- {
- var n_front = n_pages - n_pages / 2;
- var new_pages = new List ();
- for (var i = 0; i < n_pages; i++)
- {
- if (i % 2 == 0)
- new_pages.append (pages.nth_data (i / 2));
- else
- new_pages.append (pages.nth_data (n_front + (i / 2)));
- }
- pages = (owned) new_pages;
-
- reordered ();
- changed ();
- }
-
- public void flip_every_second (FlipEverySecond flip)
- {
- var new_pages = new List ();
- for (var i = 0; i < n_pages; i++)
- {
- var page = pages.nth_data (i);
- if (i % 2 == (int)flip) {
- page.rotate_left();
- page.rotate_left();
- new_pages.append (page);
- } else {
- new_pages.append (page);
- }
- }
- pages = (owned) new_pages;
-
- reordered ();
- changed ();
- }
-
- public void combine_sides_reverse ()
- {
- var new_pages = new List ();
- for (var i = 0; i < n_pages; i++)
- {
- if (i % 2 == 0)
- new_pages.append (pages.nth_data (i / 2));
- else
- new_pages.append (pages.nth_data (n_pages - 1 - (i / 2)));
- }
- pages = (owned) new_pages;
-
- reordered ();
- changed ();
- }
-
- public void delete_page (Page page)
- {
- page.pixels_changed.disconnect (page_changed_cb);
- page.crop_changed.disconnect (page_changed_cb);
- pages.remove (page);
- page_removed (page);
- changed ();
- }
-
- public Page get_page (int page_number)
- {
- if (page_number < 0)
- page_number = (int) pages.length () + page_number;
- return pages.nth_data (page_number);
- }
-
- public uint get_page_index (Page page)
- {
- return pages.index (page);
- }
-
- public async void save_async (string mime_type, int quality, File file,
- ProgressionCallback? progress_cb, Cancellable? cancellable = null) throws Error
- {
- var book_saver = new BookSaver ();
- yield book_saver.save_async (this, mime_type, quality, file,
- progress_cb, cancellable);
- }
-
- public async void postprocess_async (string mime_type, File file, bool postproc_enabled,
- string postproc_script, string postproc_arguments, bool postproc_keep_original) throws Error
- {
- var book_saver = new BookSaver ();
- yield book_saver.postprocess_async (mime_type, file, postproc_enabled,
- postproc_script, postproc_arguments, postproc_keep_original);
- }
-}
-
-private class BookSaver
-{
- private uint n_pages;
- private int quality;
- private File file;
- private unowned ProgressionCallback progression_callback;
- private double progression;
- private Mutex progression_mutex;
- private Cancellable? cancellable;
- private AsyncQueue write_queue;
- private ThreadPool encoder;
- private SourceFunc save_async_callback;
- private Postprocessor postprocessor = new Postprocessor();
-
- /* save_async get called in the main thread to start saving. It
- * distributes all encode tasks to other threads then yield so
- * the ui can continue operating. The method then return once saving
- * is completed, cancelled, or failed */
- public async void save_async (Book book, string mime_type, int quality, File file,
- ProgressionCallback? progression_callback, Cancellable? cancellable) throws Error
- {
- var timer = new Timer ();
-
- this.n_pages = book.n_pages;
- this.quality = quality;
- this.file = file;
- this.cancellable = cancellable;
- this.save_async_callback = save_async.callback;
- this.write_queue = new AsyncQueue ();
- this.progression = 0;
- this.progression_mutex = Mutex ();
-
- /* Configure a callback that monitor saving progression */
- if (progression_callback == null)
- this.progression_callback = (fraction) =>
- {
- debug ("Save progression: %f%%", fraction*100.0);
- };
- else
- this.progression_callback = progression_callback;
-
- /* Configure an encoder */
- ThreadPoolFunc? encode_delegate = null;
- switch (mime_type)
- {
- case "image/jpeg":
- encode_delegate = encode_jpeg;
- break;
- case "image/png":
- encode_delegate = encode_png;
- break;
-#if HAVE_WEBP
- case "image/webp":
- encode_delegate = encode_webp;
- break;
-#endif
- case "application/pdf":
- encode_delegate = encode_pdf;
- break;
- }
- encoder = new ThreadPool.with_owned_data (encode_delegate, (int) get_num_processors (), false);
-
- /* Configure a writer */
- Thread writer;
-
- switch (mime_type)
- {
- case "image/jpeg":
- case "image/png":
-#if HAVE_WEBP
- case "image/webp":
-#endif
- writer = new Thread (null, write_multifile);
- break;
- case "application/pdf":
- writer = new Thread (null, write_pdf);
- break;
- default:
- writer = new Thread (null, () => null);
- break;
- }
-
- /* Issue encode tasks */
- for (var i = 0; i < n_pages; i++)
- {
- var encode_task = new EncodeTask ();
- encode_task.number = i;
- encode_task.page = book.get_page(i);
- encoder.add ((owned) encode_task);
- }
-
- /* Waiting for saving to finish */
- yield;
-
- /* Any error from any thread ends up here */
- var error = writer.join ();
- if (error != null)
- throw error;
-
- timer.stop ();
- debug ("Save time: %f seconds", timer.elapsed (null));
- }
-
- public async void postprocess_async(string mime_type, File file, bool postproc_enabled,
- string postproc_script, string postproc_arguments, bool postproc_keep_original) throws Error
- {
- if ( postproc_enabled && postproc_script.length != 0 ) {
- /* Perform post-processing */
- var timer = new Timer ();
- var return_code = postprocessor.process(postproc_script,
- mime_type, // MIME Type
- postproc_keep_original, // Keep Original
- file.get_path(), // Filename
- postproc_arguments // Arguments
- );
- if ( return_code != 0 ) {
- warning ("Postprocessing script execution failed. ");
- }
- timer.stop ();
- debug ("Postprocessing time: %f seconds", timer.elapsed (null));
- }
- }
-
- /* Those methods are run in the encoder threads pool. It process
- * one encode_task issued by save_async and reissue the result with
- * a write_task */
-
- private void encode_png (owned EncodeTask encode_task)
- {
- var page = encode_task.page;
- var icc_data = page.get_icc_data_encoded ();
- var write_task = new WriteTask ();
- var image = page.get_image (true);
-
- string[] keys = { "x-dpi", "y-dpi", "icc-profile", null };
- string[] values = { "%d".printf (page.dpi), "%d".printf (page.dpi), icc_data, null };
- if (icc_data == null)
- keys[2] = null;
-
- try
- {
- image.save_to_bufferv (out write_task.data, "png", keys, values);
- }
- catch (Error error)
- {
- write_task.error = error;
- }
- write_task.number = encode_task.number;
- write_queue.push ((owned) write_task);
-
- update_progression ();
- }
-
- private void encode_jpeg (owned EncodeTask encode_task)
- {
- var page = encode_task.page;
- var icc_data = page.get_icc_data_encoded ();
- var write_task = new WriteTask ();
- var image = page.get_image (true);
-
- string[] keys = { "x-dpi", "y-dpi", "quality", "icc-profile", null };
- string[] values = { "%d".printf (page.dpi), "%d".printf (page.dpi), "%d".printf (quality), icc_data, null };
- if (icc_data == null)
- keys[3] = null;
-
- try
- {
- image.save_to_bufferv (out write_task.data, "jpeg", keys, values);
- }
- catch (Error error)
- {
- write_task.error = error;
- }
- write_task.number = encode_task.number;
- write_queue.push ((owned) write_task);
-
- update_progression ();
- }
-
-#if HAVE_WEBP
- private void encode_webp (owned EncodeTask encode_task)
- {
- var page = encode_task.page;
- var icc_data = page.get_icc_data_encoded ();
- var write_task = new WriteTask ();
- var image = page.get_image (true);
- var webp_data = WebP.encode_rgb (image.get_pixels (),
- image.get_width (),
- image.get_height (),
- image.get_rowstride (),
- (float) quality);
-#if HAVE_COLORD
- WebP.MuxError mux_error;
- var mux = WebP.Mux.new_mux ();
- uint8[] output;
-
- mux_error = mux.set_image (webp_data, false);
- debug ("mux.set_image: %s", mux_error.to_string ());
-
- if (icc_data != null)
- {
- mux_error = mux.set_chunk ("ICCP", icc_data.data, false);
- debug ("mux.set_chunk: %s", mux_error.to_string ());
- if (mux_error != WebP.MuxError.OK)
- warning ("icc profile data not saved with page %i", encode_task.number);
- }
-
- mux_error = mux.assemble (out output);
- debug ("mux.assemble: %s", mux_error.to_string ());
- if (mux_error != WebP.MuxError.OK)
- write_task.error = new FileError.FAILED (_("Unable to encode page %i").printf (encode_task.number));
-
- write_task.data = (owned) output;
-#else
-
- if (webp_data.length == 0)
- write_task.error = new FileError.FAILED (_("Unable to encode page %i").printf (encode_task.number));
-
- write_task.data = (owned) webp_data;
-#endif
- write_task.number = encode_task.number;
- write_queue.push ((owned) write_task);
-
- update_progression ();
- }
-#endif
-
- private void encode_pdf (owned EncodeTask encode_task)
- {
- var page = encode_task.page;
- var image = page.get_image (true);
- var width = image.width;
- var height = image.height;
- unowned uint8[] pixels = image.get_pixels ();
- int depth = 8;
- string color_space = "DeviceRGB";
- string? filter = null;
- uint8[] data;
-
- if (page.is_color)
- {
- depth = 8;
- color_space = "DeviceRGB";
- var data_length = height * width * 3;
- data = new uint8[data_length];
- for (var row = 0; row < height; row++)
- {
- var in_offset = row * image.rowstride;
- var out_offset = row * width * 3;
- for (var x = 0; x < width; x++)
- {
- var in_o = in_offset + x*3;
- var out_o = out_offset + x*3;
-
- data[out_o] = pixels[in_o];
- data[out_o+1] = pixels[in_o+1];
- data[out_o+2] = pixels[in_o+2];
- }
- }
- }
- else if (page.depth == 2)
- {
- int shift_count = 6;
- depth = 2;
- color_space = "DeviceGray";
- var data_length = height * ((width * 2 + 7) / 8);
- data = new uint8[data_length];
- var offset = 0;
- for (var row = 0; row < height; row++)
- {
- /* Pad to the next line */
- if (shift_count != 6)
- {
- offset++;
- shift_count = 6;
- }
-
- var in_offset = row * image.rowstride;
- for (var x = 0; x < width; x++)
- {
- /* Clear byte */
- if (shift_count == 6)
- data[offset] = 0;
-
- /* Set bits */
- var p = pixels[in_offset + x*3];
- if (p >= 192)
- data[offset] |= 3 << shift_count;
- else if (p >= 128)
- data[offset] |= 2 << shift_count;
- else if (p >= 64)
- data[offset] |= 1 << shift_count;
-
- /* Move to the next position */
- if (shift_count == 0)
- {
- offset++;
- shift_count = 6;
- }
- else
- shift_count -= 2;
- }
- }
- }
- else if (page.depth == 1)
- {
- int mask = 0x80;
-
- depth = 1;
- color_space = "DeviceGray";
- var data_length = height * ((width + 7) / 8);
- data = new uint8[data_length];
- var offset = 0;
- for (var row = 0; row < height; row++)
- {
- /* Pad to the next line */
- if (mask != 0x80)
- {
- offset++;
- mask = 0x80;
- }
-
- var in_offset = row * image.rowstride;
- for (var x = 0; x < width; x++)
- {
- /* Clear byte */
- if (mask == 0x80)
- data[offset] = 0;
-
- /* Set bit */
- if (pixels[in_offset+x*3] != 0)
- data[offset] |= (uint8) mask;
-
- /* Move to the next bit */
- mask >>= 1;
- if (mask == 0)
- {
- offset++;
- mask = 0x80;
- }
- }
- }
- }
- else
- {
- depth = 8;
- color_space = "DeviceGray";
- var data_length = height * width;
- data = new uint8 [data_length];
- for (var row = 0; row < height; row++)
- {
- var in_offset = row * image.rowstride;
- var out_offset = row * width;
- for (var x = 0; x < width; x++)
- data[out_offset+x] = pixels[in_offset+x*3];
- }
- }
-
- /* Compress data and use zlib compression if it is smaller than JPEG.
- * zlib compression is slower in the worst case, so do JPEG first
- * and stop zlib if it exceeds the JPEG size */
- var write_task = new WriteTaskPDF ();
- uint8[]? jpeg_data = null;
- try
- {
- jpeg_data = compress_jpeg (image, quality, page.dpi);
- }
- catch (Error error)
- {
- write_task.error = error;
- }
- var zlib_data = compress_zlib (data, jpeg_data.length);
- if (zlib_data != null)
- {
- filter = "FlateDecode";
- data = zlib_data;
- }
- else
- {
- /* JPEG encoder converts to 8-bit RGB, see issue #459 */
- depth = 8;
- color_space = "DeviceRGB";
- filter = "DCTDecode";
- data = jpeg_data;
- }
-
- write_task.number = encode_task.number;
- write_task.data = data;
- write_task.width = width;
- write_task.height = height;
- write_task.color_space = color_space;
- write_task.depth = depth;
- write_task.filter = filter;
- write_task.dpi = page.dpi;
- write_queue.push (write_task);
-
- update_progression ();
- }
-
- private Error? write_multifile ()
- {
- for (var i=0; i < n_pages; i++)
- {
- if (cancellable.is_cancelled ())
- {
- finished_saving ();
- return null;
- }
-
- var write_task = write_queue.pop ();
- if (write_task.error != null)
- {
- finished_saving ();
- return write_task.error;
- }
-
- var indexed_file = make_indexed_file (file.get_uri (), write_task.number, n_pages);
- try
- {
- var stream = indexed_file.replace (null, false, FileCreateFlags.NONE);
- stream.write_all (write_task.data, null);
- }
- catch (Error error)
- {
- finished_saving ();
- return error;
- }
- }
-
- update_progression ();
- finished_saving ();
- return null;
- }
-
- /* Those methods are run in the writer thread. It receive all
- * write_tasks sent to it by the encoder threads and write those to
- * disk. */
-
- private Error? write_pdf ()
- {
- /* Generate a random ID for this file */
- var id = "";
- for (var i = 0; i < 4; i++)
- id += "%08x".printf (Random.next_int ());
-
- FileOutputStream? stream = null;
- try
- {
- stream = file.replace (null, false, FileCreateFlags.NONE, null);
- }
- catch (Error error)
- {
- finished_saving ();
- return error;
- }
- var writer = new PDFWriter (stream);
-
- /* Choose object numbers */
- var catalog_number = writer.add_object ();
- var metadata_number = writer.add_object ();
- var pages_number = writer.add_object ();
- var info_number = writer.add_object ();
- var page_numbers = new uint[n_pages];
- var page_image_numbers = new uint[n_pages];
- var page_content_numbers = new uint[n_pages];
- for (var i = 0; i < n_pages; i++)
- {
- page_numbers[i] = writer.add_object ();
- page_image_numbers[i] = writer.add_object ();
- page_content_numbers[i] = writer.add_object ();
- }
- var struct_tree_root_number = writer.add_object ();
-
- /* Header */
- writer.write_string ("%PDF-1.3\n");
-
- /* Comment with binary as recommended so file is treated as binary */
- writer.write_string ("%\xe2\xe3\xcf\xd3\n");
-
- /* Catalog */
- writer.start_object (catalog_number);
- writer.write_string ("%u 0 obj\n".printf (catalog_number));
- writer.write_string ("<<\n");
- writer.write_string ("/Type /Catalog\n");
- writer.write_string ("/Metadata %u 0 R\n".printf (metadata_number));
- writer.write_string ("/MarkInfo << /Marked true >>\n");
- writer.write_string ("/StructTreeRoot %u 0 R\n".printf (struct_tree_root_number));
- writer.write_string ("/Pages %u 0 R\n".printf (pages_number));
- writer.write_string (">>\n");
- writer.write_string ("endobj\n");
-
- /* Metadata */
- var now = new DateTime.now_local ();
- var date_string = now.format ("%FT%H:%M:%S%:z");
- /* NOTE: The id has to be hardcoded to this value according to the spec... */
- var metadata = """
-
-
- 1
- A
- Simple Scan %s
- %s
- %s
- %s
-
-
-""".printf (((unichar) 0xFEFF).to_string (), VERSION, date_string, date_string, date_string);
- writer.write_string ("\n");
- writer.start_object (metadata_number);
- writer.write_string ("%u 0 obj\n".printf (metadata_number));
- writer.write_string ("<<\n");
- writer.write_string ("/Type /Metadata\n");
- writer.write_string ("/Subtype /XML\n");
- writer.write_string ("/Length %u\n".printf (metadata.length));
- writer.write_string (">>\n");
- writer.write_string ("stream\n");
- writer.write_string (metadata);
- writer.write_string ("\n");
- writer.write_string ("endstream\n");
- writer.write_string ("endobj\n");
-
- /* Pages */
- writer.write_string ("\n");
- writer.start_object (pages_number);
- writer.write_string ("%u 0 obj\n".printf (pages_number));
- writer.write_string ("<<\n");
- writer.write_string ("/Type /Pages\n");
- writer.write_string ("/Kids [");
- for (var i = 0; i < n_pages; i++)
- writer.write_string (" %u 0 R".printf (page_numbers[i]));
- writer.write_string (" ]\n");
- writer.write_string ("/Count %u\n".printf (n_pages));
- writer.write_string (">>\n");
- writer.write_string ("endobj\n");
-
- /* Process each page in order */
- var tasks_in_standby = new Queue ();
- for (int i = 0; i < n_pages; i++)
- {
- if (cancellable.is_cancelled ())
- {
- finished_saving ();
- return null;
- }
-
- var write_task = tasks_in_standby.peek_head ();
- if (write_task != null && write_task.number == i)
- tasks_in_standby.pop_head ();
- else
- {
- while (true)
- {
- write_task = (WriteTaskPDF) write_queue.pop ();
- if (write_task.error != null)
- {
- finished_saving ();
- return write_task.error;
- }
- if (write_task.number == i)
- break;
-
- tasks_in_standby.insert_sorted (write_task, (a, b) => {return a.number - b.number;});
- }
- }
-
- var page_width = write_task.width * 72.0 / write_task.dpi;
- var page_height = write_task.height * 72.0 / write_task.dpi;
- var width_buffer = new char[double.DTOSTR_BUF_SIZE];
- var height_buffer = new char[double.DTOSTR_BUF_SIZE];
-
- /* Page */
- writer.write_string ("\n");
- writer.start_object (page_numbers[i]);
- writer.write_string ("%u 0 obj\n".printf (page_numbers[i]));
- writer.write_string ("<<\n");
- writer.write_string ("/Type /Page\n");
- writer.write_string ("/Parent %u 0 R\n".printf (pages_number));
- writer.write_string ("/Resources << /XObject << /Im%d %u 0 R >> >>\n".printf (i, page_image_numbers[i]));
- writer.write_string ("/MediaBox [ 0 0 %s %s ]\n".printf (page_width.format (width_buffer, "%.2f"), page_height.format (height_buffer, "%.2f")));
- writer.write_string ("/Contents %u 0 R\n".printf (page_content_numbers[i]));
- writer.write_string (">>\n");
- writer.write_string ("endobj\n");
-
- /* Page image */
- writer.write_string ("\n");
- writer.start_object (page_image_numbers[i]);
- writer.write_string ("%u 0 obj\n".printf (page_image_numbers[i]));
- writer.write_string ("<<\n");
- writer.write_string ("/Type /XObject\n");
- writer.write_string ("/Subtype /Image\n");
- writer.write_string ("/Width %d\n".printf (write_task.width));
- writer.write_string ("/Height %d\n".printf (write_task.height));
- writer.write_string ("/ColorSpace /%s\n".printf (write_task.color_space));
- writer.write_string ("/BitsPerComponent %d\n".printf (write_task.depth));
- writer.write_string ("/Length %d\n".printf (write_task.data.length));
- if (write_task.filter != null)
- writer.write_string ("/Filter /%s\n".printf (write_task.filter));
- writer.write_string (">>\n");
- writer.write_string ("stream\n");
- writer.write (write_task.data);
- writer.write_string ("\n");
- writer.write_string ("endstream\n");
- writer.write_string ("endobj\n");
-
- /* Structure tree */
- writer.write_string ("\n");
- writer.start_object (struct_tree_root_number);
- writer.write_string ("%u 0 obj\n".printf (struct_tree_root_number));
- writer.write_string ("<<\n");
- writer.write_string ("/Type /StructTreeRoot\n");
- writer.write_string (">>\n");
- writer.write_string ("endobj\n");
-
- /* Page contents */
- var command = "q\n%s 0 0 %s 0 0 cm\n/Im%d Do\nQ".printf (page_width.format (width_buffer, "%f"), page_height.format (height_buffer, "%f"), i);
- writer.write_string ("\n");
- writer.start_object (page_content_numbers[i]);
- writer.write_string ("%u 0 obj\n".printf (page_content_numbers[i]));
- writer.write_string ("<<\n");
- writer.write_string ("/Length %d\n".printf (command.length));
- writer.write_string (">>\n");
- writer.write_string ("stream\n");
- writer.write_string (command);
- writer.write_string ("\n");
- writer.write_string ("endstream\n");
- writer.write_string ("endobj\n");
- }
-
- /* Info */
- writer.write_string ("\n");
- writer.start_object (info_number);
- writer.write_string ("%u 0 obj\n".printf (info_number));
- writer.write_string ("<<\n");
- writer.write_string ("/Creator (Simple Scan %s)\n".printf (VERSION));
- writer.write_string (">>\n");
- writer.write_string ("endobj\n");
-
- /* Cross-reference table */
- writer.write_string ("\n");
- var xref_offset = writer.offset;
- writer.write_string ("xref\n");
- writer.write_string ("0 %zu\n".printf (writer.object_offsets.length + 1));
- writer.write_string ("%010zu 65535 f \n".printf (writer.next_empty_object (0)));
- for (var i = 0; i < writer.object_offsets.length; i++)
- if (writer.object_offsets[i] == 0)
- writer.write_string ("%010zu 65535 f \n".printf (writer.next_empty_object (i + 1)));
- else
- writer.write_string ("%010zu 00000 n \n".printf (writer.object_offsets[i]));
-
- /* Trailer */
- writer.write_string ("\n");
- writer.write_string ("trailer\n");
- writer.write_string ("<<\n");
- writer.write_string ("/Size %zu\n".printf (writer.object_offsets.length + 1));
- writer.write_string ("/Info %u 0 R\n".printf (info_number));
- writer.write_string ("/Root %u 0 R\n".printf (catalog_number));
- writer.write_string ("/ID [<%s> <%s>]\n".printf (id, id));
- writer.write_string (">>\n");
- writer.write_string ("startxref\n");
- writer.write_string ("%zu\n".printf (xref_offset));
- writer.write_string ("%%EOF\n");
-
- update_progression ();
- finished_saving ();
- return null;
- }
-
- /* update_progression is called once by page by encoder threads and
- * once at the end by writer thread. */
- private void update_progression ()
- {
- double step = 1.0 / (double)(n_pages+1);
- progression_mutex.lock ();
- progression += step;
- progression_mutex.unlock ();
- Idle.add (() =>
- {
- progression_callback (progression);
- return false;
- });
- }
-
- /* finished_saving is called by the writer thread when it's done,
- * meaning there is nothing left to do or saving has been
- * cancelled */
- private void finished_saving ()
- {
- /* At this point, any remaining encode_task ought to remain unprocessed */
- ThreadPool.free ((owned) encoder, true, true);
-
- /* Wake-up save_async method in main thread */
- Idle.add ((owned)save_async_callback);
- }
-
- /* Utility methods */
-
- private static uint8[]? compress_zlib (uint8[] data, uint max_size)
- {
- var stream = ZLib.DeflateStream (ZLib.Level.BEST_COMPRESSION);
- var out_data = new uint8[max_size];
-
- stream.next_in = data;
- stream.next_out = out_data;
- while (true)
- {
- /* Compression complete */
- if (stream.avail_in == 0)
- break;
-
- /* Out of space */
- if (stream.avail_out == 0)
- return null;
-
- if (stream.deflate (ZLib.Flush.FINISH) == ZLib.Status.STREAM_ERROR)
- return null;
- }
-
- var n_written = out_data.length - stream.avail_out;
- out_data.resize ((int) n_written);
-
- return out_data;
- }
-
- private static uint8[] compress_jpeg (Gdk.Pixbuf image, int quality, int dpi) throws Error
- {
- uint8[] jpeg_data;
- string[] keys = { "quality", "x-dpi", "y-dpi", null };
- string[] values = { "%d".printf (quality), "%d".printf (dpi), "%d".printf (dpi), null };
-
- image.save_to_bufferv (out jpeg_data, "jpeg", keys, values);
- return jpeg_data;
- }
-}
-
-private class EncodeTask
-{
- public int number;
- public Page page;
-}
-
-private class WriteTask
-{
- public int number;
- public uint8[] data;
- public Error error;
-}
-
-private class WriteTaskPDF : WriteTask
-{
- public int width;
- public int height;
- public string color_space;
- public int depth;
- public string? filter;
- public int dpi;
-}
-
-private class PDFWriter
-{
- public size_t offset = 0;
- public uint[] object_offsets;
- private FileOutputStream stream;
-
- public PDFWriter (FileOutputStream stream)
- {
- this.stream = stream;
- object_offsets = new uint[0];
- }
-
- public void write (uint8[] data)
- {
- try
- {
- stream.write_all (data, null, null);
- }
- catch (Error e)
- {
- warning ("Error writing PDF: %s", e.message);
- }
- offset += data.length;
- }
-
- public void write_string (string text)
- {
- write ((uint8[]) text.to_utf8 ());
- }
-
- public uint add_object ()
- {
- object_offsets.resize (object_offsets.length + 1);
- var index = object_offsets.length - 1;
- object_offsets[index] = 0;
- return index + 1;
- }
-
- public void start_object (uint index)
- {
- object_offsets[index - 1] = (uint)offset;
- }
-
- public int next_empty_object (int start)
- {
- for (var i = start; i < object_offsets.length; i++)
- if (object_offsets[i] == 0)
- return i + 1;
- return 0;
- }
-}
-
-public File make_indexed_file (string uri, uint i, uint n_pages)
-{
- if (n_pages == 1)
- return File.new_for_uri (uri);
-
- /* Insert index before extension */
- var basename = Path.get_basename (uri);
- string prefix = uri, suffix = "";
- var extension_index = basename.last_index_of_char ('.');
- if (extension_index >= 0)
- {
- suffix = basename.slice (extension_index, basename.length);
- prefix = uri.slice (0, uri.length - suffix.length);
- }
- var width = n_pages.to_string().length;
- var number_format = "%%0%dd".printf (width);
- var filename = prefix + "-" + number_format.printf (i + 1) + suffix;
- return File.new_for_uri (filename);
-}
+/*
+ * 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 delegate void ProgressionCallback (double fraction);
+
+// Assumes first page has index 0
+public enum FlipEverySecond {
+ Even = 1,
+ Odd = 0,
+}
+
+public class Book : Object
+{
+ private List pages;
+
+ public uint n_pages { get { return pages.length (); } }
+
+ public signal void page_added (Page page);
+ public signal void page_removed (Page page);
+ public signal void reordered ();
+ public signal void cleared ();
+ public signal void changed ();
+
+ public Book ()
+ {
+ pages = new List ();
+ }
+
+ ~Book ()
+ {
+ disconnect_all_pages ();
+ }
+
+ private void disconnect_all_pages ()
+ {
+ foreach (var page in pages)
+ {
+ page.pixels_changed.disconnect (page_changed_cb);
+ page.crop_changed.disconnect (page_changed_cb);
+ }
+ }
+
+ public void clear ()
+ {
+ disconnect_all_pages ();
+ pages = null;
+ cleared ();
+ }
+
+ private void page_changed_cb (Page page)
+ {
+ changed ();
+ }
+
+ public void append_page (Page page)
+ {
+ page.pixels_changed.connect (page_changed_cb);
+ page.crop_changed.connect (page_changed_cb);
+
+ pages.append (page);
+ page_added (page);
+ changed ();
+ }
+
+ public void move_page (Page page, uint location)
+ {
+ uint old_location = get_page_index (page);
+ /* if moving in front, the old location is shifted + 1 */
+ if (location < old_location)
+ {
+ old_location++;
+ }
+ pages.remove (page);
+ pages.insert (page, (int) location);
+ reordered ();
+ changed ();
+ }
+
+ public void delete_page (Page? page)
+ {
+ if (page == null)
+ return;
+ /* Toggle the is_deleted flag instead of removing the page */
+ page.set_page_is_deleted (!page.is_deleted);
+ reordered ();
+ changed ();
+ }
+
+ public void permanently_delete_page (Page? page)
+ {
+ if (page == null)
+ return;
+ page.pixels_changed.disconnect (page_changed_cb);
+ page.crop_changed.disconnect (page_changed_cb);
+ pages.remove (page);
+ page_removed (page);
+ changed ();
+ }
+
+ public Page get_page (int page_number)
+ {
+ if (page_number < 0)
+ page_number = (int) pages.length () + page_number;
+ return pages.nth_data (page_number);
+ }
+
+ public uint get_page_index (Page page)
+ {
+ return pages.index (page);
+ }
+
+ public async void save_async (string mime_type, int quality, File file,
+ ProgressionCallback? progress_cb, Cancellable? cancellable = null, bool selected_pages_only = false, string meta_title = "", string meta_author = "", string meta_subject = "", string meta_keywords = "", DateTime meta_created = new GLib.DateTime.now_local(), bool meta_pdfa = true) throws Error
+ {
+ var book_saver = new BookSaver ();
+ yield book_saver.save_async (this, mime_type, quality, file,
+ progress_cb, cancellable, selected_pages_only, meta_title, meta_author, meta_subject, meta_keywords, meta_created, meta_pdfa);
+ }
+
+ public async void postprocess_async (string mime_type, File file, bool postproc_enabled,
+ string postproc_script, string postproc_arguments, bool postproc_keep_original) throws Error
+ {
+ var book_saver = new BookSaver ();
+ yield book_saver.postprocess_async (mime_type, file, postproc_enabled,
+ postproc_script, postproc_arguments, postproc_keep_original);
+ }
+}
+
+private class BookSaver
+{
+ private uint n_pages;
+ private int quality;
+ private File file;
+ private unowned ProgressionCallback progression_callback;
+ private double progression;
+ private Mutex progression_mutex;
+ private Cancellable? cancellable;
+ private AsyncQueue write_queue;
+ private ThreadPool encoder;
+ private SourceFunc save_async_callback;
+ private Postprocessor postprocessor = new Postprocessor();
+ private string saver_meta_title;
+ private string saver_meta_author;
+ private string saver_meta_subject;
+ private string saver_meta_keywords;
+ private DateTime saver_meta_created;
+ private bool saver_meta_pdfa;
+
+ /* save_async get called in the main thread to start saving. It
+ * distributes all encode tasks to other threads then yield so
+ * the ui can continue operating. The method then return once saving
+ * is completed, cancelled, or failed */
+ public async void save_async (Book book, string mime_type, int quality, File file,
+ ProgressionCallback? progression_callback, Cancellable? cancellable, bool selected_pages_only = false, string meta_title, string meta_author, string meta_subject, string meta_keywords, DateTime meta_created, bool meta_pdfa) throws Error
+ {
+ saver_meta_title = meta_title;
+ saver_meta_author = meta_author;
+ saver_meta_subject = meta_subject;
+ saver_meta_keywords = meta_keywords;
+ saver_meta_created = meta_created;
+ saver_meta_pdfa = meta_pdfa;
+
+ var timer = new Timer ();
+
+ /* Count only pages to save */
+ uint n_pages_to_save = 0;
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (!page.is_deleted && (!selected_pages_only || page.is_selected))
+ n_pages_to_save++;
+ }
+ this.n_pages = n_pages_to_save;
+ this.quality = quality;
+ this.file = file;
+ this.cancellable = cancellable;
+ this.save_async_callback = save_async.callback;
+ this.write_queue = new AsyncQueue ();
+ this.progression = 0;
+ this.progression_mutex = Mutex ();
+
+ /* Configure a callback that monitor saving progression */
+ if (progression_callback == null)
+ this.progression_callback = (fraction) =>
+ {
+ debug ("Save progression: %f%%", fraction*100.0);
+ };
+ else
+ this.progression_callback = progression_callback;
+
+ /* Configure an encoder */
+ ThreadPoolFunc? encode_delegate = null;
+ switch (mime_type)
+ {
+ case "image/jpeg":
+ encode_delegate = encode_jpeg;
+ break;
+ case "image/png":
+ encode_delegate = encode_png;
+ break;
+#if HAVE_WEBP
+ case "image/webp":
+ encode_delegate = encode_webp;
+ break;
+#endif
+ case "application/pdf":
+ encode_delegate = encode_pdf;
+ break;
+ }
+ encoder = new ThreadPool.with_owned_data (encode_delegate, (int) get_num_processors (), false);
+
+ /* Configure a writer */
+ Thread writer;
+
+ switch (mime_type)
+ {
+ case "image/jpeg":
+ case "image/png":
+#if HAVE_WEBP
+ case "image/webp":
+#endif
+ writer = new Thread (null, write_multifile);
+ break;
+ case "application/pdf":
+ writer = new Thread (null, write_pdf);
+ break;
+ default:
+ writer = new Thread (null, () => null);
+ break;
+ }
+
+ /* Issue encode tasks */
+ int output_index = 0;
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page(i);
+
+ /* Skip deleted / unselected (if selected pages only) pages */
+ if (page.is_deleted || (selected_pages_only && !page.is_selected))
+ continue;
+
+ var encode_task = new EncodeTask ();
+ encode_task.number = output_index; /* Use sequential output index */
+ encode_task.page = page;
+ encoder.add ((owned) encode_task);
+ output_index++;
+ }
+
+ /* Waiting for saving to finish */
+ yield;
+
+ /* Any error from any thread ends up here */
+ var error = writer.join ();
+ if (error != null)
+ throw error;
+
+ timer.stop ();
+ debug ("Save time: %f seconds", timer.elapsed (null));
+ }
+
+ public async void postprocess_async(string mime_type, File file, bool postproc_enabled,
+ string postproc_script, string postproc_arguments, bool postproc_keep_original) throws Error
+ {
+ if ( postproc_enabled && postproc_script.length != 0 ) {
+ /* Perform post-processing */
+ var timer = new Timer ();
+ var return_code = postprocessor.process(postproc_script,
+ mime_type, // MIME Type
+ postproc_keep_original, // Keep Original
+ file.get_path(), // Filename
+ postproc_arguments // Arguments
+ );
+ if ( return_code != 0 ) {
+ warning ("Postprocessing script execution failed. ");
+ }
+ timer.stop ();
+ debug ("Postprocessing time: %f seconds", timer.elapsed (null));
+ }
+ }
+
+ /* Those methods are run in the encoder threads pool. It process
+ * one encode_task issued by save_async and reissue the result with
+ * a write_task */
+
+ private void encode_png (owned EncodeTask encode_task)
+ {
+ var page = encode_task.page;
+ var icc_data = page.get_icc_data_encoded ();
+ var write_task = new WriteTask ();
+ var image = page.get_image (true, true);
+
+ string[] keys = { "x-dpi", "y-dpi", "icc-profile", null };
+ string[] values = { "%d".printf (page.dpi), "%d".printf (page.dpi), icc_data, null };
+ if (icc_data == null)
+ keys[2] = null;
+
+ try
+ {
+ image.save_to_bufferv (out write_task.data, "png", keys, values);
+ }
+ catch (Error error)
+ {
+ write_task.error = error;
+ }
+ write_task.number = encode_task.number;
+ write_queue.push ((owned) write_task);
+
+ update_progression ();
+ }
+
+ private void encode_jpeg (owned EncodeTask encode_task)
+ {
+ var page = encode_task.page;
+ var icc_data = page.get_icc_data_encoded ();
+ var write_task = new WriteTask ();
+ var image = page.get_image (true, true);
+
+ string[] keys = { "x-dpi", "y-dpi", "quality", "icc-profile", null };
+ string[] values = { "%d".printf (page.dpi), "%d".printf (page.dpi), "%d".printf (quality), icc_data, null };
+ if (icc_data == null)
+ keys[3] = null;
+
+ try
+ {
+ image.save_to_bufferv (out write_task.data, "jpeg", keys, values);
+ }
+ catch (Error error)
+ {
+ write_task.error = error;
+ }
+ write_task.number = encode_task.number;
+ write_queue.push ((owned) write_task);
+
+ update_progression ();
+ }
+
+#if HAVE_WEBP
+ private void encode_webp (owned EncodeTask encode_task)
+ {
+ var page = encode_task.page;
+ var icc_data = page.get_icc_data_encoded ();
+ var write_task = new WriteTask ();
+ var image = page.get_image (true, true);
+ var webp_data = WebP.encode_rgb (image.get_pixels (),
+ image.get_width (),
+ image.get_height (),
+ image.get_rowstride (),
+ (float) quality);
+#if HAVE_COLORD
+ WebP.MuxError mux_error;
+ var mux = WebP.Mux.new_mux ();
+ uint8[] output;
+
+ mux_error = mux.set_image (webp_data, false);
+ debug ("mux.set_image: %s", mux_error.to_string ());
+
+ if (icc_data != null)
+ {
+ mux_error = mux.set_chunk ("ICCP", icc_data.data, false);
+ debug ("mux.set_chunk: %s", mux_error.to_string ());
+ if (mux_error != WebP.MuxError.OK)
+ warning ("icc profile data not saved with page %i", encode_task.number);
+ }
+
+ mux_error = mux.assemble (out output);
+ debug ("mux.assemble: %s", mux_error.to_string ());
+ if (mux_error != WebP.MuxError.OK)
+ write_task.error = new FileError.FAILED (_("Unable to encode page %i").printf (encode_task.number));
+
+ write_task.data = (owned) output;
+#else
+
+ if (webp_data.length == 0)
+ write_task.error = new FileError.FAILED (_("Unable to encode page %i").printf (encode_task.number));
+
+ write_task.data = (owned) webp_data;
+#endif
+ write_task.number = encode_task.number;
+ write_queue.push ((owned) write_task);
+
+ update_progression ();
+ }
+#endif
+
+ private void encode_pdf (owned EncodeTask encode_task)
+ {
+ var page = encode_task.page;
+ var image = page.get_image (true, true);
+ var width = image.width;
+ var height = image.height;
+ unowned uint8[] pixels = image.get_pixels ();
+ int depth = 8;
+ string color_space = "DeviceRGB";
+ string? filter = null;
+ uint8[] data;
+
+ if (page.is_color)
+ {
+ depth = 8;
+ color_space = "DeviceRGB";
+ var data_length = height * width * 3;
+ data = new uint8[data_length];
+ for (var row = 0; row < height; row++)
+ {
+ var in_offset = row * image.rowstride;
+ var out_offset = row * width * 3;
+ for (var x = 0; x < width; x++)
+ {
+ var in_o = in_offset + x*3;
+ var out_o = out_offset + x*3;
+
+ data[out_o] = pixels[in_o];
+ data[out_o+1] = pixels[in_o+1];
+ data[out_o+2] = pixels[in_o+2];
+ }
+ }
+ }
+ else if (page.depth == 2)
+ {
+ int shift_count = 6;
+ depth = 2;
+ color_space = "DeviceGray";
+ var data_length = height * ((width * 2 + 7) / 8);
+ data = new uint8[data_length];
+ var offset = 0;
+ for (var row = 0; row < height; row++)
+ {
+ /* Pad to the next line */
+ if (shift_count != 6)
+ {
+ offset++;
+ shift_count = 6;
+ }
+
+ var in_offset = row * image.rowstride;
+ for (var x = 0; x < width; x++)
+ {
+ /* Clear byte */
+ if (shift_count == 6)
+ data[offset] = 0;
+
+ /* Set bits */
+ var p = pixels[in_offset + x*3];
+ if (p >= 192)
+ data[offset] |= 3 << shift_count;
+ else if (p >= 128)
+ data[offset] |= 2 << shift_count;
+ else if (p >= 64)
+ data[offset] |= 1 << shift_count;
+
+ /* Move to the next position */
+ if (shift_count == 0)
+ {
+ offset++;
+ shift_count = 6;
+ }
+ else
+ shift_count -= 2;
+ }
+ }
+ }
+ else if (page.depth == 1)
+ {
+ int mask = 0x80;
+
+ depth = 1;
+ color_space = "DeviceGray";
+ var data_length = height * ((width + 7) / 8);
+ data = new uint8[data_length];
+ var offset = 0;
+ for (var row = 0; row < height; row++)
+ {
+ /* Pad to the next line */
+ if (mask != 0x80)
+ {
+ offset++;
+ mask = 0x80;
+ }
+
+ var in_offset = row * image.rowstride;
+ for (var x = 0; x < width; x++)
+ {
+ /* Clear byte */
+ if (mask == 0x80)
+ data[offset] = 0;
+
+ /* Set bit */
+ if (pixels[in_offset+x*3] != 0)
+ data[offset] |= (uint8) mask;
+
+ /* Move to the next bit */
+ mask >>= 1;
+ if (mask == 0)
+ {
+ offset++;
+ mask = 0x80;
+ }
+ }
+ }
+ }
+ else
+ {
+ depth = 8;
+ color_space = "DeviceGray";
+ var data_length = height * width;
+ data = new uint8 [data_length];
+ for (var row = 0; row < height; row++)
+ {
+ var in_offset = row * image.rowstride;
+ var out_offset = row * width;
+ for (var x = 0; x < width; x++)
+ data[out_offset+x] = pixels[in_offset+x*3];
+ }
+ }
+
+ /* Compress data and use zlib compression if it is smaller than JPEG.
+ * zlib compression is slower in the worst case, so do JPEG first
+ * and stop zlib if it exceeds the JPEG size */
+ var write_task = new WriteTaskPDF ();
+ uint8[]? jpeg_data = null;
+ try
+ {
+ jpeg_data = compress_jpeg (image, quality, page.dpi);
+ }
+ catch (Error error)
+ {
+ write_task.error = error;
+ }
+ var zlib_data = compress_zlib (data, jpeg_data.length);
+ if (zlib_data != null)
+ {
+ filter = "FlateDecode";
+ data = zlib_data;
+ }
+ else
+ {
+ /* JPEG encoder converts to 8-bit RGB, see issue #459 */
+ depth = 8;
+ color_space = "DeviceRGB";
+ filter = "DCTDecode";
+ data = jpeg_data;
+ }
+
+ write_task.number = encode_task.number;
+ write_task.data = data;
+ write_task.width = width;
+ write_task.height = height;
+ write_task.color_space = color_space;
+ write_task.depth = depth;
+ write_task.filter = filter;
+ write_task.dpi = page.dpi;
+ write_queue.push (write_task);
+
+ update_progression ();
+ }
+
+ private Error? write_multifile ()
+ {
+ for (var i=0; i < n_pages; i++)
+ {
+ if (cancellable.is_cancelled ())
+ {
+ finished_saving ();
+ return null;
+ }
+
+ var write_task = write_queue.pop ();
+ if (write_task.error != null)
+ {
+ finished_saving ();
+ return write_task.error;
+ }
+
+ var indexed_file = make_indexed_file (file.get_uri (), write_task.number, n_pages);
+ try
+ {
+ var stream = indexed_file.replace (null, false, FileCreateFlags.NONE);
+ stream.write_all (write_task.data, null);
+ }
+ catch (Error error)
+ {
+ finished_saving ();
+ return error;
+ }
+ }
+
+ update_progression ();
+ finished_saving ();
+ return null;
+ }
+
+ /* Those methods are run in the writer thread. It receive all
+ * write_tasks sent to it by the encoder threads and write those to
+ * disk. */
+
+ private Error? write_pdf ()
+ {
+ /* Generate a random ID for this file */
+ var id = "";
+ for (var i = 0; i < 4; i++)
+ id += "%08x".printf (Random.next_int ());
+
+ FileOutputStream? stream = null;
+ try
+ {
+ stream = file.replace (null, false, FileCreateFlags.NONE, null);
+ }
+ catch (Error error)
+ {
+ finished_saving ();
+ return error;
+ }
+ var writer = new PDFWriter (stream);
+
+ /* Choose object numbers */
+ var catalog_number = writer.add_object ();
+ var metadata_number = writer.add_object ();
+ var pages_number = writer.add_object ();
+ var info_number = writer.add_object ();
+ var page_numbers = new uint[n_pages];
+ var page_image_numbers = new uint[n_pages];
+ var page_content_numbers = new uint[n_pages];
+ for (var i = 0; i < n_pages; i++)
+ {
+ page_numbers[i] = writer.add_object ();
+ page_image_numbers[i] = writer.add_object ();
+ page_content_numbers[i] = writer.add_object ();
+ }
+ var struct_tree_root_number = writer.add_object ();
+
+ /* Header */
+ writer.write_string ("%PDF-1.3\n");
+
+ /* Comment with binary as recommended so file is treated as binary */
+ writer.write_string ("%\xe2\xe3\xcf\xd3\n");
+
+ /* Catalog */
+ writer.start_object (catalog_number);
+ writer.write_string ("%u 0 obj\n".printf (catalog_number));
+ writer.write_string ("<<\n");
+ writer.write_string ("/Type /Catalog\n");
+ writer.write_string ("/Metadata %u 0 R\n".printf (metadata_number));
+ writer.write_string ("/MarkInfo << /Marked true >>\n");
+ writer.write_string ("/StructTreeRoot %u 0 R\n".printf (struct_tree_root_number));
+ writer.write_string ("/Pages %u 0 R\n".printf (pages_number));
+ writer.write_string (">>\n");
+ writer.write_string ("endobj\n");
+
+ /* Metadata */
+ var pdfa_description = "";
+ if (saver_meta_pdfa)
+ {
+ pdfa_description = """
+ 1
+ A
+ """;
+ }
+ var now = new DateTime.now_local ();
+ var date_string = now.format ("%FT%H:%M:%S%:z");
+ var created_string = this.saver_meta_created.format ("%FT%H:%M:%S%:z");
+ /* NOTE: The id has to be hardcoded to this value according to the spec... */
+ var metadata = """
+
+ %s
+
+ Simple Scan %s
+ %s
+ %s
+ %s
+
+
+
+
+ %s
+
+
+
+
+ %s
+
+
+
+
+ %s
+
+
+
+
+
+ %s
+
+
+
+""".printf (((unichar) 0xFEFF).to_string (), pdfa_description, VERSION, created_string, date_string, date_string, this.saver_meta_title, this.saver_meta_author, this.saver_meta_subject, this.saver_meta_keywords);
+ writer.write_string ("\n");
+ writer.start_object (metadata_number);
+ writer.write_string ("%u 0 obj\n".printf (metadata_number));
+ writer.write_string ("<<\n");
+ writer.write_string ("/Type /Metadata\n");
+ writer.write_string ("/Subtype /XML\n");
+ writer.write_string ("/Length %u\n".printf (metadata.length));
+ writer.write_string (">>\n");
+ writer.write_string ("stream\n");
+ writer.write_string (metadata);
+ writer.write_string ("\n");
+ writer.write_string ("endstream\n");
+ writer.write_string ("endobj\n");
+
+ /* Pages */
+ writer.write_string ("\n");
+ writer.start_object (pages_number);
+ writer.write_string ("%u 0 obj\n".printf (pages_number));
+ writer.write_string ("<<\n");
+ writer.write_string ("/Type /Pages\n");
+ writer.write_string ("/Kids [");
+ for (var i = 0; i < n_pages; i++)
+ writer.write_string (" %u 0 R".printf (page_numbers[i]));
+ writer.write_string (" ]\n");
+ writer.write_string ("/Count %u\n".printf (n_pages));
+ writer.write_string (">>\n");
+ writer.write_string ("endobj\n");
+
+ /* Process each page in order */
+ var tasks_in_standby = new Queue ();
+ for (int i = 0; i < n_pages; i++)
+ {
+ if (cancellable.is_cancelled ())
+ {
+ finished_saving ();
+ return null;
+ }
+
+ var write_task = tasks_in_standby.peek_head ();
+ if (write_task != null && write_task.number == i)
+ tasks_in_standby.pop_head ();
+ else
+ {
+ while (true)
+ {
+ write_task = (WriteTaskPDF) write_queue.pop ();
+ if (write_task.error != null)
+ {
+ finished_saving ();
+ return write_task.error;
+ }
+ if (write_task.number == i)
+ break;
+
+ tasks_in_standby.insert_sorted (write_task, (a, b) => {return a.number - b.number;});
+ }
+ }
+
+ var page_width = write_task.width * 72.0 / write_task.dpi;
+ var page_height = write_task.height * 72.0 / write_task.dpi;
+ var width_buffer = new char[double.DTOSTR_BUF_SIZE];
+ var height_buffer = new char[double.DTOSTR_BUF_SIZE];
+
+ /* Page */
+ writer.write_string ("\n");
+ writer.start_object (page_numbers[i]);
+ writer.write_string ("%u 0 obj\n".printf (page_numbers[i]));
+ writer.write_string ("<<\n");
+ writer.write_string ("/Type /Page\n");
+ writer.write_string ("/Parent %u 0 R\n".printf (pages_number));
+ writer.write_string ("/Resources << /XObject << /Im%d %u 0 R >> >>\n".printf (i, page_image_numbers[i]));
+ writer.write_string ("/MediaBox [ 0 0 %s %s ]\n".printf (page_width.format (width_buffer, "%.2f"), page_height.format (height_buffer, "%.2f")));
+ writer.write_string ("/Contents %u 0 R\n".printf (page_content_numbers[i]));
+ writer.write_string (">>\n");
+ writer.write_string ("endobj\n");
+
+ /* Page image */
+ writer.write_string ("\n");
+ writer.start_object (page_image_numbers[i]);
+ writer.write_string ("%u 0 obj\n".printf (page_image_numbers[i]));
+ writer.write_string ("<<\n");
+ writer.write_string ("/Type /XObject\n");
+ writer.write_string ("/Subtype /Image\n");
+ writer.write_string ("/Width %d\n".printf (write_task.width));
+ writer.write_string ("/Height %d\n".printf (write_task.height));
+ writer.write_string ("/ColorSpace /%s\n".printf (write_task.color_space));
+ writer.write_string ("/BitsPerComponent %d\n".printf (write_task.depth));
+ writer.write_string ("/Length %d\n".printf (write_task.data.length));
+ if (write_task.filter != null)
+ writer.write_string ("/Filter /%s\n".printf (write_task.filter));
+ writer.write_string (">>\n");
+ writer.write_string ("stream\n");
+ writer.write (write_task.data);
+ writer.write_string ("\n");
+ writer.write_string ("endstream\n");
+ writer.write_string ("endobj\n");
+
+ /* Structure tree */
+ writer.write_string ("\n");
+ writer.start_object (struct_tree_root_number);
+ writer.write_string ("%u 0 obj\n".printf (struct_tree_root_number));
+ writer.write_string ("<<\n");
+ writer.write_string ("/Type /StructTreeRoot\n");
+ writer.write_string (">>\n");
+ writer.write_string ("endobj\n");
+
+ /* Page contents */
+ var command = "q\n%s 0 0 %s 0 0 cm\n/Im%d Do\nQ".printf (page_width.format (width_buffer, "%f"), page_height.format (height_buffer, "%f"), i);
+ writer.write_string ("\n");
+ writer.start_object (page_content_numbers[i]);
+ writer.write_string ("%u 0 obj\n".printf (page_content_numbers[i]));
+ writer.write_string ("<<\n");
+ writer.write_string ("/Length %d\n".printf (command.length));
+ writer.write_string (">>\n");
+ writer.write_string ("stream\n");
+ writer.write_string (command);
+ writer.write_string ("\n");
+ writer.write_string ("endstream\n");
+ writer.write_string ("endobj\n");
+ }
+
+ /* Info */
+ writer.write_string ("\n");
+ writer.start_object (info_number);
+ writer.write_string ("%u 0 obj\n".printf (info_number));
+ writer.write_string ("<<\n");
+ writer.write_string ("/Creator (Simple Scan %s)\n".printf (VERSION));
+ writer.write_string (">>\n");
+ writer.write_string ("endobj\n");
+
+ /* Cross-reference table */
+ writer.write_string ("\n");
+ var xref_offset = writer.offset;
+ writer.write_string ("xref\n");
+ writer.write_string ("0 %zu\n".printf (writer.object_offsets.length + 1));
+ writer.write_string ("%010zu 65535 f \n".printf (writer.next_empty_object (0)));
+ for (var i = 0; i < writer.object_offsets.length; i++)
+ if (writer.object_offsets[i] == 0)
+ writer.write_string ("%010zu 65535 f \n".printf (writer.next_empty_object (i + 1)));
+ else
+ writer.write_string ("%010zu 00000 n \n".printf (writer.object_offsets[i]));
+
+ /* Trailer */
+ writer.write_string ("\n");
+ writer.write_string ("trailer\n");
+ writer.write_string ("<<\n");
+ writer.write_string ("/Size %zu\n".printf (writer.object_offsets.length + 1));
+ writer.write_string ("/Info %u 0 R\n".printf (info_number));
+ writer.write_string ("/Root %u 0 R\n".printf (catalog_number));
+ writer.write_string ("/ID [<%s> <%s>]\n".printf (id, id));
+ writer.write_string (">>\n");
+ writer.write_string ("startxref\n");
+ writer.write_string ("%zu\n".printf (xref_offset));
+ writer.write_string ("%%EOF\n");
+
+ update_progression ();
+ finished_saving ();
+ return null;
+ }
+
+ /* update_progression is called once by page by encoder threads and
+ * once at the end by writer thread. */
+ private void update_progression ()
+ {
+ double step = 1.0 / (double)(n_pages+1);
+ progression_mutex.lock ();
+ progression += step;
+ progression_mutex.unlock ();
+ Idle.add (() =>
+ {
+ progression_callback (progression);
+ return false;
+ });
+ }
+
+ /* finished_saving is called by the writer thread when it's done,
+ * meaning there is nothing left to do or saving has been
+ * cancelled */
+ private void finished_saving ()
+ {
+ /* At this point, any remaining encode_task ought to remain unprocessed */
+ ThreadPool.free ((owned) encoder, true, true);
+
+ /* Wake-up save_async method in main thread */
+ Idle.add ((owned)save_async_callback);
+ }
+
+ /* Utility methods */
+
+ private static uint8[]? compress_zlib (uint8[] data, uint max_size)
+ {
+ var stream = ZLib.DeflateStream (ZLib.Level.BEST_COMPRESSION);
+ var out_data = new uint8[max_size];
+
+ stream.next_in = data;
+ stream.next_out = out_data;
+ while (true)
+ {
+ /* Compression complete */
+ if (stream.avail_in == 0)
+ break;
+
+ /* Out of space */
+ if (stream.avail_out == 0)
+ return null;
+
+ if (stream.deflate (ZLib.Flush.FINISH) == ZLib.Status.STREAM_ERROR)
+ return null;
+ }
+
+ var n_written = out_data.length - stream.avail_out;
+ out_data.resize ((int) n_written);
+
+ return out_data;
+ }
+
+ private static uint8[] compress_jpeg (Gdk.Pixbuf image, int quality, int dpi) throws Error
+ {
+ uint8[] jpeg_data;
+ string[] keys = { "quality", "x-dpi", "y-dpi", null };
+ string[] values = { "%d".printf (quality), "%d".printf (dpi), "%d".printf (dpi), null };
+
+ image.save_to_bufferv (out jpeg_data, "jpeg", keys, values);
+ return jpeg_data;
+ }
+}
+
+private class EncodeTask
+{
+ public int number;
+ public Page page;
+}
+
+private class WriteTask
+{
+ public int number;
+ public uint8[] data;
+ public Error error;
+}
+
+private class WriteTaskPDF : WriteTask
+{
+ public int width;
+ public int height;
+ public string color_space;
+ public int depth;
+ public string? filter;
+ public int dpi;
+}
+
+private class PDFWriter
+{
+ public size_t offset = 0;
+ public uint[] object_offsets;
+ private FileOutputStream stream;
+
+ public PDFWriter (FileOutputStream stream)
+ {
+ this.stream = stream;
+ object_offsets = new uint[0];
+ }
+
+ public void write (uint8[] data)
+ {
+ try
+ {
+ stream.write_all (data, null, null);
+ }
+ catch (Error e)
+ {
+ warning ("Error writing PDF: %s", e.message);
+ }
+ offset += data.length;
+ }
+
+ public void write_string (string text)
+ {
+ write ((uint8[]) text.to_utf8 ());
+ }
+
+ public uint add_object ()
+ {
+ object_offsets.resize (object_offsets.length + 1);
+ var index = object_offsets.length - 1;
+ object_offsets[index] = 0;
+ return index + 1;
+ }
+
+ public void start_object (uint index)
+ {
+ object_offsets[index - 1] = (uint)offset;
+ }
+
+ public int next_empty_object (int start)
+ {
+ for (var i = start; i < object_offsets.length; i++)
+ if (object_offsets[i] == 0)
+ return i + 1;
+ return 0;
+ }
+}
+
+public File make_indexed_file (string uri, uint i, uint n_pages)
+{
+ if (n_pages == 1)
+ return File.new_for_uri (uri);
+
+ /* Insert index before extension */
+ var basename = Path.get_basename (uri);
+ string prefix = uri, suffix = "";
+ var extension_index = basename.last_index_of_char ('.');
+ if (extension_index >= 0)
+ {
+ suffix = basename.slice (extension_index, basename.length);
+ prefix = uri.slice (0, uri.length - suffix.length);
+ }
+ var width = n_pages.to_string().length;
+ var number_format = "%%0%dd".printf (width);
+ var filename = prefix + "-" + number_format.printf (i + 1) + suffix;
+ return File.new_for_uri (filename);
+}
diff --git a/src/meson.build b/src/meson.build
index 3cafc7cd5e2042074621beb1e7d013e3c7ca0989..77e542a9e9d4ac19bf09ee3b38ae3c6f49aa4d28 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,40 +1,41 @@
-vala_args = [ '--pkg=posix', '--vapidir=' + meson.current_source_dir () ]
-dependencies = [ glib_dep, gtk_dep, libadwaita_dep, zlib_dep, cairo_dep, gdk_pixbuf_dep, gusb_dep, sane_dep ]
-if colord_dep.found ()
- vala_args += [ '-D', 'HAVE_COLORD' ]
- dependencies += colord_dep
-endif
-if packagekit_dep.found ()
- vala_args += [ '-D', 'HAVE_PACKAGEKIT' ]
- dependencies += packagekit_dep
-endif
-if webp_dep.found () and (not colord_dep.found () or webpmux_dep.found ()) # Webpmux only required if colord
- vala_args += [ '-D', 'HAVE_WEBP' ]
- dependencies += [ webp_dep, webpmux_dep ]
-endif
-
-simple_scan = executable ('simple-scan',
- [ 'config.vapi',
- 'app-window.vala',
- 'authorize-dialog.vala',
- 'book.vala',
- 'book-view.vala',
- 'drivers-dialog.vala',
- 'page.vala',
- 'page-texture.vala',
- 'page-view.vala',
- 'postprocessor.vala',
- 'preferences-dialog.vala',
- 'reorder-pages-dialog.vala',
- 'simple-scan.vala',
- 'scanner.vala',
- 'screensaver.vala',
- 'autosave-manager.vala' ] + resources,
- dependencies: dependencies,
- vala_args: vala_args,
- c_args: [ '-DVERSION="@0@"'.format (meson.project_version ()),
- '-DGETTEXT_PACKAGE="simple-scan"',
- '-DLOCALE_DIR="@0@"'.format (localedir),
- '-DI_KNOW_THE_PACKAGEKIT_GLIB2_API_IS_SUBJECT_TO_CHANGE' ],
- link_args: [ '-lm' ],
- install: true )
+vala_args = [ '--pkg=posix', '--vapidir=' + meson.current_source_dir () ]
+dependencies = [ glib_dep, gtk_dep, libadwaita_dep, zlib_dep, cairo_dep, gdk_pixbuf_dep, gusb_dep, sane_dep ]
+if colord_dep.found ()
+ vala_args += [ '-D', 'HAVE_COLORD' ]
+ dependencies += colord_dep
+endif
+if packagekit_dep.found ()
+ vala_args += [ '-D', 'HAVE_PACKAGEKIT' ]
+ dependencies += packagekit_dep
+endif
+if webp_dep.found () and (not colord_dep.found () or webpmux_dep.found ()) # Webpmux only required if colord
+ vala_args += [ '-D', 'HAVE_WEBP' ]
+ dependencies += [ webp_dep, webpmux_dep ]
+endif
+
+simple_scan = executable ('simple-scan',
+ [ 'config.vapi',
+ 'app-window.vala',
+ 'authorize-dialog.vala',
+ 'book.vala',
+ 'book-view.vala',
+ 'drivers-dialog.vala',
+ 'page.vala',
+ 'page-texture.vala',
+ 'page-view.vala',
+ 'postprocessor.vala',
+ 'preferences-dialog.vala',
+ 'reorder-pages-dialog.vala',
+ 'simple-scan.vala',
+ 'scanner.vala',
+ 'screensaver.vala',
+ 'snapshot-area.vala',
+ 'autosave-manager.vala' ] + resources,
+ dependencies: dependencies,
+ vala_args: vala_args,
+ c_args: [ '-DVERSION="@0@"'.format (meson.project_version ()),
+ '-DGETTEXT_PACKAGE="simple-scan"',
+ '-DLOCALE_DIR="@0@"'.format (localedir),
+ '-DI_KNOW_THE_PACKAGEKIT_GLIB2_API_IS_SUBJECT_TO_CHANGE' ],
+ link_args: [ '-lm' ],
+ install: true )
diff --git a/src/page-texture.vala b/src/page-texture.vala
index 1e55433b2208d681b53f19a9100db77fd66bd598..7d82891894e7352dbb13e21ae21a696af21c4a34 100644
--- a/src/page-texture.vala
+++ b/src/page-texture.vala
@@ -1,663 +1,153 @@
-/*
- * Copyright (C) 2009-2015 Canonical Ltd.
- * Author: Robert Ancell ,
- * Bartłomiej Maryńczak
- *
- * 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.
- */
-
-private class PageToPixbuf : Object
-{
- /* Image to render at current resolution */
- public Gdk.Pixbuf? pixbuf { get { return pixbuf_; } }
- private Gdk.Pixbuf? pixbuf_ = null;
-
- /* Direction of currently scanned image */
- private ScanDirection scan_direction;
-
- /* Next scan line to render */
- private int scan_line;
-
- /* Dimensions of image to generate */
- public int width;
- public int height;
-
- private static uchar get_sample (uchar[] pixels, int offset, int x, int depth, int sample)
- {
- // FIXME
- return 0xFF;
- }
-
- private static void get_pixel (Page page, int x, int y, uchar[] pixel)
- {
- switch (page.scan_direction)
- {
- case ScanDirection.TOP_TO_BOTTOM:
- break;
- case ScanDirection.BOTTOM_TO_TOP:
- x = page.scan_width - x - 1;
- y = page.scan_height - y - 1;
- break;
- case ScanDirection.LEFT_TO_RIGHT:
- var t = x;
- x = page.scan_width - y - 1;
- y = t;
- break;
- case ScanDirection.RIGHT_TO_LEFT:
- var t = x;
- x = y;
- y = page.scan_height - t - 1;
- break;
- }
-
- var depth = page.depth;
- var n_channels = page.n_channels;
- unowned uchar[] pixels = page.get_pixels ();
- var offset = page.rowstride * y;
-
- /* Optimise for 8 bit images */
- if (depth == 8 && n_channels == 3)
- {
- var o = offset + x * n_channels;
- pixel[0] = pixels[o];
- pixel[1] = pixels[o+1];
- pixel[2] = pixels[o+2];
- return;
- }
- else if (depth == 8 && n_channels == 1)
- {
- pixel[0] = pixel[1] = pixel[2] = pixels[offset + x];
- return;
- }
-
- /* Optimise for bitmaps */
- else if (depth == 1 && n_channels == 1)
- {
- var o = offset + (x / 8);
- pixel[0] = pixel[1] = pixel[2] = (pixels[o] & (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 o = offset + (x / 4);
- var sample = (pixels[o] >> block_shift[x % 4]) & 0x3;
- sample = sample * 255 / 3;
-
- pixel[0] = pixel[1] = pixel[2] = (uchar) sample;
- return;
- }
-
- /* Use slow method */
- pixel[0] = get_sample (pixels, offset, x, depth, x * n_channels);
- pixel[1] = get_sample (pixels, offset, x, depth, x * n_channels + 1);
- pixel[2] = get_sample (pixels, offset, x, depth, x * n_channels + 2);
- }
-
- private static void set_pixel (Page page, double l, double r, double t, double b, uchar[] output, int offset)
- {
- /* 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.
- */
-
- var L = (int) l;
- if (L != l)
- L++;
- var R = (int) r;
- var T = (int) t;
- if (T != t)
- T++;
- var B = (int) b;
-
- var red = 0.0;
- var green = 0.0;
- var blue = 0.0;
-
- /* Target can fit inside one source pixel
- * +-----+
- * | |
- * | +--+| +-----+-----+ +-----+ +-----+ +-----+
- * +-+--++ or | +-++ | or | +-+ | or | +--+| or | +--+
- * | +--+| | +-++ | | +-+ | | | || | | |
- * | | +-----+-----+ +-----+ +-+--++ +--+--+
- * +-----+
- */
- if ((r - l <= 1.0 && (int)r == (int)l) || (b - t <= 1.0 && (int)b == (int)t))
- {
- /* Inside */
- if ((int)l == (int)r || (int)t == (int)b)
- {
- uchar p[3];
- get_pixel (page, (int)l, (int)t, p);
- output[offset] = p[0];
- output[offset+1] = p[1];
- output[offset+2] = p[2];
- return;
- }
-
- /* Stradling horizontal edge */
- if (L > R)
- {
- uchar p[3];
- get_pixel (page, R, T-1, p);
- red += p[0] * (r-l)*(T-t);
- green += p[1] * (r-l)*(T-t);
- blue += p[2] * (r-l)*(T-t);
- for (var y = T; y < B; y++)
- {
- get_pixel (page, R, y, p);
- red += p[0] * (r-l);
- green += p[1] * (r-l);
- blue += p[2] * (r-l);
- }
- get_pixel (page, R, B, p);
- 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
- {
- uchar p[3];
- get_pixel (page, L - 1, B, p);
- red += p[0] * (b-t)*(L-l);
- green += p[1] * (b-t)*(L-l);
- blue += p[2] * (b-t)*(L-l);
- for (var x = L; x < R; x++) {
- get_pixel (page, x, B, p);
- red += p[0] * (b-t);
- green += p[1] * (b-t);
- blue += p[2] * (b-t);
- }
- get_pixel (page, R, B, p);
- red += p[0] * (b-t)*(r-R);
- green += p[1] * (b-t)*(r-R);
- blue += p[2] * (b-t)*(r-R);
- }
-
- var scale = 1.0 / ((r - l) * (b - t));
- output[offset] = (uchar)(red * scale + 0.5);
- output[offset+1] = (uchar)(green * scale + 0.5);
- output[offset+2] = (uchar)(blue * scale + 0.5);
- return;
- }
-
- /* Add the middle pixels */
- for (var x = L; x < R; x++)
- {
- for (var y = T; y < B; y++)
- {
- uchar p[3];
- get_pixel (page, x, y, p);
- red += p[0];
- green += p[1];
- blue += p[2];
- }
- }
-
- /* Add the weighted top and bottom pixels */
- for (var x = L; x < R; x++)
- {
- if (t != T)
- {
- uchar p[3];
- get_pixel (page, x, T - 1, p);
- red += p[0] * (T - t);
- green += p[1] * (T - t);
- blue += p[2] * (T - t);
- }
-
- if (b != B)
- {
- uchar p[3];
- get_pixel (page, x, B, p);
- red += p[0] * (b - B);
- green += p[1] * (b - B);
- blue += p[2] * (b - B);
- }
- }
-
- /* Add the left and right pixels */
- for (var y = T; y < B; y++)
- {
- if (l != L)
- {
- uchar p[3];
- get_pixel (page, L - 1, y, p);
- red += p[0] * (L - l);
- green += p[1] * (L - l);
- blue += p[2] * (L - l);
- }
-
- if (r != R)
- {
- uchar p[3];
- get_pixel (page, R, y, p);
- red += p[0] * (r - R);
- green += p[1] * (r - R);
- blue += p[2] * (r - R);
- }
- }
-
- /* Add the corner pixels */
- if (l != L && t != T)
- {
- uchar p[3];
- get_pixel (page, L - 1, T - 1, p);
- 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)
- {
- uchar p[3];
- get_pixel (page, R, T - 1, p);
- 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)
- {
- uchar p[3];
- get_pixel (page, R, B, p);
- 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)
- {
- uchar p[3];
- get_pixel (page, L - 1, B, p);
- 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] */
- var scale = 1.0 / ((r - l) * (b - t));
- output[offset] = (uchar)(red * scale + 0.5);
- output[offset+1] = (uchar)(green * scale + 0.5);
- output[offset+2] = (uchar)(blue * scale + 0.5);
- }
-
- public static void update_preview (Page page, ref Gdk.Pixbuf? output_image, int output_width, int output_height,
- ScanDirection scan_direction, int old_scan_line, int scan_line)
- {
- var input_width = page.width;
- var input_height = page.height;
-
- /* Create new image if one does not exist or has changed size */
- int L, R, T, B;
- if (output_image == null ||
- output_image.width != output_width ||
- output_image.height != output_height)
- {
- output_image = new Gdk.Pixbuf (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 (scan_direction)
- {
- case ScanDirection.TOP_TO_BOTTOM:
- L = 0;
- R = output_width - 1;
- T = (int)((double)old_scan_line * output_height / input_height);
- B = (int)((double)scan_line * output_height / input_height + 0.5);
- break;
- case ScanDirection.LEFT_TO_RIGHT:
- L = (int)((double)old_scan_line * output_width / input_width);
- R = (int)((double)scan_line * output_width / input_width + 0.5);
- T = 0;
- B = output_height - 1;
- break;
- case ScanDirection.BOTTOM_TO_TOP:
- L = 0;
- R = output_width - 1;
- T = (int)((double)(input_height - scan_line) * output_height / input_height);
- B = (int)((double)(input_height - old_scan_line) * output_height / input_height + 0.5);
- break;
- case ScanDirection.RIGHT_TO_LEFT:
- L = (int)((double)(input_width - scan_line) * output_width / input_width);
- R = (int)((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;
-
- return_if_fail (L >= 0);
- return_if_fail (R < output_width);
- return_if_fail (T >= 0);
- return_if_fail (B < output_height);
- return_if_fail (output_image != null);
-
- unowned uchar[] output = output_image.get_pixels ();
- var output_rowstride = output_image.rowstride;
- var output_n_channels = output_image.n_channels;
-
- if (!page.has_data)
- {
- for (var x = L; x <= R; x++)
- for (var y = T; y <= B; y++)
- {
- var o = output_rowstride * y + x * output_n_channels;
- output[o] = output[o+1] = output[o+2] = 0xFF;
- }
- return;
- }
-
- /* Update changed area */
- for (var x = L; x <= R; x++)
- {
- var l = (double)x * input_width / output_width;
- var r = (double)(x + 1) * input_width / output_width;
-
- for (var y = T; y <= B; y++)
- {
- var t = (double)y * input_height / output_height;
- var b = (double)(y + 1) * input_height / output_height;
-
- set_pixel (page,
- l, r, t, b,
- output, output_rowstride * y + x * output_n_channels);
- }
- }
- }
-
- public void update (Page page)
- {
- var old_scan_line = scan_line;
- scan_line = page.scan_line;
-
- /* Delete old image if scan direction changed */
- var left_steps = scan_direction - page.scan_direction;
- if (left_steps != 0 && pixbuf_ != null)
- pixbuf_ = null;
- scan_direction = page.scan_direction;
-
- update_preview (page,
- ref pixbuf_,
- width,
- height,
- page.scan_direction,
- old_scan_line,
- scan_line);
- }
-}
-
-/**
- * Just update texture contents
- */
-private class TextureUpdateTask
-{
- public Page page;
-}
-
-/**
- * Resize the texture
- */
-private class TextureResizeTask: TextureUpdateTask
-{
- public int width { get; private set; }
- public int height { get; private set; }
-
- public TextureResizeTask (int width, int height)
- {
- this.width = width;
- this.height = height;
- }
-}
-
-public class PageViewTexture : Object
-{
- public Gdk.Pixbuf? pixbuf { get; private set; }
- public signal void new_buffer ();
-
- private int requested_width;
- private int requested_height;
- private TextureUpdateTask queued = null;
-
- private bool in_proggres;
-
- private ThreadPool resize_pool;
-
- private Page page;
-
- public PageViewTexture (Page page)
- {
- this.page = page;
-
- try {
- resize_pool = new ThreadPool.with_owned_data (thread_func, 1, false);
- }
- catch (ThreadError error)
- {
- // Pool is non-exclusive so this should never happen
- }
- }
-
- /**
- * Notify that data needs updating (eg. pixels changed during scanning process)
- */
- public void request_update ()
- {
- queued = new TextureUpdateTask ();
- }
-
- /**
- * Set size of the page, ignored if size did not change.
- */
- public void request_resize (int width, int height)
- {
- if (requested_width == width && requested_height == height)
- {
- return;
- }
-
- requested_width = width;
- requested_height = height;
-
- queued = new TextureResizeTask (requested_width, requested_height);
- }
-
- public void queue_update () throws ThreadError
- {
- if (in_proggres || queued == null)
- {
- return;
- }
-
- in_proggres = true;
-
- // We copy the page as it will be sent to resize thread
- queued.page = page.copy ();
- resize_pool.add (queued);
-
- queued = null;
- }
-
- private PageToPixbuf page_view = new PageToPixbuf ();
- private void thread_func(owned TextureUpdateTask task)
- {
- if (task is TextureResizeTask)
- {
- page_view.width = task.width;
- page_view.height = task.height;
- }
-
- page_view.update (task.page);
-
- Gdk.Pixbuf? new_pixbuf = null;
- if (page_view.pixbuf != null)
- {
- // We are sending this buffer back to main thread, therefore copy
- new_pixbuf = page_view.pixbuf.copy ();
- }
-
-
- Idle.add(() => {
- new_pixbuf_cb (new_pixbuf);
- return false;
- });
- }
-
- private void new_pixbuf_cb (Gdk.Pixbuf? pixbuf)
- {
- in_proggres = false;
- this.pixbuf = pixbuf;
- new_buffer ();
- }
-}
-
-public class PagePaintable: Gdk.Paintable, Object
-{
- private Page page;
- private PageViewTexture page_texture;
- private Gdk.Texture? texture;
-
- public PagePaintable (Page page)
- {
- this.page = page;
- page.pixels_changed.connect (pixels_changed);
- page.size_changed.connect (pixels_changed);
- page.scan_direction_changed.connect (pixels_changed);
-
- page_texture = new PageViewTexture (page);
- page_texture.new_buffer.connect (texture_updated);
-
- pixels_changed ();
- }
-
- ~PagePaintable ()
- {
- page.pixels_changed.disconnect (pixels_changed);
- page.size_changed.disconnect (pixels_changed);
- page.scan_direction_changed.disconnect (pixels_changed);
- page_texture.new_buffer.disconnect (texture_updated);
- }
-
- private void pixels_changed ()
- {
- page_texture.request_update ();
- try {
- page_texture.queue_update ();
- }
- catch (Error e)
- {
- warning ("Failed to queue_update of the texture: %s", e.message);
- invalidate_contents ();
- }
- }
-
- private void texture_updated ()
- {
- if (page_texture.pixbuf != null)
- texture = Gdk.Texture.for_pixbuf(page_texture.pixbuf);
- else
- texture = null;
-
- invalidate_contents ();
- }
-
- public override double get_intrinsic_aspect_ratio ()
- {
- return (double) page.width / (double) page.height;
- }
-
- public void snapshot (Gdk.Snapshot gdk_snapshot, double width, double height) {
- var snapshot = (Gtk.Snapshot) gdk_snapshot;
-
- var rect = Graphene.Rect();
- rect.size.width = (float) width;
- rect.size.height = (float) height;
-
- page_texture.request_resize ((int) width, (int) height);
-
- try {
- page_texture.queue_update ();
- }
- catch (Error e)
- {
- warning ("Failed to queue_update of the texture: %s", e.message);
- // Ask for another redraw
- invalidate_contents ();
- }
-
- if (texture != null)
- {
- snapshot.append_texture(texture, rect);
- }
- else
- {
- snapshot.append_color ({1.0f, 1.0f, 1.0f, 1.0f}, rect);
- }
- }
-
-}
\ No newline at end of file
+/*
+ * Copyright (C) 2009-2015 Canonical Ltd.
+ * Author: Robert Ancell ,
+ * Bartłomiej Maryńczak
+ *
+ * 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.
+ */
+
+/*
+ * Updates the texture on Page.pixels_changed (). The texture is always stored
+ * in the page's raw (TOP_TO_BOTTOM) orientation (and rotated when drawing the texture).
+ * The texture is rebuilt whenever the page's pixel data change (scaling up/down,
+ * scan progress, color filters, deskew, page append).
+ *
+ * To keep up with bursty pixel updates during a live scan (where each scan-line
+ * batch fires pixels_changed) we coalesce: while a rebuild is already scheduled
+ * or running, additional pixels_changed signals just set a "redo" flag.
+ */
+public class PageViewTexture : Object
+{
+ public Gdk.Texture? texture { get; private set; }
+
+ public signal void texture_changed ();
+
+ private Page page;
+
+ /* Do not update "multiple" times if already rebuilding -
+ just once more if rebuilt. */
+ private bool pending;
+ private bool pending_redo;
+
+ public PageViewTexture (Page page)
+ {
+ this.page = page;
+ page.pixels_changed.connect (schedule_rebuild);
+
+ /* Build an initial texture if pixels are already available */
+ schedule_rebuild ();
+ }
+
+ ~PageViewTexture ()
+ {
+ page.pixels_changed.disconnect (schedule_rebuild);
+ }
+
+ public void schedule_rebuild ()
+ {
+ if (pending)
+ {
+ /* Already running, schedule one more pass after that */
+ pending_redo = true;
+ return;
+ }
+
+ pending = true;
+ Idle.add (() => {
+ run_rebuild ();
+ return false;
+ });
+ }
+
+ private void run_rebuild ()
+ {
+ pending_redo = false;
+ rebuild_texture ();
+
+ /* If another pixels_changed arrived while we were rebuilding,
+ run again. */
+ if (pending_redo)
+ {
+ Idle.add (() => {
+ run_rebuild ();
+ return false;
+ });
+ }
+ else
+ {
+ pending = false;
+ }
+ }
+
+ /* Rebuild the texture from page.pixels. The result is in the raw scan
+ (TOP_TO_BOTTOM) orientation regardless of page.scan_direction. */
+ private void rebuild_texture ()
+ {
+ if (!page.has_data)
+ {
+ texture = null;
+ texture_changed ();
+ return;
+ }
+
+ page.acquire_pixel_lock ();
+
+ bool scaled = page.is_scaled_down ();
+ int w = scaled ? page.pixels_width : page.scan_width;
+ int h = scaled ? page.pixels_height : page.scan_height;
+
+ if (w <= 0 || h <= 0)
+ {
+ page.release_pixel_lock ();
+ texture = null;
+ texture_changed ();
+ return;
+ }
+
+ /* Allocate a tightly-packed R8G8B8 buffer. */
+ int stride = w * 3;
+ uint8[] data = new uint8[stride * h];
+
+ if (scaled)
+ {
+ /* Scaled-down buffer is already RGB without padding: copy it as-is. */
+ unowned uchar[] src = page.get_pixels ();
+ int src_stride = page.get_pixels_rowstride ();
+ for (int y = 0; y < h; y++)
+ Memory.copy ((uint8*) data + y * stride,
+ (uint8*) src + y * src_stride,
+ (size_t) stride);
+ }
+ else
+ {
+ /* Page.pixels may be in any format (depth, n_channels):
+ convert per pixel using Page.get_pixel */
+ for (int y = 0; y < h; y++)
+ {
+ for (int x = 0; x < w; x++)
+ {
+ page.get_pixel (x, y, data, 3 * (y * w + x), true);
+ }
+ }
+ }
+ page.release_pixel_lock ();
+
+ var bytes = new GLib.Bytes.take ((owned) data);
+ texture = new Gdk.MemoryTexture (w, h, Gdk.MemoryFormat.R8G8B8, bytes, (size_t) stride);
+ texture_changed ();
+ }
+
+ public int get_width ()
+ {
+ return texture.get_width ();
+ }
+
+ public int get_height ()
+ {
+ return texture.get_height ();
+ }
+}
diff --git a/src/page-view.vala b/src/page-view.vala
index 08feb997f07e95b168ffaad0d426bc1c8f94e7c1..6334935f366163adc6da29cb77ec2cac24beae1a 100644
--- a/src/page-view.vala
+++ b/src/page-view.vala
@@ -1,670 +1,889 @@
-/*
- * 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 CropLocation
-{
- NONE = 0,
- MIDDLE,
- TOP,
- BOTTOM,
- LEFT,
- RIGHT,
- TOP_LEFT,
- TOP_RIGHT,
- BOTTOM_LEFT,
- BOTTOM_RIGHT
-}
-
-public class PageView : Object
-{
- /* Page being rendered */
- public Page page { get; private set; }
-
- /* Image to render at current resolution */
- private PageViewTexture page_texture;
-
- /* Border around image */
- private bool selected_ = false;
- public bool selected
- {
- get { return selected_; }
- set
- {
- if ((this.selected && selected) || (!this.selected && !selected))
- return;
- this.selected = selected;
- changed ();
- }
- }
-
- private int ruler_width = 8;
-
- private int border_width = 2;
-
- /* True if image needs to be regenerated */
- private bool update_image = true;
-
- /* Dimensions of image to generate */
- private int width_;
- private int height_;
-
- /* Location to place this page */
- public int x_offset { get; set; }
- public int y_offset { get; set; }
-
- private CropLocation crop_location;
- private double selected_crop_px;
- private double selected_crop_py;
- private int selected_crop_x;
- private int selected_crop_y;
- private int selected_crop_w;
- private int selected_crop_h;
-
- /* Cursor over this page */
- public string cursor { get; private set; default = "default"; }
-
- private int animate_n_segments = 7;
- private int animate_segment;
- private uint animate_timeout;
-
- public signal void size_changed ();
- public signal void changed ();
-
- public PageView (Page page)
- {
- this.page = page;
- page.pixels_changed.connect (page_pixels_changed_cb);
- page.size_changed.connect (page_size_changed_cb);
- page.crop_changed.connect (page_overlay_changed_cb);
- page.scan_line_changed.connect (page_overlay_changed_cb);
- page.scan_direction_changed.connect (scan_direction_changed_cb);
-
- page_texture = new PageViewTexture(page);
- page_texture.new_buffer.connect (new_buffer_cb);
- }
-
- ~PageView ()
- {
- page.pixels_changed.disconnect (page_pixels_changed_cb);
- page.size_changed.disconnect (page_size_changed_cb);
- page.crop_changed.disconnect (page_overlay_changed_cb);
- page.scan_line_changed.disconnect (page_overlay_changed_cb);
- page.scan_direction_changed.disconnect (scan_direction_changed_cb);
-
- page_texture.new_buffer.disconnect (new_buffer_cb);
- }
-
- private void new_buffer_cb()
- {
- changed ();
- }
-
- private int get_preview_width ()
- {
- return width_ - (border_width + ruler_width) * 2;
- }
-
- private int get_preview_height ()
- {
- return height_ - (border_width + ruler_width) * 2;
- }
-
- private int page_to_screen_x (int x)
- {
- return (int) ((double)x * get_preview_width () / page.width + 0.5);
- }
-
- private int page_to_screen_y (int y)
- {
- return (int) ((double)y * get_preview_height () / page.height + 0.5);
- }
-
- private int screen_to_page_x (int x)
- {
- return (int) ((double)x * page.width / get_preview_width () + 0.5);
- }
-
- private int screen_to_page_y (int y)
- {
- return (int) ((double)y * page.height / get_preview_height () + 0.5);
- }
-
- private CropLocation get_crop_location (int x, int y)
- {
- if (!page.has_crop)
- return CropLocation.NONE;
-
- var cx = page.crop_x;
- var cy = page.crop_y;
- var cw = page.crop_width;
- var ch = page.crop_height;
- var dx = page_to_screen_x (cx) + border_width + ruler_width;
- var dy = page_to_screen_y (cy) + border_width + ruler_width;
- var dw = page_to_screen_x (cw) + border_width + ruler_width;
- var dh = page_to_screen_y (ch) + border_width + ruler_width;
- var ix = x - dx;
- var iy = y - dy;
-
- if (ix < 0 || ix > dw || iy < 0 || iy > dh)
- return CropLocation.NONE;
-
- /* Can't resize named crops */
- var name = page.crop_name;
- if (name != null)
- return CropLocation.MIDDLE;
-
- /* Adjust borders so can select */
- int crop_border = 20;
- 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 CropLocation.TOP_LEFT;
- /* Top right */
- if (ix > dw - crop_border && iy < crop_border)
- return CropLocation.TOP_RIGHT;
- /* Bottom left */
- if (ix < crop_border && iy > dh - crop_border)
- return CropLocation.BOTTOM_LEFT;
- /* Bottom right */
- if (ix > dw - crop_border && iy > dh - crop_border)
- return CropLocation.BOTTOM_RIGHT;
-
- /* Left */
- if (ix < crop_border)
- return CropLocation.LEFT;
- /* Right */
- if (ix > dw - crop_border)
- return CropLocation.RIGHT;
- /* Top */
- if (iy < crop_border)
- return CropLocation.TOP;
- /* Bottom */
- if (iy > dh - crop_border)
- return CropLocation.BOTTOM;
-
- /* In the middle */
- return CropLocation.MIDDLE;
- }
-
- public void button_press (int x, int y)
- {
- /* See if selecting crop */
- var location = get_crop_location (x, y);
- if (location != CropLocation.NONE)
- {
- crop_location = location;
- selected_crop_px = x;
- selected_crop_py = y;
- selected_crop_x = page.crop_x;
- selected_crop_y = page.crop_y;
- selected_crop_w = page.crop_width;
- selected_crop_h = page.crop_height;
- }
- }
-
- public void motion (int x, int y)
- {
- var location = get_crop_location (x, y);
-
- string cursor;
- switch (location)
- {
- case CropLocation.MIDDLE:
- cursor = "all-resize";
- break;
- case CropLocation.TOP:
- cursor = "n-resize";
- break;
- case CropLocation.BOTTOM:
- cursor = "s-resize";
- break;
- case CropLocation.LEFT:
- cursor = "w-resize";
- break;
- case CropLocation.RIGHT:
- cursor = "e-resize";
- break;
- case CropLocation.TOP_LEFT:
- cursor = "nw-resize";
- break;
- case CropLocation.TOP_RIGHT:
- cursor = "ne-resize";
- break;
- case CropLocation.BOTTOM_LEFT:
- cursor = "sw-resize";
- break;
- case CropLocation.BOTTOM_RIGHT:
- cursor = "se-resize";
- break;
- default:
- cursor = "default";
- break;
- }
-
- if (crop_location == CropLocation.NONE)
- {
- this.cursor = cursor;
- return;
- }
-
- /* Move the crop */
- var pw = page.width;
- var ph = page.height;
- var cw = page.crop_width;
- var ch = page.crop_height;
-
- var dx = screen_to_page_x (x - (int) selected_crop_px);
- var dy = screen_to_page_y (y - (int) selected_crop_py);
-
- var new_x = selected_crop_x;
- var new_y = selected_crop_y;
- var new_w = selected_crop_w;
- var new_h = selected_crop_h;
-
- /* Limit motion to remain within page and minimum crop size */
- var min_size = screen_to_page_x (15);
- if (crop_location == CropLocation.TOP_LEFT ||
- crop_location == CropLocation.LEFT ||
- crop_location == CropLocation.BOTTOM_LEFT)
- {
- if (dx > new_w - min_size)
- dx = new_w - min_size;
- if (new_x + dx < 0)
- dx = -new_x;
- }
- if (crop_location == CropLocation.TOP_LEFT ||
- crop_location == CropLocation.TOP ||
- crop_location == CropLocation.TOP_RIGHT)
- {
- if (dy > new_h - min_size)
- dy = new_h - min_size;
- if (new_y + dy < 0)
- dy = -new_y;
- }
-
- if (crop_location == CropLocation.TOP_RIGHT ||
- crop_location == CropLocation.RIGHT ||
- crop_location == CropLocation.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 (crop_location == CropLocation.BOTTOM_LEFT ||
- crop_location == CropLocation.BOTTOM ||
- crop_location == CropLocation.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 (crop_location == CropLocation.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 (crop_location == CropLocation.MIDDLE)
- {
- new_x += dx;
- new_y += dy;
- }
- if (crop_location == CropLocation.TOP_LEFT ||
- crop_location == CropLocation.LEFT ||
- crop_location == CropLocation.BOTTOM_LEFT)
- {
- new_x += dx;
- new_w -= dx;
- }
- if (crop_location == CropLocation.TOP_LEFT ||
- crop_location == CropLocation.TOP ||
- crop_location == CropLocation.TOP_RIGHT)
- {
- new_y += dy;
- new_h -= dy;
- }
-
- if (crop_location == CropLocation.TOP_RIGHT ||
- crop_location == CropLocation.RIGHT ||
- crop_location == CropLocation.BOTTOM_RIGHT)
- new_w += dx;
- if (crop_location == CropLocation.BOTTOM_LEFT ||
- crop_location == CropLocation.BOTTOM ||
- crop_location == CropLocation.BOTTOM_RIGHT)
- new_h += dy;
-
- page.move_crop (new_x, new_y);
-
- /* If reshaped crop, must be a custom crop */
- if (new_w != cw || new_h != ch)
- page.set_custom_crop (new_w, new_h);
- }
-
- public void button_release (int x, int y)
- {
- /* Complete crop */
- crop_location = CropLocation.NONE;
- changed ();
- }
-
- private bool animation_cb ()
- {
- animate_segment = (animate_segment + 1) % animate_n_segments;
- changed ();
- return true;
- }
-
- private void update_animation ()
- {
- bool animate, is_animating;
-
- animate = page.is_scanning && !page.has_data;
- is_animating = animate_timeout != 0;
- if (animate == is_animating)
- return;
-
- if (animate)
- {
- animate_segment = 0;
- if (animate_timeout == 0)
- animate_timeout = Timeout.add (150, animation_cb);
- }
- else
- {
- if (animate_timeout != 0)
- Source.remove (animate_timeout);
- animate_timeout = 0;
- }
- }
-
- /* It is necessary to ask the ruler color since it is themed with the GTK */
- /* theme foreground color, and this class doesn't have any GTK widget */
- /* available to lookup the color. */
- public void render (Cairo.Context context, Gdk.RGBA ruler_color)
- {
- update_animation ();
-
- page_texture.request_resize (get_preview_width (), get_preview_height ());
-
- try {
- page_texture.queue_update ();
- }
- catch (Error e)
- {
- warning ("Failed to queue_update of the texture: %s", e.message);
- // Ask for another redraw
- changed ();
- }
-
- var w = get_preview_width ();
- var h = get_preview_height ();
-
- context.set_line_width (1);
- context.translate (x_offset, y_offset);
-
- /* Draw image */
- context.translate (border_width + ruler_width, border_width + ruler_width);
-
- if (page_texture.pixbuf != null)
- {
- float x_scale = (float) w / (float) page_texture.pixbuf.width;
- float y_scale = (float) h / (float) page_texture.pixbuf.height;
-
- context.save ();
- context.scale(x_scale, y_scale);
-
- // context.rectangle (0, 0.0, w, h);
- Gdk.cairo_set_source_pixbuf (context, page_texture.pixbuf, 0, 0);
- context.paint ();
- context.restore ();
- }
- else
- {
- Gdk.cairo_set_source_rgba (context, {1.0f, 1.0f, 1.0f, 1.0f});
- context.rectangle (0, 0.0, w, h);
- context.fill ();
- }
-
- /* Draw page border */
- Gdk.cairo_set_source_rgba (context, ruler_color);
- context.set_line_width (border_width);
-
- context.rectangle (0,
- 0.0,
- w,
- h);
- context.stroke ();
-
- /* Draw horizontal ruler */
- context.set_line_width (1);
- var ruler_tick = 0;
- var line = 0.0;
- var big_ruler_tick = 5;
-
- while (ruler_tick <= page.width)
- {
- line = page_to_screen_x (ruler_tick) + 0.5;
- if (big_ruler_tick == 5)
- {
- context.move_to (line, 0);
- context.line_to (line, -ruler_width);
- context.move_to (line, h);
- context.line_to (line, h + ruler_width);
- big_ruler_tick = 0;
- }
- else
- {
- context.move_to (line, -2);
- context.line_to (line, -5);
- context.move_to (line, h + 2);
- context.line_to (line, h + 5);
- }
- ruler_tick = ruler_tick + page.dpi/5;
- big_ruler_tick = big_ruler_tick + 1;
- }
- context.stroke ();
-
- /* Draw vertical ruler */
- ruler_tick = 0;
- line = 0.0;
- big_ruler_tick = 5;
- while (ruler_tick <= page.height)
- {
- line = page_to_screen_y (ruler_tick) + 0.5;
-
- if (big_ruler_tick == 5)
- {
- context.move_to (0, line);
- context.line_to (-ruler_width, line);
-
- context.move_to (w, line);
- context.line_to (w + ruler_width, line);
- big_ruler_tick = 0;
- }
- else
- {
- context.move_to (-2, line);
- context.line_to (-5, line);
-
- context.move_to (w + 2, line);
- context.line_to (w + 5, line);
- }
- ruler_tick = ruler_tick + page.dpi/5;
- big_ruler_tick = big_ruler_tick + 1;
- }
- context.stroke ();
-
- /* Draw scan line */
- if (page.is_scanning && page.scan_line > 0)
- {
- var scan_line = page.scan_line;
-
- double s;
- double x1, y1, x2, y2;
- switch (page.scan_direction)
- {
- case ScanDirection.TOP_TO_BOTTOM:
- s = page_to_screen_y (scan_line);
- x1 = 0; y1 = s + 0.5;
- x2 = w; y2 = s + 0.5;
- break;
- case ScanDirection.BOTTOM_TO_TOP:
- s = page_to_screen_y (scan_line);
- x1 = 0; y1 = h - s + 0.5;
- x2 = w; y2 = h - s + 0.5;
- break;
- case ScanDirection.LEFT_TO_RIGHT:
- s = page_to_screen_x (scan_line);
- x1 = s + 0.5; y1 = 0;
- x2 = s + 0.5; y2 = h;
- break;
- case ScanDirection.RIGHT_TO_LEFT:
- s = page_to_screen_x (scan_line);
- x1 = w - s + 0.5; y1 = 0;
- x2 = w - s + 0.5; y2 = h;
- break;
- default:
- x1 = y1 = x2 = y2 = 0;
- break;
- }
-
- context.move_to (x1, y1);
- context.line_to (x2, y2);
- context.set_source_rgb (1.0, 0.0, 0.0);
- context.stroke ();
- }
-
- /* Draw crop */
- if (page.has_crop)
- {
- var x = page.crop_x;
- var y = page.crop_y;
- var crop_width = page.crop_width;
- var crop_height = page.crop_height;
-
- var dx = page_to_screen_x (x);
- var dy = page_to_screen_y (y);
- var dw = page_to_screen_x (crop_width);
- var dh = page_to_screen_y (crop_height);
-
- /* Shade out cropped area */
- context.rectangle (0, 0, w, h);
- context.new_sub_path ();
- context.rectangle (dx, dy, dw, dh);
- context.set_fill_rule (Cairo.FillRule.EVEN_ODD);
- context.set_source_rgba (0.25, 0.25, 0.25, 0.2);
- context.fill ();
-
- /* Show new edge */
- context.set_source_rgb (1.0, 1.0, 1.0);
- context.move_to (-border_width, dy - 1.5);
- context.line_to (border_width + w, dy - 1.5);
- context.move_to (-border_width, dy + dh + 1.5);
- context.line_to (border_width + w, dy + dh + 1.5);
- context.stroke ();
-
- context.move_to (dx - 1.5, -border_width);
- context.line_to (dx - 1.5, border_width + h);
- context.move_to (dx + dw + 1.5, -border_width);
- context.line_to (dx + dw + 1.5, border_width + h);
- context.stroke ();
-
- context.rectangle (dx - 0.5, dy - 0.5, dw + 1, dh + 1);
- context.set_source_rgb (0.0, 0.0, 0.0);
- context.stroke ();
- }
- }
-
- public int width
- {
- get { return width_; }
- set
- {
- // FIXME: Automatically update when get updated image
- var h = (int) ((double) value * page.height / page.width);
- if (width_ == value && height_ == h)
- return;
-
- width_ = value;
- height_ = h;
-
- /* Regenerate image */
- update_image = true;
-
- size_changed ();
- changed ();
- }
- }
-
- public int height
- {
- get { return height_; }
- set
- {
- // FIXME: Automatically update when get updated image
- var w = (int) ((double) value * page.width / page.height);
- if (width_ == w && height_ == value)
- return;
-
- width_ = w;
- height_ = value;
-
- /* Regenerate image */
- update_image = true;
-
- size_changed ();
- changed ();
- }
- }
-
- private void page_pixels_changed_cb (Page p)
- {
- /* Regenerate image */
- update_image = true;
- page_texture.request_update ();
- changed ();
- }
-
- private void page_size_changed_cb (Page p)
- {
- /* Regenerate image */
- update_image = true;
- size_changed ();
- changed ();
- }
-
- private void page_overlay_changed_cb (Page p)
- {
- changed ();
- }
-
- private void scan_direction_changed_cb (Page p)
- {
- /* Regenerate image */
- update_image = true;
- page_texture.request_update ();
- size_changed ();
- changed ();
- }
-}
+/*
+ * 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 CropLocation
+{
+ NONE = 0,
+ MIDDLE,
+ TOP,
+ BOTTOM,
+ LEFT,
+ RIGHT,
+ TOP_LEFT,
+ TOP_RIGHT,
+ BOTTOM_LEFT,
+ BOTTOM_RIGHT
+}
+
+public class PageView : Object
+{
+ /* Page being rendered */
+ public Page page { get; private set; }
+
+ /* Image to render at current resolution */
+ private PageViewTexture page_texture;
+
+ /* Border around image */
+ private bool selected_ = false;
+ public bool selected
+ {
+ get { return selected_; }
+ set
+ {
+ if ((this.selected && selected) || (!this.selected && !selected))
+ return;
+ this.selected = selected;
+ changed ();
+ }
+ }
+
+ private int ruler_width = 8;
+
+ private int border_width = 2;
+
+ /* Dimensions of image to generate */
+ private int width_;
+ private int height_;
+
+ /* Location to place this page */
+ public int x_offset { get; set; }
+ public int y_offset { get; set; }
+
+ private CropLocation crop_location;
+ private double selected_crop_px;
+ private double selected_crop_py;
+ private int selected_crop_x;
+ private int selected_crop_y;
+ private int selected_crop_w;
+ private int selected_crop_h;
+
+ /* Cursor over this page */
+ public string cursor { get; private set; default = "default"; }
+
+ private int animate_n_segments = 7;
+ private int animate_segment;
+ private uint animate_timeout;
+
+ public signal void size_changed ();
+ public signal void changed ();
+ public signal void scan_progress_changed (double progress);
+
+ public PageView (Page page)
+ {
+ this.page = page;
+ page.pixels_changed.connect (page_pixels_changed_cb);
+ page.size_changed.connect (page_size_changed_cb);
+ page.crop_changed.connect (page_overlay_changed_cb);
+ page.scan_line_changed.connect (page_scan_line_changed_cb);
+ page.scan_direction_changed.connect (scan_direction_changed_cb);
+ page.is_deleted_changed.connect (page_overlay_changed_cb);
+
+ page_texture = new PageViewTexture (page);
+ page_texture.texture_changed.connect (texture_changed_cb);
+ }
+
+ ~PageView ()
+ {
+ page.pixels_changed.disconnect (page_pixels_changed_cb);
+ page.size_changed.disconnect (page_size_changed_cb);
+ page.crop_changed.disconnect (page_overlay_changed_cb);
+ page.scan_line_changed.disconnect (page_scan_line_changed_cb);
+ page.scan_direction_changed.disconnect (scan_direction_changed_cb);
+ page.is_deleted_changed.disconnect (page_overlay_changed_cb);
+
+ page_texture.texture_changed.disconnect (texture_changed_cb);
+ }
+
+ private void texture_changed_cb ()
+ {
+ changed ();
+ }
+
+ public int get_texture_width ()
+ {
+ return page_texture.get_width ();
+ }
+
+ public int get_texture_height ()
+ {
+ return page_texture.get_height ();
+ }
+
+ public int get_preview_width ()
+ {
+ return width_ - (border_width + ruler_width) * 2;
+ }
+
+ public int get_preview_height ()
+ {
+ return height_ - (border_width + ruler_width) * 2;
+ }
+
+ /** Pixel offset (within the PageView's bounding box) where the page image starts. */
+ public int get_image_origin_x ()
+ {
+ return border_width + ruler_width;
+ }
+
+ public int get_image_origin_y ()
+ {
+ return border_width + ruler_width;
+ }
+
+ public int page_to_screen_x (int x)
+ {
+ return (int) ((double)x * get_preview_width () / page.width + 0.5);
+ }
+
+ public int page_to_screen_y (int y)
+ {
+ return (int) ((double)y * get_preview_height () / page.height + 0.5);
+ }
+
+ public int screen_to_page_x (int x)
+ {
+ return (int) ((double)x * page.width / get_preview_width () + 0.5);
+ }
+
+ public int screen_to_page_y (int y)
+ {
+ return (int) ((double)y * page.height / get_preview_height () + 0.5);
+ }
+
+ private CropLocation get_crop_location (int x, int y)
+ {
+ if (!page.has_crop)
+ return CropLocation.NONE;
+
+ var cx = page.crop_x;
+ var cy = page.crop_y;
+ var cw = page.crop_width;
+ var ch = page.crop_height;
+ var dx = page_to_screen_x (cx) + border_width + ruler_width;
+ var dy = page_to_screen_y (cy) + border_width + ruler_width;
+ var dw = page_to_screen_x (cw) + border_width + ruler_width;
+ var dh = page_to_screen_y (ch) + border_width + ruler_width;
+ var ix = x - dx;
+ var iy = y - dy;
+
+ if (ix < 0 || ix > dw || iy < 0 || iy > dh)
+ return CropLocation.NONE;
+
+ /* Can't resize named crops */
+ var name = page.crop_name;
+ /* custom is the autosave name for null */
+ if (name != null && name != "custom")
+ return CropLocation.MIDDLE;
+
+ /* Adjust borders so can select */
+ int crop_border = 20;
+ 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 CropLocation.TOP_LEFT;
+ /* Top right */
+ if (ix > dw - crop_border && iy < crop_border)
+ return CropLocation.TOP_RIGHT;
+ /* Bottom left */
+ if (ix < crop_border && iy > dh - crop_border)
+ return CropLocation.BOTTOM_LEFT;
+ /* Bottom right */
+ if (ix > dw - crop_border && iy > dh - crop_border)
+ return CropLocation.BOTTOM_RIGHT;
+
+ /* Left */
+ if (ix < crop_border)
+ return CropLocation.LEFT;
+ /* Right */
+ if (ix > dw - crop_border)
+ return CropLocation.RIGHT;
+ /* Top */
+ if (iy < crop_border)
+ return CropLocation.TOP;
+ /* Bottom */
+ if (iy > dh - crop_border)
+ return CropLocation.BOTTOM;
+
+ /* In the middle */
+ return CropLocation.MIDDLE;
+ }
+
+ public bool button_press (int x, int y)
+ {
+ /* See if selecting crop */
+ var location = get_crop_location (x, y);
+ if (location != CropLocation.NONE)
+ {
+ crop_location = location;
+ selected_crop_px = x;
+ selected_crop_py = y;
+ selected_crop_x = page.crop_x;
+ selected_crop_y = page.crop_y;
+ selected_crop_w = page.crop_width;
+ selected_crop_h = page.crop_height;
+ return true;
+ }
+
+ return false;
+ }
+
+ public void motion (int x, int y)
+ {
+ var location = get_crop_location (x, y);
+
+ string cursor;
+ switch (location)
+ {
+ case CropLocation.MIDDLE:
+ cursor = "all-resize";
+ break;
+ case CropLocation.TOP:
+ cursor = "n-resize";
+ break;
+ case CropLocation.BOTTOM:
+ cursor = "s-resize";
+ break;
+ case CropLocation.LEFT:
+ cursor = "w-resize";
+ break;
+ case CropLocation.RIGHT:
+ cursor = "e-resize";
+ break;
+ case CropLocation.TOP_LEFT:
+ cursor = "nw-resize";
+ break;
+ case CropLocation.TOP_RIGHT:
+ cursor = "ne-resize";
+ break;
+ case CropLocation.BOTTOM_LEFT:
+ cursor = "sw-resize";
+ break;
+ case CropLocation.BOTTOM_RIGHT:
+ cursor = "se-resize";
+ break;
+ default:
+ cursor = "default";
+ break;
+ }
+
+ if (crop_location == CropLocation.NONE)
+ {
+ this.cursor = cursor;
+ return;
+ }
+
+ /* Move the crop */
+ var pw = page.width;
+ var ph = page.height;
+ var cw = page.crop_width;
+ var ch = page.crop_height;
+
+ var dx = screen_to_page_x (x - (int) selected_crop_px);
+ var dy = screen_to_page_y (y - (int) selected_crop_py);
+
+ var new_x = selected_crop_x;
+ var new_y = selected_crop_y;
+ var new_w = selected_crop_w;
+ var new_h = selected_crop_h;
+
+ /* Limit motion to remain within page and minimum crop size */
+ var min_size = screen_to_page_x (15);
+ if (crop_location == CropLocation.TOP_LEFT ||
+ crop_location == CropLocation.LEFT ||
+ crop_location == CropLocation.BOTTOM_LEFT)
+ {
+ if (dx > new_w - min_size)
+ dx = new_w - min_size;
+ if (new_x + dx < 0)
+ dx = -new_x;
+ }
+ if (crop_location == CropLocation.TOP_LEFT ||
+ crop_location == CropLocation.TOP ||
+ crop_location == CropLocation.TOP_RIGHT)
+ {
+ if (dy > new_h - min_size)
+ dy = new_h - min_size;
+ if (new_y + dy < 0)
+ dy = -new_y;
+ }
+
+ if (crop_location == CropLocation.TOP_RIGHT ||
+ crop_location == CropLocation.RIGHT ||
+ crop_location == CropLocation.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 (crop_location == CropLocation.BOTTOM_LEFT ||
+ crop_location == CropLocation.BOTTOM ||
+ crop_location == CropLocation.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 (crop_location == CropLocation.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 (crop_location == CropLocation.MIDDLE)
+ {
+ new_x += dx;
+ new_y += dy;
+ }
+ if (crop_location == CropLocation.TOP_LEFT ||
+ crop_location == CropLocation.LEFT ||
+ crop_location == CropLocation.BOTTOM_LEFT)
+ {
+ new_x += dx;
+ new_w -= dx;
+ }
+ if (crop_location == CropLocation.TOP_LEFT ||
+ crop_location == CropLocation.TOP ||
+ crop_location == CropLocation.TOP_RIGHT)
+ {
+ new_y += dy;
+ new_h -= dy;
+ }
+
+ if (crop_location == CropLocation.TOP_RIGHT ||
+ crop_location == CropLocation.RIGHT ||
+ crop_location == CropLocation.BOTTOM_RIGHT)
+ new_w += dx;
+ if (crop_location == CropLocation.BOTTOM_LEFT ||
+ crop_location == CropLocation.BOTTOM ||
+ crop_location == CropLocation.BOTTOM_RIGHT)
+ new_h += dy;
+
+ page.move_crop (new_x, new_y);
+
+ /* If reshaped crop, must be a custom crop */
+ if (new_w != cw || new_h != ch)
+ page.set_custom_crop (new_w, new_h);
+ }
+
+ public void button_release (int x, int y)
+ {
+ /* Complete crop */
+ crop_location = CropLocation.NONE;
+ changed ();
+ }
+
+ private bool animation_cb ()
+ {
+ animate_segment = (animate_segment + 1) % animate_n_segments;
+ changed ();
+ return true;
+ }
+
+ private void update_animation ()
+ {
+ bool animate, is_animating;
+
+ animate = page.is_scanning && !page.has_data;
+ is_animating = animate_timeout != 0;
+ if (animate == is_animating)
+ return;
+
+ if (animate)
+ {
+ animate_segment = 0;
+ if (animate_timeout == 0)
+ animate_timeout = Timeout.add (150, animation_cb);
+ }
+ else
+ {
+ if (animate_timeout != 0)
+ Source.remove (animate_timeout);
+ animate_timeout = 0;
+ }
+ }
+
+ /* helper functions for drawing lines */
+ private static void append_vline (Gtk.Snapshot snapshot, Gdk.RGBA color, float x, float y0, float y1, float width=1.0f)
+ {
+ if (y1 == y0)
+ return;
+ if (y1 < y0)
+ {
+ var t = y0; y0 = y1; y1 = t;
+ }
+ var rect = Graphene.Rect ().init (x, y0, width, y1 - y0);
+ snapshot.append_color (color, rect);
+ }
+
+ private static void append_hline (Gtk.Snapshot snapshot, Gdk.RGBA color, float x0, float x1, float y, float width=1.0f)
+ {
+ if (x1 == x0)
+ return;
+ if (x1 < x0)
+ {
+ var t = x0; x0 = x1; x1 = t;
+ }
+ var rect = Graphene.Rect ().init (x0, y, x1 - x0, width);
+ snapshot.append_color (color, rect);
+ }
+
+ /* draw a border around a rectangle */
+ private static void append_rect_outline (Gtk.Snapshot snapshot, Gdk.RGBA color,
+ float x, float y, float w, float h, float thickness = 1.0f)
+ {
+ var top = Graphene.Rect ().init (x, y, w, thickness);
+ var bottom = Graphene.Rect ().init (x, y + h - thickness, w, thickness);
+ var left = Graphene.Rect ().init (x, y, thickness, h);
+ var right = Graphene.Rect ().init (x + w - thickness, y, thickness, h);
+ snapshot.append_color (color, top);
+ snapshot.append_color (color, bottom);
+ snapshot.append_color (color, left);
+ snapshot.append_color (color, right);
+ }
+
+ private static void append_filled_rect (Gtk.Snapshot snapshot, Gdk.RGBA color,
+ float x, float y, float w, float h)
+ {
+ var rect = Graphene.Rect ().init (x, y, w, h);
+ snapshot.append_color (color, rect);
+ }
+
+ /* add a texture and rotate it according to the scan direction (the texture
+ is always TOP_BOTTOM) */
+ private static void append_rotated_texture (Gtk.Snapshot snapshot, Gdk.Texture? texture,
+ ScanDirection scan_direction, int w, int h)
+ {
+ /* scan direction: top, left, bottom, right */
+ float angles[] = { 0.0f, 270.0f, 180.0f, 90.0f };
+ float deg = angles[scan_direction];
+
+ snapshot.save ();
+
+ /* translate according to rotation (rotated around top left corner)
+ so that the new top left corner after the rotation is at the
+ position of the top left corner before the rotation */
+ if (deg != 0.0f) {
+ float tx = (deg <= 180.0f) ? (float)w : 0.0f;
+ float ty = (deg >= 180.0f) ? (float)h : 0.0f;
+
+ snapshot.translate (Graphene.Point () { x = tx, y = ty });
+ snapshot.rotate (deg);
+ }
+
+ var rect = Graphene.Rect ().init (0.0f, 0.0f, (float)((deg % 180 != 0) ? h : w), (float)((deg % 180 != 0) ? w : h));
+ if (texture != null)
+ {
+ snapshot.append_texture (texture, rect);
+ }
+ else
+ {
+ Gdk.RGBA white = { 1.0f, 1.0f, 1.0f, 1.0f };
+ snapshot.append_color (white, rect);
+ }
+ snapshot.restore ();
+ }
+
+ /* draw an icon on the page - used for pages marked as deleted */
+ private static void append_icon (Gtk.Snapshot snapshot, string icon_name,
+ float x, float y, float size, Gdk.RGBA fg)
+ {
+ var display = Gdk.Display.get_default ();
+ if (display == null)
+ return;
+ var theme = Gtk.IconTheme.get_for_display (display);
+ var paintable = theme.lookup_icon (icon_name, null, (int) size, 1,
+ Gtk.TextDirection.NONE,
+ Gtk.IconLookupFlags.FORCE_SYMBOLIC);
+
+ snapshot.save ();
+ snapshot.translate (Graphene.Point () {
+ x = x, y = y
+ });
+
+ if (paintable is Gtk.SymbolicPaintable)
+ {
+ Gdk.RGBA[] colors = { fg, fg, fg, fg };
+ ((Gtk.SymbolicPaintable) paintable).snapshot_symbolic (snapshot, size, size, colors);
+ }
+ else
+ {
+ paintable.snapshot (snapshot, size, size);
+ }
+
+ snapshot.restore ();
+ }
+
+ /* code for rendering the page */
+ public void render (Gtk.Snapshot snapshot, bool is_selected_page=false, bool show_grid=true)
+ {
+ /* Set up colors - use the accent color from the theme */
+ var ruler_color = Adw.StyleManager.get_default ().accent_color.to_rgba ();
+ Gdk.RGBA white = { 1.0f, 1.0f, 1.0f, 1.0f };
+ Gdk.RGBA black = { 0.0f, 0.0f, 0.0f, 1.0f };
+ Gdk.RGBA shade = { 0.0f, 0.0f, 0.0f, 0.2f };
+
+ update_animation ();
+
+ var w = get_preview_width ();
+ var h = get_preview_height ();
+
+ snapshot.save ();
+ snapshot.translate (Graphene.Point () {
+ x = (float) x_offset, y = (float) y_offset
+ });
+
+ /* Move into the inner image area (after border and rulers). All subsequent
+ coordinates are relative to the top-left of the page image. */
+ snapshot.translate (Graphene.Point () {
+ x = (float) (border_width + ruler_width),
+ y = (float) (border_width + ruler_width)
+ });
+
+ /* Draw image (or white placeholder while no texture is ready).
+ The texture is stored in the raw scan (TOP_TO_BOTTOM) orientation;
+ apply scan_direction rotation here so PageViewTexture does not need
+ to rebuild on rotation. */
+ append_rotated_texture (snapshot, page_texture.texture, page.scan_direction, w, h);
+
+ /* ruler */
+ if (is_selected_page && page.has_crop)
+ {
+ /* Draw accent color page border for ruler */
+ var border_width_thin = (float) border_width / 2.0f;
+ append_rect_outline (snapshot, ruler_color,
+ -border_width_thin, -border_width_thin,
+ (float) w + 2 * border_width_thin,
+ (float) h + 2 * border_width_thin,
+ border_width_thin);
+
+ /* inch (top) */
+ var ruler_tick = 0.0;
+ var ruler_tick_count = 0;
+ while (ruler_tick <= page.width)
+ {
+ float line = (float) page_to_screen_x ((int) ruler_tick);
+ if (ruler_tick_count % 50 == 0)
+ append_vline (snapshot, ruler_color, line, -ruler_width, 0, 2.0f);
+ else if (ruler_tick_count % 5 == 0)
+ append_vline (snapshot, ruler_color, line, -ruler_width, 0);
+ else
+ append_vline (snapshot, ruler_color, line, -5, -2);
+ ruler_tick += page.dpi / 5;
+ ruler_tick_count += 1;
+ }
+
+ /* cm (bottom) */
+ ruler_tick_count = 0;
+ ruler_tick = 0.0;
+ while (ruler_tick <= page.width)
+ {
+ float line = (float) page_to_screen_x ((int) ruler_tick);
+
+ /* bold ruler each 10 cm */
+ if (ruler_tick_count % 20 == 0)
+ {
+ append_vline (snapshot, ruler_color, line, h, h + ruler_width, 2.0f);
+ }
+ else if (ruler_tick_count % 2 == 0)
+ {
+ append_vline (snapshot, ruler_color, line, h, h + ruler_width);
+ }
+ else
+ {
+ append_vline (snapshot, ruler_color, line, h + 2, h + 5);
+ }
+ ruler_tick += page.dpi / 2.54 / 2;
+ ruler_tick_count++;
+ }
+
+ /* Draw vertical ruler */
+ ruler_tick = 0.0;
+ ruler_tick_count = 0;
+ /* inch (left) */
+ while (ruler_tick <= page.height)
+ {
+ float line = (float) page_to_screen_y ((int) ruler_tick);
+ if (ruler_tick_count % 50 == 0)
+ append_hline (snapshot, ruler_color, -ruler_width, 0, line, 2.0f);
+ if (ruler_tick_count % 5 == 0)
+ append_hline (snapshot, ruler_color, -ruler_width, 0, line);
+ else
+ append_hline (snapshot, ruler_color, -5, -2, line);
+ ruler_tick += page.dpi / 5;
+ ruler_tick_count++;
+ }
+
+ ruler_tick = 0.0;
+ ruler_tick_count = 0;
+ /* cm (right) */
+ while (ruler_tick <= page.height)
+ {
+ float line = (float) page_to_screen_y ((int) ruler_tick);
+
+ /* bold ruler each 10 cm */
+ if (ruler_tick_count % 20 == 0)
+ {
+ append_hline (snapshot, ruler_color, w, w + ruler_width, line, 2.0f);
+ }
+ else if (ruler_tick_count % 2 == 0)
+ {
+ append_hline (snapshot, ruler_color, w, w + ruler_width, line);
+ }
+ else
+ {
+ append_hline (snapshot, ruler_color, w + 2, w + 5, line);
+ }
+ ruler_tick += page.dpi / 2.54 / 2;
+ ruler_tick_count++;
+ }
+
+ /* add inch and cm labels */
+ append_label (snapshot, "″", -8, -12, ruler_color);
+ append_label (snapshot, "cm", w - 12, h + 4, ruler_color);
+ }
+
+ /* Draw scan line */
+ if (page.is_scanning && page.scan_line > 0)
+ {
+ var scan_line = page.scan_line;
+ var accent = Adw.StyleManager.get_default ().accent_color.to_rgba ();
+
+ switch (page.scan_direction)
+ {
+ case ScanDirection.TOP_TO_BOTTOM:
+ append_hline (snapshot, accent, 0, w, page_to_screen_y (scan_line));
+ break;
+ case ScanDirection.BOTTOM_TO_TOP:
+ append_hline (snapshot, accent, 0, w, h - page_to_screen_y (scan_line));
+ break;
+ case ScanDirection.LEFT_TO_RIGHT:
+ append_vline (snapshot, accent, page_to_screen_x (scan_line), 0, h);
+ break;
+ case ScanDirection.RIGHT_TO_LEFT:
+ append_vline (snapshot, accent, w - page_to_screen_x (scan_line), 0, h);
+ break;
+ }
+ }
+
+ /* Draw crop */
+ if (page.has_crop)
+ {
+ var x = page.crop_x;
+ var y = page.crop_y;
+ var crop_width = page.crop_width;
+ var crop_height = page.crop_height;
+
+ var dx = page_to_screen_x (x);
+ var dy = page_to_screen_y (y);
+ var dw = page_to_screen_x (crop_width);
+ var dh = page_to_screen_y (crop_height);
+
+ /* Shade out cropped area: four rectangles around the crop region */
+ /* top */
+ if (dy > 0)
+ append_filled_rect (snapshot, shade, 0, 0, w, dy);
+ /* bottom */
+ if (dy + dh < h)
+ append_filled_rect (snapshot, shade, 0, dy + dh, w, h - (dy + dh));
+ /* left */
+ if (dx > 0)
+ append_filled_rect (snapshot, shade, 0, dy, dx, dh);
+ /* right */
+ if (dx + dw < w)
+ append_filled_rect (snapshot, shade, dx + dw, dy, w - (dx + dw), dh);
+
+ /* only draw helper lines (edge to ruler) if selected */
+ if (is_selected_page) {
+
+ /* horizontal helper lines (above/below crop, extending to rulers) */
+ append_hline (snapshot, white, -border_width, border_width + w, dy - 2);
+ append_hline (snapshot, white, -border_width, border_width + w, dy + dh + 1);
+
+ /* vertical helper lines (left/right of crop, extending to rulers) */
+ append_vline (snapshot, white, dx - 2, -border_width, border_width + h);
+ append_vline (snapshot, white, dx + dw + 1, -border_width, border_width + h);
+
+ /* draw cross to indicate crop center */
+ int cross_size = 5;
+ append_hline (snapshot, ruler_color,
+ dx + dw / 2 - cross_size, dx + dw / 2 + cross_size,
+ dy + dh / 2);
+ append_vline (snapshot, ruler_color,
+ dx + dw / 2,
+ dy + dh / 2 - cross_size, dy + dh / 2 + cross_size);
+ }
+
+ /* crop rectangle outline */
+ append_rect_outline (snapshot, black, dx - 1, dy - 1, dw + 2, dh + 2, 1.0f);
+ /* white border to make crop visible on dark pages */
+ append_rect_outline (snapshot, white, dx - 2, dy - 2, dw + 4, dh + 4, 1.0f);
+ }
+
+ /* Draw deleted page overlay */
+ if (page.is_deleted)
+ {
+ /* Draw red semi-transparent rectangle (similar to GNOME red 5 on white) */
+ Gdk.RGBA red_overlay = { 0.929f, 0.0f, 0.071f, 0.5f };
+ append_filled_rect (snapshot, red_overlay, 0, 0, (float) w, (float) h);
+
+ /* Draw trash icon in white in the center */
+ float icon_size = 24.0f;
+ float center_x = w / 2.0f;
+ float center_y = h / 2.0f;
+ append_icon (snapshot, "user-trash-symbolic",
+ center_x - icon_size / 2.0f,
+ center_y - icon_size / 2.0f,
+ icon_size, white);
+ }
+
+ snapshot.restore ();
+ }
+
+ /* text labels (page number) */
+ private void append_label (Gtk.Snapshot snapshot, string text, double x, double y, Gdk.RGBA color)
+ {
+ var font_map = Pango.CairoFontMap.get_default() as Pango.FontMap;
+ if (font_map == null)
+ return;
+ var pango_ctx = font_map.create_context ();
+
+ /* Ensure a sensible default font is set; without it the layout would render
+ with a zero-size font. */
+ var font_desc = new Pango.FontDescription ();
+ font_desc.set_family ("Sans");
+ font_desc.set_size (10 * Pango.SCALE);
+ pango_ctx.set_font_description (font_desc);
+
+ var layout = new Pango.Layout (pango_ctx);
+ layout.set_text (text, -1);
+
+ snapshot.save ();
+ snapshot.translate (Graphene.Point () {
+ x = (float) x, y = (float) y
+ });
+ snapshot.append_layout (layout, color);
+ snapshot.restore ();
+ }
+
+ /* a small version of the page for dragging pages with the cursor */
+ public void render_preview (Gtk.Snapshot snapshot, int x, int y, int width, int height)
+ {
+ /* Set up colors - use the accent color from the theme */
+ var ruler_color = Adw.StyleManager.get_default ().accent_color.to_rgba ();
+ Gdk.RGBA white = { 1.0f, 1.0f, 1.0f, 1.0f };
+
+ snapshot.save ();
+ snapshot.translate (Graphene.Point () {
+ x = (float) x, y = (float) y
+ });
+
+ var current_texture = page_texture.texture;
+ if (current_texture != null)
+ {
+ append_rotated_texture (snapshot, current_texture,
+ page.scan_direction, width, height);
+ }
+ else
+ {
+ var rect = Graphene.Rect ();
+ rect.init (0.0f, 0.0f, (float) width, (float) height);
+ snapshot.append_color (white, rect);
+ }
+
+ /* 1-px ruler-color outline around the preview */
+ append_rect_outline (snapshot, ruler_color, 0, 0, (float) width, (float) height, 1.0f);
+
+ snapshot.restore ();
+ }
+
+ public int width
+ {
+ get { return width_; }
+ set
+ {
+ // FIXME: Automatically update when get updated image
+ var h = (int) ((double) value * page.height / page.width);
+ if (width_ == value && height_ == h)
+ return;
+
+ width_ = value;
+ height_ = h;
+
+ size_changed ();
+ changed ();
+ }
+ }
+
+ public int height
+ {
+ get { return height_; }
+ set
+ {
+ // FIXME: Automatically update when get updated image
+ var w = (int) ((double) value * page.width / page.height);
+ if (width_ == w && height_ == value)
+ return;
+
+ width_ = w;
+ height_ = value;
+
+ size_changed ();
+ changed ();
+ }
+ }
+
+ public void page_pixels_changed_cb (Page p)
+ {
+ /* Texture is rebuilt by PageViewTexture; we just need a redraw. */
+ changed ();
+ }
+
+ private void page_size_changed_cb (Page p)
+ {
+ size_changed ();
+ changed ();
+ }
+
+ private void page_overlay_changed_cb (Page p)
+ {
+ changed ();
+ }
+
+ private void page_scan_line_changed_cb (Page p)
+ {
+ double progress = 0.0;
+ if (page.scan_height > 0)
+ progress = (double) page.scan_line / (double) page.scan_height;
+
+ scan_progress_changed (progress);
+ changed ();
+ }
+
+ private void scan_direction_changed_cb (Page p)
+ {
+ /* No texture rebuild needed: rotation is applied at snapshot time. */
+ size_changed ();
+ changed ();
+ }
+}
diff --git a/src/page.vala b/src/page.vala
index 67c8010bdceca68304deab13450a7012a249ffe3..4f0a5765ee4f5c5371ce546a0ac446325a352bb6 100644
--- a/src/page.vala
+++ b/src/page.vala
@@ -1,702 +1,2446 @@
-/*
- * 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 Page copy()
- {
- var copy = new Page.from_data (
- scan_width,
- scan_height,
- rowstride,
- n_channels,
- depth,
- dpi,
- scan_direction,
- color_profile,
- pixels,
- has_crop,
- crop_name,
- crop_x,
- crop_y,
- crop_width,
- crop_height
- );
-
- copy.scan_line = scan_line;
-
- return copy;
- }
-
- 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 clipboard = window.get_clipboard();
- var image = get_image (true);
- clipboard.set_value (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);
- }
-}
+/*
+ * 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
+{
+ /* store original pixels in autosave path */
+ private static string AUTOSAVE_DIR = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "autosaves");
+
+ /* 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; }
+
+ /* Mutex to ensure only one operation is accessing pixels at a time */
+ private GLib.Mutex pixels_mutex;
+
+ /* True if the page is selected for multi-page operations */
+ public bool is_selected { get; set; default = false; }
+
+ /* True if in page view */
+ public bool is_selected_page_view { get; set; default = false; }
+
+ /* Number of rows in this page or -1 if currently unknown */
+ private int expected_rows;
+
+ /* Page size selected by the user*/
+ public string page_size { get; private set; }
+
+ /* 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; }
+
+ /* Rowstride of the current pixels */
+ private int pixels_rowstride;
+
+ /* Pixel data */
+ private uchar[] pixels = {};
+
+ /* Original pixel data before auto-rotate */
+ public string raw_backup_path { get; private set; }
+ public string fullres_pixels_path { get; private set; }
+
+ public bool pixels_are_being_edited = false;
+ public int pixels_width { get; private set; }
+ public int pixels_height { get; private set; }
+
+ /* Original metadata before filters (depth reduction) */
+ public int original_depth { get; private set; }
+ public string? original_color_profile { get; private set; }
+ public int original_rowstride { get; private set; }
+ public int original_n_channels { get; private set; }
+ public bool is_filtered { get; private set; }
+
+ /* To force re-rendering after changing pixels */
+ private bool theres_a_change;
+
+ /* true if page has been auto-rotated */
+ public bool has_auto_rotate { get; private set; }
+
+ /* Page is getting data */
+ public bool is_scanning { get; private set; }
+
+ /* true if have some page data */
+ public bool has_data { get; private set; }
+
+ /* true if page was detected as empty by empty page detection */
+ public bool has_no_text { get; set; }
+
+ /* true if page is marked for deletion (soft delete) */
+ public bool is_deleted { 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 filter_changed ();
+ public signal void crop_changed ();
+ public signal void scan_finished ();
+ public signal void is_deleted_changed ();
+
+ private void notify_pixels_and_filter_changed ()
+ {
+ Idle.add (() =>
+ {
+ pixels_changed ();
+ filter_changed ();
+ return Source.REMOVE;
+ });
+ }
+
+ private void notify_pixels_changed ()
+ {
+ Idle.add (() =>
+ {
+ pixels_changed ();
+ return Source.REMOVE;
+ });
+ }
+
+ public Page (int width, int height, int dpi, ScanDirection scan_direction)
+ {
+ scan_width = width;
+ scan_height = height;
+
+ this.dpi = dpi;
+ this.scan_direction = scan_direction;
+ this.theres_a_change = false;
+ this.raw_backup_path = "";
+ this.fullres_pixels_path = "";
+ this.pixels_width = 0;
+ this.pixels_height = 0;
+ this.pixels_rowstride = 0;
+ this.has_auto_rotate = false;
+ this.has_crop = false;
+ this.has_no_text = false;
+ this.page_size = "Automatic";
+ this.has_data = false;
+ this.is_deleted = false;
+ this.pixels_are_being_edited = false;
+ }
+
+ public Page.from_data (int scan_width,
+ int scan_height,
+ string page_size,
+ int rowstride,
+ int n_channels,
+ int depth,
+ int dpi,
+ ScanDirection scan_direction,
+ string? color_profile,
+ uchar[]? pixels,
+ string raw_backup_path,
+ string fullres_pixels_path,
+ int pixels_width = 0,
+ int pixels_height = 0,
+ int pixels_rowstride = 0,
+ bool has_auto_rotate,
+ bool has_crop,
+ string? crop_name,
+ int crop_x,
+ int crop_y,
+ int crop_width,
+ int crop_height,
+ bool is_deleted,
+ int original_depth = 0,
+ string? original_color_profile = null,
+ int original_rowstride = 0,
+ int original_n_channels = 0,
+ bool is_filtered = false
+ )
+ {
+ this.scan_width = scan_width;
+ this.scan_height = scan_height;
+ this.page_size = page_size;
+ 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 ?? new uchar[0];
+ this.raw_backup_path = raw_backup_path;
+ this.fullres_pixels_path = fullres_pixels_path;
+ this.pixels_width = pixels_width;
+ this.pixels_height = pixels_height;
+ this.pixels_rowstride = pixels_rowstride;
+ this.original_depth = original_depth;
+ this.original_color_profile = original_color_profile;
+ this.original_rowstride = original_rowstride;
+ this.original_n_channels = original_n_channels;
+ this.is_filtered = is_filtered;
+ this.theres_a_change = false;
+ this.has_auto_rotate = has_auto_rotate;
+ 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;
+ this.is_deleted = is_deleted;
+ this.pixels_are_being_edited = false;
+ }
+
+ public Page copy (bool new_page = true)
+ {
+ /* this is for creating a new page and not just updating the texture */
+
+ var was_scaled_down = is_scaled_down ();
+
+ if (new_page)
+ {
+ /* restore fullsize pixels before making the copy */
+ update_pixels (true, false);
+ }
+
+ var copy = new Page.from_data (
+ scan_width,
+ scan_height,
+ page_size,
+ rowstride,
+ n_channels,
+ depth,
+ dpi,
+ scan_direction,
+ color_profile,
+ pixels,
+ raw_backup_path,
+ fullres_pixels_path,
+ pixels_width,
+ pixels_height,
+ pixels_rowstride,
+ has_auto_rotate,
+ has_crop,
+ crop_name,
+ crop_x,
+ crop_y,
+ crop_width,
+ crop_height,
+ is_deleted,
+ original_depth,
+ original_color_profile,
+ original_rowstride,
+ original_n_channels,
+ is_filtered
+ );
+
+ copy.scan_line = scan_line;
+ copy.has_no_text = has_no_text;
+ copy.theres_a_change = theres_a_change;
+ copy.is_deleted = false; // Don't copy deleted state, as this is a new page
+ copy.is_selected = false;
+
+ copy.pixels_are_being_edited = false;
+
+ if (new_page)
+ {
+ /* create a new file path for fullres pixels */
+ copy.fullres_pixels_path = "";
+ copy.save_fullres_pixels ();
+ copy.update_pixels (false, true);
+
+ if (was_scaled_down)
+ update_pixels (false, false);
+ }
+
+ return copy;
+ }
+
+ public void set_page_info (ScanPageInfo info)
+ {
+ expected_rows = info.height;
+ page_size = info.page_size;
+ /* scale crop when scanning a new page with different dpi! */
+ double crop_scaling = (double)info.dpi / (double)dpi;
+
+ pixels_mutex.lock ();
+ dpi = (int) info.dpi;
+
+ if (has_crop)
+ {
+ if (
+ ((int)((crop_width + crop_x) * crop_scaling + 0.5) > info.width)
+ ||
+ (
+ ((int)((crop_height + crop_y) * crop_scaling + 0.5) > info.height)
+ && info.height > 0
+ )
+ )
+ {
+ set_no_crop ();
+ }
+ else
+ {
+ set_custom_crop ((int)(crop_width * crop_scaling), (int)(crop_height * crop_scaling + 0.5));
+ move_crop ((int)(crop_x * crop_scaling + 0.5), (int)(crop_y * crop_scaling + 0.5));
+ }
+ }
+
+ /* Create a white page */
+ scan_width = info.width;
+ scan_height = info.height;
+ /* Variable height, try sqrt(2) ratio for now (as for DIN A sizes).
+ Do not use the height from the driver, because
+ for some scanners this is just very high */
+ if (scan_height < 0 || page_size == "Automatic")
+ scan_height = (int)(scan_width * 1.41421);
+ 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);
+ pixels_mutex.unlock ();
+
+ 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 but only for automatic page size, otherwise just drop extra lines */
+ size_changed = false;
+ if (page_size == "Automatic") {
+ 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 */
+
+ /* Don't add lines if there is no space left */
+ if (line_number >= scan_height) {
+ warning ("Dropping line %d because it is past the end of the page", line_number);
+ return;
+ }
+ else
+ {
+ 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 (page_size == "Automatic" &&
+ scan_line + 1 != scan_height)
+ {
+ var rows = scan_height;
+ scan_height = scan_line + 1;
+ pixels.resize (scan_height * rowstride);
+ debug ("Trimming page from %d lines to %d lines", rows, scan_height);
+
+ size_has_changed = true;
+ }
+
+ /* Automatic page size is failing for some drivers producing extremely long pages.
+ Therefore just remove identical lines at top and bottom.
+ Since we always scan in color now, it is unlikely that this may trim white pages
+ at top and bottom, because two scan lines are never "perfect" (one color value).
+ Still, this is acceptable for "Automatic" I think. */
+ if (page_size == "Automatic") {
+
+ var last_line_offset = (scan_height - 1) * rowstride;
+ /* remove identical lines at top */
+ while (scan_height > 1) {
+ var line_to_compare_offset = (scan_height - 2) * rowstride;
+ if (Memory.cmp (&pixels[last_line_offset], &pixels[line_to_compare_offset], rowstride) == 0)
+ scan_height -= 1;
+ else
+ break;
+ }
+ pixels.resize (scan_height * rowstride);
+
+ /* remove identical lines at bottom */
+ var n_identical_lines = 0;
+ for (int i=1; i 0)
+ {
+ try
+ {
+ raw_backup_path = Path.build_filename(AUTOSAVE_DIR, "simple-scan-raw-backup-XXXXXX");
+ // mkstemp modifies tmpl in-place to contain the final filename
+ int fd = FileUtils.mkstemp(raw_backup_path);
+ if (fd == -1) {
+ error("Failed to create original pixels file");
+ }
+ FileUtils.close(fd);
+ FileUtils.set_data (raw_backup_path, pixels);
+
+ original_depth = depth;
+ original_color_profile = color_profile;
+ original_rowstride = rowstride;
+ original_n_channels = n_channels;
+ }
+ catch (Error e)
+ {
+ warning ("Failed to set raw backup: %s", e.message);
+ }
+
+ }
+ }
+
+ /* erase the pixels in the given rectangle (set them to black) */
+ public void erase_rect (int rx, int ry, int rw, int rh, bool store_and_new_texture = false)
+ {
+ if (!has_data || rw <= 0 || rh <= 0)
+ return;
+
+ /* Make sure original pixel data is preserved for "Restore Image". */
+ keep_original_data ();
+
+ /* Rotate (rx, ry, rw, rh) from page (rotated) coords to raw scan coords. */
+ int sx, sy, sw, sh;
+ rotated_rect_to_scan (rx, ry, rw, rh, out sx, out sy, out sw, out sh);
+
+ /* Clip to scan bounds */
+ int x0 = int.max (0, sx);
+ int y0 = int.max (0, sy);
+ int x1 = int.min (scan_width, sx + sw);
+ int y1 = int.min (scan_height, sy + sh);
+ if (x0 >= x1 || y0 >= y1)
+ return;
+
+ /* wait until pixels are accessible */
+ pixels_mutex.lock ();
+
+ if (depth == 8 && n_channels == 3)
+ {
+ /* Fast path: 8-bit RGB. Set R, G, B to 0. */
+ for (int y = y0; y < y1; y++)
+ {
+ int row = y * rowstride;
+ for (int x = x0; x < x1; x++)
+ {
+ int o = row + x * 3;
+ pixels[o + 0] = 0;
+ pixels[o + 1] = 0;
+ pixels[o + 2] = 0;
+ }
+ }
+ }
+ else if (depth == 8 && n_channels == 1)
+ {
+ /* Fast path: 8-bit grayscale. */
+ for (int y = y0; y < y1; y++)
+ {
+ int row = y * rowstride;
+ for (int x = x0; x < x1; x++)
+ pixels[row + x] = 0;
+ }
+ }
+ else if (depth == 1 && n_channels == 1)
+ {
+ for (int y = y0; y < y1; y++)
+ {
+ int row = y * rowstride;
+ for (int x = x0; x < x1; x++)
+ {
+ int byte_idx = row + (x / 8);
+ /* set one bit as mask and shift it to the right position,
+ then with |= force it black (bit set is black) */
+ pixels[byte_idx] |= (uchar) (1 << (7 - (x % 8)));
+ }
+ }
+ }
+ else if (depth == 2 && n_channels == 1)
+ {
+ /* 2-bit: 4 pixels per byte, MSB first. Black = 0b00. */
+ int[] block_shift = { 6, 4, 2, 0 };
+ for (int y = y0; y < y1; y++)
+ {
+ int row = y * rowstride;
+ for (int x = x0; x < x1; x++)
+ {
+ int byte_idx = row + (x / 4);
+ int shift = block_shift[x % 4];
+ /* 3 is 0b11, so after inversion (~) 00 at that position */
+ pixels[byte_idx] &= (uchar) ~(3 << shift);
+ }
+ }
+ }
+ else
+ {
+ /* Generic slow path for any other depth/channel layout: clear all
+ channels of the affected pixels. Walks bit-by-bit. */
+ for (int y = y0; y < y1; y++)
+ {
+ int row = y * rowstride;
+ for (int x = x0; x < x1; x++)
+ {
+ for (int c = 0; c < n_channels; c++)
+ {
+ int bit_pos = x * n_channels * depth + c * depth;
+ for (int b = 0; b < depth; b++)
+ {
+ int abs_bit = bit_pos + b;
+ int byte_idx = row + (abs_bit / 8);
+ int bit_in_byte = 7 - (abs_bit % 8);
+ pixels[byte_idx] &= (uchar) ~(1 << bit_in_byte);
+ }
+ }
+ }
+ }
+ }
+
+ if (store_and_new_texture)
+ {
+ save_fullres_pixels ();
+ theres_a_change = true;
+ pixels_changed ();
+ }
+ pixels_mutex.unlock ();
+ }
+
+ /* Convert the coordinates of a rectangle to raw (TOP_BOTTOM) image dimensions */
+ private void rotated_rect_to_scan (int rx, int ry, int rw, int rh,
+ out int sx, out int sy, out int sw, out int sh)
+ {
+ switch (scan_direction)
+ {
+ case ScanDirection.TOP_TO_BOTTOM:
+ sx = rx; sy = ry; sw = rw; sh = rh;
+ break;
+ case ScanDirection.BOTTOM_TO_TOP:
+ sx = scan_width - (rx + rw);
+ sy = scan_height - (ry + rh);
+ sw = rw; sh = rh;
+ break;
+ case ScanDirection.LEFT_TO_RIGHT:
+ sx = scan_width - (ry + rh);
+ sy = rx;
+ sw = rh;
+ sh = rw;
+ break;
+ case ScanDirection.RIGHT_TO_LEFT:
+ sx = ry;
+ sy = scan_height - (rx + rw);
+ sw = rh;
+ sh = rw;
+ break;
+ default:
+ sx = rx; sy = ry; sw = rw; sh = rh;
+ 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 c_width, int c_height)
+ {
+ return_if_fail (c_width > 0);
+ return_if_fail (c_height > 0);
+
+ crop_name = "custom";
+ has_crop = true;
+ crop_width = c_width;
+ crop_height = c_height;
+
+ crop_changed ();
+ }
+
+ public void set_only_crop_name (string name)
+ {
+ crop_name = name;
+ }
+
+ public bool 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 false;
+ }
+
+ var pw = width;
+ var ph = height;
+
+ /* Rotate to match original aspect */
+ if (pw > ph)
+ {
+ var t = w;
+ w = h;
+ h = t;
+ }
+
+ var cw = (int) (w * dpi + 0.5);
+ var ch = (int) (h * dpi + 0.5);
+
+ /* check if larger than page. If yes, don't set */
+ if (cw > pw || ch > ph)
+ return false;
+
+ /* set the crop now */
+ crop_name = name;
+ has_crop = true;
+
+ crop_height = ch;
+ crop_width = cw;
+
+ /* center crop */
+ crop_x = (pw - crop_width) / 2;
+ crop_y = (ph - crop_height) / 2;
+
+ crop_changed ();
+ return true;
+ }
+
+ 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;
+
+ /* do not rotate if too large to rotate */
+ if (crop_width > height || crop_height > width)
+ return;
+
+ /* rotate */
+ var t = crop_width;
+ crop_width = crop_height;
+ crop_height = t;
+
+ /* rotate around the centre */
+ crop_x = crop_x + crop_height / 2 - crop_width / 2;
+ crop_y = crop_y + crop_width / 2 - crop_height / 2;
+
+ /* move crop so that it fits within the page */
+ if (crop_x + crop_width > width)
+ {
+ crop_x = width - crop_width;
+ }
+
+ if (crop_y + crop_height > height)
+ {
+ crop_y = height - crop_height;
+ }
+
+ if (crop_x < 0)
+ {
+ crop_x = 0;
+ }
+
+ if (crop_y < 0)
+ {
+ crop_y = 0;
+ }
+
+ crop_changed ();
+ }
+
+ public int get_pixels_rowstride ()
+ {
+ return pixels_rowstride;
+ }
+
+ public bool is_scaled_down ()
+ {
+ return (pixels_width > 0 && pixels_width != scan_width);
+ }
+
+ public unowned uchar[] get_pixels ()
+ {
+ return pixels;
+ }
+
+ public uchar[] get_fullres_pixels ()
+ {
+ uchar[] output;
+ if (fullres_pixels_path != "")
+ {
+ try
+ {
+ FileUtils.get_data (fullres_pixels_path, out output);
+ return output;
+ }
+ catch (Error e)
+ {
+ warning ("Failed to load full resolution pixels: %s", e.message);
+ }
+ }
+ warning ("Failed to load full resolution pixels (no path)");
+ output = {};
+ return output;
+ }
+
+ public bool save_fullres_pixels ()
+ {
+ if (is_scanning || pixels == null || has_data == false)
+ return false;
+
+ try
+ {
+ if (fullres_pixels_path == "")
+ {
+ DirUtils.create_with_parents (AUTOSAVE_DIR, 0700);
+ fullres_pixels_path = Path.build_filename (AUTOSAVE_DIR, "simple-scan-fullres-pixels-XXXXXX");
+ int fd = FileUtils.mkstemp (fullres_pixels_path);
+ if (fd == -1)
+ {
+ warning ("Failed to create full resolution pixels file");
+ fullres_pixels_path = "";
+ return false;
+ }
+ FileUtils.close (fd);
+ }
+ FileUtils.set_data (fullres_pixels_path, pixels);
+ return true;
+ }
+ catch (Error e)
+ {
+ warning ("Failed to save full resolution pixels: %s", e.message);
+ return false;
+ }
+ }
+
+ /* get the lock from outside */
+ public void acquire_pixel_lock ()
+ {
+ pixels_mutex.lock ();
+ }
+
+ public void release_pixel_lock ()
+ {
+ pixels_mutex.unlock ();
+ }
+
+ public void update_pixels (bool full_res = false, bool update_texture = true)
+ {
+ if (is_scanning)
+ {
+ return;
+ }
+
+ /* just use the raw pixels, while editing pixels, too */
+ if (full_res || pixels_are_being_edited)
+ {
+ /* already full resolution */
+ if (pixels_width == scan_width && pixels_height == scan_height)
+ {
+ return;
+ }
+
+ if (fullres_pixels_path != "")
+ {
+ /* wait until pixels are accessible */
+ pixels_mutex.lock ();
+ pixels = null;
+ pixels = get_fullres_pixels ();
+ has_data = true;
+ pixels_height = scan_height;
+ pixels_width = scan_width;
+ pixels_rowstride = rowstride;
+ pixels_mutex.unlock ();
+ }
+ else
+ {
+ return;
+ }
+
+ /* do not update if still editing pixels... */
+ if (!pixels_are_being_edited && update_texture)
+ {
+ theres_a_change = true;
+ pixels_changed ();
+ }
+ return;
+ }
+
+ if (!has_data)
+ {
+ return;
+ }
+
+ /* already scaled down... */
+ if (is_scaled_down ())
+ {
+ return;
+ }
+
+ /* reset scan_direction */
+ var old_scan_direction = scan_direction;
+ scan_direction = ScanDirection.TOP_TO_BOTTOM;
+
+ int target_width = scan_width;
+ int target_height = scan_height;
+
+ /* scale height to 512 px */
+ double scale = 1.0;
+ if (scan_width > scan_height && scan_width > 512)
+ scale = 512.0 / scan_width;
+ else if (scan_height >= scan_width && scan_height > 512)
+ scale = 512.0 / scan_height;
+
+ target_width = (int) (scan_width * scale + 0.5);
+ target_height = (int) (scan_height * scale + 0.5);
+
+ var pixbuf = get_image (false);
+
+ var scaled = pixbuf.scale_simple(
+ target_width,
+ target_height,
+ Gdk.InterpType.BILINEAR
+ );
+
+ unowned uint8[] scaled_pixels_arr = scaled.get_pixels();
+
+ int sw = scaled.get_width();
+ int sh = scaled.get_height();
+ int scaled_rowstride = scaled.get_rowstride ();
+
+ /* wait until pixels are accessible */
+ pixels_mutex.lock ();
+
+ pixels_width = sw;
+ pixels_height = sh;
+ pixels_rowstride = sw * 3;
+
+ pixels = null;
+ pixels = new uint8[sw * sh * 3];
+
+ for (int y = 0; y < sh; y++) {
+ for (int x = 0; x < sw; x++) {
+ int src_idx = y * scaled_rowstride + x * 3;
+ int dest_idx = (y * sw + x) * 3;
+ for (int c = 0; c < 3; c++) {
+ pixels[dest_idx + c] = scaled_pixels_arr[src_idx + c];
+ }
+ }
+ }
+
+ /* reset scan direction */
+ scan_direction = old_scan_direction;
+
+ pixels_mutex.unlock ();
+
+ /* do not update if still editing pixels... */
+ if (!pixels_are_being_edited && update_texture)
+ {
+ theres_a_change = true;
+ pixels_changed ();
+ }
+ pixbuf = null;
+ scaled = null;
+ scaled_pixels_arr = null;
+
+ }
+
+ public bool get_pixels_have_changed ()
+ {
+ if (theres_a_change)
+ {
+ theres_a_change = false;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ // 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;
+ }
+
+ /**
+ * Read a single pixel from the page buffer as 8-bit RGB. If
+ * {@param ignore_scan_direction} is true, coordinates are interpreted in
+ * the raw (unrotated) buffer; otherwise the scan_direction transform is
+ * applied so callers see the rotated page.
+ */
+ public void get_pixel (int x, int y, uchar[] pixel, int offset, bool ignore_scan_direction=false)
+ {
+ var true_width = is_scaled_down () ? pixels_width : scan_width;
+ var true_height = is_scaled_down () ? pixels_height : scan_height;
+ var true_rowstride = is_scaled_down () ? get_pixels_rowstride () : rowstride;
+
+ if (!ignore_scan_direction)
+ {
+ switch (scan_direction)
+ {
+ case ScanDirection.TOP_TO_BOTTOM:
+ break;
+ case ScanDirection.BOTTOM_TO_TOP:
+ x = true_width - x - 1;
+ y = true_height - y - 1;
+ break;
+ case ScanDirection.LEFT_TO_RIGHT:
+ var t = x;
+ x = true_width - y - 1;
+ y = t;
+ break;
+ case ScanDirection.RIGHT_TO_LEFT:
+ var t = x;
+ x = y;
+ y = true_height - t - 1;
+ break;
+ }
+ }
+
+ var line_offset = true_rowstride * y;
+
+ /* Optimise for 8 bit images */
+ if ((depth == 8 && n_channels == 3) || is_scaled_down ())
+ {
+ 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);
+ }
+
+ private int get_raw_pixel_value(int x, int y, int channel, uchar[] raw_pixels)
+ {
+ // if outside of the page, return white (max value)
+ if (x < 0 || y < 0 || x >= scan_width || y >= scan_height)
+ {
+ /* for line art, white is 0 */
+ if (depth == 1 && n_channels == 1)
+ return 0;
+ return (1 << depth) - 1;
+ }
+
+ int bit_position = x * n_channels * depth + channel * depth;
+ int byte_position = bit_position / 8;
+ int shift = bit_position % 8;
+
+ int byte_value = 0;
+ // get byte
+ if (depth <= 8)
+ {
+ byte_value = raw_pixels[y * rowstride + byte_position];
+ }
+ // For depths > 8 just combine the bytes.
+ else
+ {
+ for (int d = 0; d < depth / 8; d++)
+ byte_value = byte_value | raw_pixels[y * rowstride + byte_position + d] << (d * 8);
+ }
+ // remove bits on the left and right of the value we want to get
+ // for that shift the result and mask with the max value for the depth
+ // example: first pixel white (other 3 black) in 2 bit is 11000000.
+ // we want to shift it by 6 to the right and mask with 11 to get the value 3 (white)
+ byte_value = (byte_value >> (8 - shift - (depth - 1) % 8 - 1)) & ((1 << depth) - 1);
+ return byte_value;
+ }
+
+ public Gdk.Pixbuf get_image (bool apply_crop, bool full_res=false)
+ {
+ var was_scaled_down = is_scaled_down ();
+
+ if (full_res)
+ {
+ pixels_are_being_edited = true;
+ /* get full resolution image */
+ update_pixels (true, false);
+ }
+
+ /* wait until pixels are accessible */
+ pixels_mutex.lock ();
+
+ 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);
+ }
+
+ pixels_mutex.unlock ();
+ if (full_res)
+ {
+ /* reset resolution */
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+ }
+ 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 clipboard = window.get_clipboard();
+ pixels_are_being_edited = true;
+ var was_scaled_down = is_scaled_down ();
+ update_pixels (true, false);
+ var image = get_image (true);
+
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ clipboard.set_value (image);
+ }
+
+ /* Get grayscale pixel values
+ using luminance */
+ private int[] get_grayscale_pixels (bool ignore_scan_direction=false)
+ {
+ /* wait until pixels are accessible */
+ pixels_mutex.lock ();
+
+ var h = ignore_scan_direction ? scan_height : height;
+ var w = ignore_scan_direction ? scan_width : width;
+ var gray = new int[h * w];
+ uint8[] image_pixels = new uint8[h * w * 3];
+
+ for (var y = 0; y < h; y++)
+ {
+ for (var x = 0; x < w; x++)
+ get_pixel (x, y, image_pixels, 3 * (y * w + x), ignore_scan_direction);
+ }
+
+ for (var i = 0; i < h * w; i++)
+ {
+ var r = image_pixels[i * 3 + 0];
+ var g = image_pixels[i * 3 + 1];
+ var b = image_pixels[i * 3 + 2];
+ gray[i] = (int) (0.299 * r + 0.587 * g + 0.114 * b + 0.5);
+ }
+ image_pixels = null;
+ pixels_mutex.unlock ();
+ return gray;
+ }
+
+ /* Check if page is empty based on brightness changes threshold.
+ * For each line of pixels, counts significant brightness changes (> 95) between consecutive pixels.
+ * If any line has more changes than the threshold, the page is considered not empty.
+ * Returns true if the page is empty (threshold not exceeded on any line), false otherwise.
+ */
+
+ public bool is_empty_page (int threshold)
+ {
+ if (!has_data)
+ return true;
+
+ int brightness_jump = 95;
+
+ /* threshold 0 is never empty... */
+ if (threshold == 0)
+ return false;
+
+ /* get full resolution */
+ pixels_are_being_edited = true;
+ var was_scaled_down = is_scaled_down ();
+ update_pixels (true, false);
+
+ var gray = get_grayscale_pixels ();
+
+ /* Check each line for brightness changes */
+ for (uint row = 0; row < height; row++)
+ {
+ int brightness_changes = 0;
+ int previous_brightness = -1;
+ var line_offset = row * width;
+
+ /* For each pixel in this line, calculate brightness and compare to previous */
+ for (int col = 0; col < width; col++)
+ {
+ int current_brightness = gray[line_offset + col];
+
+ if (previous_brightness >= 0)
+ {
+ var brightness_diff = (current_brightness - previous_brightness).abs ();
+ if (brightness_diff > brightness_jump)
+ brightness_changes++;
+ }
+
+ previous_brightness = current_brightness;
+ }
+
+ /* If this line exceeds the threshold, page is not empty */
+ if (brightness_changes >= threshold)
+ {
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ notify_pixels_changed ();
+ return false;
+ }
+ }
+
+ gray = null;
+
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ notify_pixels_changed ();
+ return true;
+ }
+
+ /* Determine the orientation of the text in the image
+ by first finding the first text line and column and selecting a square in the middle of the page with the first line and column as the offset
+ and rotating it by different angles and looking for the angle
+ that has the biggest contrast between the lines of the square
+ */
+ public bool deskew_text_page ()
+ {
+ if (!has_data)
+ return false;
+
+ pixels_are_being_edited = true;
+ var was_scaled_down = is_scaled_down ();
+ /* get full resolution */
+ update_pixels (true, false);
+
+ int brightness_jump = 95;
+ int min_jumps = 8;
+
+ var gray = get_grayscale_pixels ();
+ int first_row = -1;
+ int first_col = -1;
+
+ // consider cropping area only
+ var start_x = 0;
+ var start_y = 0;
+ var stop_x = width;
+ var stop_y = height;
+ if (has_crop)
+ {
+ start_x = crop_x;
+ start_y = crop_y;
+ stop_x = crop_x + crop_width;
+ stop_y = crop_y + crop_height;
+ }
+
+ /* Find first row (height offset) */
+ for (int row = start_y; row < stop_y; row++)
+ {
+ int changes = 0;
+ for (int col = start_x + 1; col < stop_x; col++)
+ {
+ var diff = (gray[row * width + col] - gray[row * width + col - 1]).abs ();
+ if (diff > brightness_jump)
+ changes++;
+ }
+ if (changes >= min_jumps)
+ {
+ first_row = row;
+ break;
+ }
+ }
+
+ /* Find first column (width offset) */
+ for (int col = start_x; col < stop_x; col++)
+ {
+ int changes = 0;
+ for (int row = start_y + 1; row < stop_y; row++)
+ {
+ var diff = (gray[row * width + col] - gray[(row - 1) * width + col]).abs ();
+ if (diff > brightness_jump)
+ changes++;
+ }
+ if (changes >= min_jumps)
+ {
+ first_col = col;
+ break;
+ }
+ }
+
+ if (first_row < 0 || first_col < 0)
+ {
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ return false;
+ }
+
+ var cropped_height = stop_y - first_row;
+ var cropped_width = stop_x - first_col;
+ // center the square if enough space
+ if (first_row - start_y < (stop_y - start_y) / 4 && first_col - start_x < (stop_x - start_x) / 4)
+ {
+ cropped_height = (stop_y - start_y) - (2 * (first_row - start_y));
+ cropped_width = (stop_x - start_x) - (2 * (first_col - start_x));
+ }
+
+ /* we limit ourselves to angles between -16 and 16 degrees
+ and start coarse and then go finer around the best angle found
+ for performance reasons */
+ double step_width_angle = 4;
+ double stop_angle = 16 - step_width_angle;
+ double start_angle = -stop_angle;
+
+
+ /* the square edge is the biggest that can fit in the cropped area even when rotated by max_angle
+ (or 45 degrees if the angle is larger)
+ (this can be derived from a simple rotation of a square width edge 1 centered around (0|0)
+ using the rotation matrix and just looking at x of the bottom right corner for example) */
+ double max_angle = Math.fmin (stop_angle + step_width_angle, 45);
+ double max_width_factor = Math.cos (max_angle * Math.PI / 180.0) + Math.sin (max_angle * Math.PI / 180.0);
+ var edge = (int) (Math.fmin (cropped_height, cropped_width) / max_width_factor);
+
+ /* if the are is too small, then this is usuall unreliable */
+ if (edge < 100)
+ {
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ return false;
+ }
+
+ /* we start at first line plus the space we will need when rotating the square by 45 degrees
+ (although we do just +- 16 degrees for now for performance reasons) */
+ var begin_y = first_row + ((max_width_factor - 1.0) / 2.0) * edge;
+ var begin_x = first_col + ((max_width_factor - 1.0) / 2.0) * edge;
+ var center_y = begin_y + edge / 2.0;
+ var center_x = begin_x + edge / 2.0;
+
+ double best_angle = 0;
+ double best_value = -1;
+ double worst_value = edge * 255;
+
+ while (step_width_angle >= 0.1) {
+ for (double angle = start_angle; angle <= stop_angle; angle += step_width_angle)
+ {
+ /* skip already calculated angle */
+ if (best_value >= 0 && angle == best_angle) {
+ continue;
+ }
+ var rad = (angle * Math.PI) / 180.0;
+ var cos_angle = Math.cos (rad);
+ var sin_angle = Math.sin (rad);
+
+ var rotated = new double[edge * edge];
+
+ for (int yy = 0; yy < edge; yy++)
+ {
+ for (int xx = 0; xx < edge; xx++)
+ {
+ var target_x = begin_x + xx;
+ var target_y = begin_y + yy;
+ var dx = target_x - center_x;
+ var dy = target_y - center_y;
+
+ var src_x = cos_angle * dx + sin_angle * dy + center_x;
+ var src_y = -sin_angle * dx + cos_angle * dy + center_y;
+
+ int x0 = (int) Math.floor (src_x);
+ int y0 = (int) Math.floor (src_y);
+
+ /* note: it is probably enough to use only one pixel...
+ keeping the code for interpolation in case we want to use it later on */
+ /* int x1 = x0 + 1;
+ int y1 = y0 + 1;
+ double wx = src_x - x0;
+ double wy = src_y - y0; */
+
+ var v00 = gray[y0 * width + x0];
+ /* var v10 = gray[y0 * width + x1];
+ var v01 = gray[y1 * width + x0];
+ var v11 = gray[y1 * width + x1];
+
+ var top = v00 * (1.0 - wx) + v10 * wx;
+ var bottom = v01 * (1.0 - wx) + v11 * wx;
+ rotated[yy * edge + xx] = top * (1.0 - wy) + bottom * wy; */
+ rotated[yy * edge + xx] = v00;
+
+ }
+ }
+
+ double line_change_sum = 0;
+ var line_means = new double[edge];
+ for (int yy = 0; yy < edge; yy++)
+ {
+ double sum = 0;
+ for (int xx = 0; xx < edge; xx++)
+ sum += rotated[yy * edge + xx];
+ line_means[yy] = sum / edge;
+ }
+ for (int yy = 1; yy < edge; yy++)
+ line_change_sum += (line_means[yy] - line_means[yy - 1]).abs ();
+
+ if (line_change_sum > best_value)
+ {
+ best_value = line_change_sum;
+ best_angle = angle;
+ }
+ if (line_change_sum < worst_value)
+ worst_value = line_change_sum;
+ }
+ start_angle = best_angle - step_width_angle / 2;
+ stop_angle = best_angle + step_width_angle / 2;
+ step_width_angle = step_width_angle / 2;
+ }
+
+ /* it should be significantly (2 times? This is just guessing a reasonable value) better
+ than the worst angle, otherwise we probably just have a page with no or little text and it is not reliable to rotate it */
+ debug ("best angle: %f, best value: %f, worst value: %f", best_angle, best_value, worst_value);
+ if (worst_value <= 0 || best_value < worst_value * 2)
+ {
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ return false;
+ }
+
+ rotate (best_angle);
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+ gray = null;
+ notify_pixels_and_filter_changed ();
+ return true;
+ }
+
+ /* Rotate the image by the specified angle - if manual, perform autosave
+ (otherwise the automatic deskew function calls it!) */
+ public void rotate (double angle, bool manual = false)
+ {
+ keep_original_data ();
+
+ /* wait until pixels are accessible */
+ pixels_mutex.lock ();
+
+ var pixels_copy = pixels.copy ();
+
+ has_auto_rotate = true;
+
+ /* pre-calculate some values for rotation */
+ var center_x = (width - 1) / 2.0;
+ var center_y = (height - 1) / 2.0;
+ var rad = (angle * Math.PI) / 180.0;
+ var cos_angle = Math.cos (rad);
+ var sin_angle = Math.sin (rad);
+
+ /* for writing the new pixel */
+ int buffer = 0;
+ int buffer_length = 0;
+
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ var dx = x - center_x;
+ var dy = y - center_y;
+ var src_x = cos_angle * dx + sin_angle * dy + center_x;
+ var src_y = -sin_angle * dx + cos_angle * dy + center_y;
+
+ for (int c = 0; c < n_channels; c++)
+ {
+ // bilinear interpolation
+ int x0 = (int) Math.floor (src_x);
+ int y0 = (int) Math.floor (src_y);
+ int x1 = x0 + 1;
+ int y1 = y0 + 1;
+
+ var v00 = get_raw_pixel_value (x0, y0, c, pixels_copy);
+ var v10 = get_raw_pixel_value (x1, y0, c, pixels_copy);
+ var v01 = get_raw_pixel_value (x0, y1, c, pixels_copy);
+ var v11 = get_raw_pixel_value (x1, y1, c, pixels_copy);
+
+ double wx = src_x - x0;
+ double wy = src_y - y0;
+ var top = v00 * (1.0 - wx) + v10 * wx;
+ var bottom = v01 * (1.0 - wx) + v11 * wx;
+ int interpolated_value = (int) (top * (1.0 - wy) + bottom * wy + 0.5);
+
+ /* Write the interpolated value using buffer approach */
+
+ /* the position where to write the byte */
+ int byte_position = (x * n_channels * depth + c * depth) / 8;
+
+ /* store the new value in the buffer (make room for the new value and add it) */
+ buffer = (buffer << depth) + (interpolated_value & ((1 << depth) - 1));
+ buffer_length += depth;
+
+ /* write bytes from buffer */
+ int current_byte_pos = y * rowstride + byte_position;
+ while (buffer_length >= 8)
+ {
+ pixels[current_byte_pos] = (uchar) ((buffer >> (buffer_length - 8)) & ((1 << 8) - 1));
+ buffer &= (1 << (buffer_length - 8)) - 1;
+ buffer_length -= 8;
+ current_byte_pos++;
+ }
+ }
+ }
+
+ /* Reset buffer at row boundaries to avoid bit misalignment */
+ buffer = 0;
+ buffer_length = 0;
+ }
+
+ save_fullres_pixels ();
+ pixels_mutex.unlock ();
+ if (manual)
+ notify_pixels_and_filter_changed ();
+ }
+
+ /* Helper function to calculate quantiles from grayscale values in crop area */
+ private int[] get_quantiles (double quantile_percent_low, double quantile_percent_high, int[] gray)
+ {
+ /* Collect grayscale values from crop area, auto crop or else entire page.
+ NOTE: 'gray' is in raw scan coordinates (scan_width x scan_height, see
+ callers passing ignore_scan_direction=true), so the sampling region must
+ also be expressed in raw coordinates. crop_* and get_border() work in
+ rotated page coordinates, so convert them back to scan space first. */
+ int[] values = {};
+ var start_x = 0;
+ var start_y = 0;
+ var stop_x = scan_width;
+ var stop_y = scan_height;
+ if (has_crop)
+ {
+ int sx, sy, sw, sh;
+ rotated_rect_to_scan (crop_x, crop_y, crop_width, crop_height, out sx, out sy, out sw, out sh);
+ start_x = sx;
+ start_y = sy;
+ stop_x = sx + sw;
+ stop_y = sy + sh;
+ }
+ /* try to determine a good crop automatically */
+ else
+ {
+ var border = get_border();
+ if (border[0] >= 0) {
+ /* border is {first_col, first_row, last_col, last_row} in rotated coords */
+ int sx, sy, sw, sh;
+ rotated_rect_to_scan (border[0], border[1],
+ border[2] - border[0], border[3] - border[1],
+ out sx, out sy, out sw, out sh);
+ start_x = sx;
+ start_y = sy;
+ stop_x = sx + sw;
+ stop_y = sy + sh;
+ }
+ }
+
+ /* Clamp to the raw scan bounds to avoid out-of-range access */
+ start_x = int.max (0, start_x);
+ start_y = int.max (0, start_y);
+ stop_x = int.min (scan_width, stop_x);
+ stop_y = int.min (scan_height, stop_y);
+ if (stop_x <= start_x || stop_y <= start_y)
+ {
+ start_x = 0;
+ start_y = 0;
+ stop_x = scan_width;
+ stop_y = scan_height;
+ }
+
+ /* Draw 100000 samples */
+ int samples = 100000;
+ /* Rather hypothetical case: "very" small images... */
+ if ((stop_y - start_y) * (stop_x - start_x) < samples)
+ {
+ for (int y = start_y; y < stop_y; y++)
+ {
+ for (int x = start_x; x < stop_x; x++)
+ values += (gray[y * scan_width + x]);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < samples; i++)
+ {
+ int x = GLib.Random.int_range(start_x, stop_x);
+ int y = GLib.Random.int_range(start_y, stop_y);
+ values += (gray[y * scan_width + x]);
+ }
+ }
+
+ Posix.qsort (values, values.length, sizeof (int), (a, b) => { return *(int*)a - *(int*)b; }
+ );
+
+ int idx_low = (int)(quantile_percent_low / 100 * (values.length - 1));
+ int idx_high = (int)(quantile_percent_high / 100 * (values.length - 1));
+
+ int lower = values[idx_low];
+ int upper = values[idx_high];
+
+ values = null;
+ return { lower, upper };
+ }
+
+ public void convert_to_grayscale ()
+ {
+ if (!has_data)
+ return;
+
+ /* already grayscale */
+ if (n_channels == 1)
+ return;
+
+ pixels_are_being_edited = true;
+ var was_scaled_down = is_scaled_down ();
+ /* get full resolution */
+ update_pixels (true, false);
+
+ /* keep original for undo (both pixels and metadata) */
+ keep_original_data ();
+
+ var gray = get_grayscale_pixels (true);
+
+ /* wait until pixels are accessible */
+ pixels_mutex.lock ();
+
+ depth = 8;
+ n_channels = 1;
+ color_profile = null;
+ rowstride = scan_width;
+
+ pixels = null;
+ pixels = new uchar[scan_height * scan_width];
+
+ for (var i = 0; i < gray.length; i++)
+ {
+ pixels[i] = (uchar) gray[i];
+ }
+
+ save_fullres_pixels ();
+ pixels_mutex.unlock ();
+
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ notify_pixels_and_filter_changed ();
+ }
+
+ public void convert_to_2bit_grayscale ()
+ {
+ if (!has_data)
+ return;
+
+ /* already two bit depth... */
+ if (depth <= 2 && n_channels == 1)
+ return;
+
+ pixels_are_being_edited = true;
+
+ var was_scaled_down = is_scaled_down ();
+ /* get full resolution */
+ update_pixels (true, false);
+
+ keep_original_data ();
+
+ var gray = get_grayscale_pixels (true);
+ var quantiles = get_quantiles (2.5, 97.5, gray);
+
+ /* small difference - just use depth min max to get a white / black page. */
+ if (quantiles[1] - quantiles[0] < 16)
+ {
+ quantiles[0] = 0;
+ quantiles[1] = 255;
+ }
+
+ var cutoff_0 = quantiles[0] + (quantiles[1] - quantiles[0]) * 0.25;
+ var cutoff_1 = quantiles[0] + (quantiles[1] - quantiles[0]) * 0.5;
+ var cutoff_2 = quantiles[0] + (quantiles[1] - quantiles[0]) * 0.75;
+
+ /* wait until pixels are accessible */
+ pixels_mutex.lock ();
+
+ depth = 2;
+ n_channels = 1;
+ color_profile = null;
+ /* Calculate new rowstride for 2-bit images */
+ rowstride = (scan_width * 2 + 7) / 8;
+
+ pixels = null;
+ pixels = new uchar[scan_height * rowstride];
+
+ /* Convert grayscale to 2-bit with thresholds */
+ int buffer = 0;
+ int buffer_length = 0;
+ int byte_pos = 0;
+
+ for (int y = 0; y < scan_height; y++)
+ {
+ for (int x = 0; x < scan_width; x++)
+ {
+ var gray_value = gray[y * scan_width + x];
+ int bit_value = 0;
+
+ if (gray_value <= cutoff_0)
+ bit_value = 0;
+ else if (gray_value <= cutoff_1)
+ bit_value = 1;
+ else if (gray_value <= cutoff_2)
+ bit_value = 2;
+ else
+ bit_value = 3;
+
+ buffer = (buffer << 2) | bit_value;
+ buffer_length += 2;
+
+ if (buffer_length == 8)
+ {
+ pixels[byte_pos++] = (uchar) buffer;
+ buffer = 0;
+ buffer_length = 0;
+ }
+ }
+
+ /* Pad last byte if necessary at end of row */
+ if (buffer_length > 0)
+ {
+ buffer <<= (8 - buffer_length);
+ pixels[byte_pos++] = (uchar) buffer;
+ buffer = 0;
+ buffer_length = 0;
+ }
+ }
+
+ save_fullres_pixels ();
+ pixels_mutex.unlock ();
+
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ quantiles = null;
+ notify_pixels_and_filter_changed ();
+ }
+
+ public void convert_to_line_art ()
+ {
+ if (!has_data)
+ return;
+
+ /* already line art... */
+ if (depth == 1 && n_channels == 1)
+ return;
+
+ pixels_are_being_edited = true;
+ var was_scaled_down = is_scaled_down ();
+ /* get full resolution */
+ update_pixels (true, false);
+ keep_original_data ();
+
+ var gray = get_grayscale_pixels (true);
+ var quantiles = get_quantiles (2.5, 97.5, gray);
+
+ /* small difference - just use depth min max to get a white / black page. */
+ if (quantiles[1] - quantiles[0] < 16)
+ {
+ quantiles[0] = 0;
+ quantiles[1] = 255;
+ }
+
+ var cutoff = (quantiles[0] + quantiles[1]) / 2;
+
+ /* wait until pixels are accessible */
+ pixels_mutex.lock ();
+
+ depth = 1;
+ n_channels = 1;
+ color_profile = null;
+ /* Calculate new rowstride for 1-bit images */
+ rowstride = (scan_width + 7) / 8;
+
+ pixels = null;
+ pixels = new uchar[scan_height * rowstride];
+
+ /* Convert grayscale to 1-bit with threshold */
+ int buffer = 0;
+ int buffer_position = 0;
+ int byte_pos = 0;
+
+ for (int y = 0; y < scan_height; y++)
+ {
+ for (int x = 0; x < scan_width; x++)
+ {
+ var gray_value = gray[y * scan_width + x];
+ /* 0 is white, 1 black */
+ int bit_value = gray_value > cutoff ? 0 : 1;
+
+ buffer = (buffer << 1) | bit_value;
+ buffer_position++;
+
+ if (buffer_position == 8)
+ {
+ pixels[byte_pos++] = (uchar) buffer;
+ buffer = 0;
+ buffer_position = 0;
+ }
+ }
+
+ /* Pad last byte if necessary at end of row */
+ if (buffer_position > 0)
+ {
+ buffer <<= (8 - buffer_position);
+ pixels[byte_pos++] = (uchar) buffer;
+ buffer = 0;
+ buffer_position = 0;
+ }
+ }
+
+ save_fullres_pixels ();
+ pixels_mutex.unlock ();
+
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ notify_pixels_and_filter_changed ();
+ }
+
+ /* Helper function to count brightness changes */
+ private int count_changes(int[] gray, int c_width, int c_height, int index, bool is_row) {
+
+ /* value can be tuned... */
+ int brightness_jump = 95;
+
+ int changes = 0;
+ if (is_row) {
+ /* Horizontal: for row 'index', check columns */
+ for (int col = 1; col < c_width; col++) {
+ var diff = (gray[index * c_width + col] - gray[index * c_width + col - 1]).abs();
+ if (diff > brightness_jump) changes++;
+ }
+ }
+ else
+ {
+ /* Vertical: for col 'index', check rows */
+ for (int row = 1; row < c_height; row++) {
+ var diff = (gray[row * c_width + index] - gray[(row - 1) * c_width + index]).abs();
+ if (diff > brightness_jump) changes++;
+ }
+ }
+ return changes;
+ }
+
+ public int[] get_border() {
+
+ /* value can be tuned... */
+ int min_jumps = 8;
+
+ /* get full resolution */
+ pixels_are_being_edited = true;
+ var was_scaled_down = is_scaled_down ();
+ update_pixels (true, false);
+
+ var gray = get_grayscale_pixels ();
+
+ int first_row = -1;
+ /* Find first row (height offset) */
+ for (int row = 0; row < height; row++) {
+ if (count_changes(gray, width, height, row, true) >= min_jumps) {
+ first_row = row;
+ break;
+ }
+ }
+
+ /* nothing found, don't waste our time. */
+ if (first_row < 0)
+ {
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ return {-1, -1, -1, -1};
+ }
+
+ /* first column */
+ int first_col = -1;
+ for (int col = 0; col < width; col++) {
+ if (count_changes(gray, width, height, col, false) >= min_jumps) {
+ first_col = col;
+ break;
+ }
+ }
+ if (first_col < 0)
+ {
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ return {-1, -1, -1, -1};
+ }
+
+ int last_row = -1;
+ /* last row */
+ for (int row = height - 1; row >= 0; row--) {
+ if (count_changes(gray, width, height, row, true) >= min_jumps) {
+ last_row = row;
+ break;
+ }
+ }
+ /* No or small area - just return */
+ if (first_row > last_row - 100)
+ {
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ return {-1, -1, -1, -1};
+ }
+
+ /* last column */
+ int last_col = -1;
+ for (int col = width - 1; col >= 0; col--) {
+ if (count_changes(gray, width, height, col, false) >= min_jumps) {
+ last_col = col;
+ break;
+ }
+ }
+ if (first_col > last_col - 100)
+ {
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ return {-1, -1, -1, -1};
+ }
+
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ gray = null;
+ return {first_col, first_row, last_col, last_row};
+ }
+
+ public Page split_page ()
+ {
+ var copy = copy ();
+
+ /* For cropped pages, split the crop area. Otherwise, use full page. */
+ if (has_crop)
+ {
+ int crop_area_width = crop_width;
+ int crop_area_height = crop_height;
+
+ if (crop_area_width > crop_area_height)
+ {
+ /* Crop area is wider than tall - split left/right */
+ var split_x = crop_area_width / 2;
+
+ /* Original page: left half of crop */
+ set_custom_crop (split_x, crop_area_height);
+ move_crop (crop_x, crop_y);
+
+ /* Copy: right half of crop */
+ copy.set_custom_crop (split_x, crop_area_height);
+ copy.move_crop (crop_x + split_x, crop_y);
+ }
+ else
+ {
+ /* Crop area is taller than wide - split top/bottom */
+ var split_y = crop_area_height / 2;
+
+ /* Original page: top half of crop */
+ set_custom_crop (crop_area_width, split_y);
+ move_crop (crop_x, crop_y);
+
+ /* Copy: bottom half of crop */
+ copy.set_custom_crop (crop_area_width, split_y);
+ copy.move_crop (crop_x, crop_y + split_y);
+ }
+ /* Make sure to autosave any original data */
+ filter_changed ();
+ }
+ else
+ {
+ /* No crop - use original logic based on page orientation */
+ if (is_landscape)
+ {
+ /* Page is landscape - split vertically (left/right halves) */
+ var split_x = width / 2;
+
+ /* Original page: left half */
+ set_custom_crop (split_x, height);
+
+ /* Copy: right half */
+ copy.set_custom_crop (split_x, height);
+ copy.move_crop (split_x, 0);
+ }
+ else
+ {
+ /* Page is portrait - split horizontally (top/bottom halves) */
+ var split_y = height / 2;
+
+ /* Original page: top half */
+ set_custom_crop (width, split_y);
+
+ /* Copy: bottom half */
+ copy.set_custom_crop (width, split_y);
+ copy.move_crop (0, split_y);
+ }
+ }
+
+ return copy;
+ }
+
+ /**
+ * Automatically determine and apply the most appropriate color filter.
+ *
+ * Analyzes the image to find the number of predominant brightness levels
+ * - 2 levels: Convert to 1-bit grayscale (line art)
+ * - 4 levels: Convert to 2-bit grayscale
+ * - Many levels but mostly grayscale: Convert to 8-bit grayscale
+ * - Otherwise: Keep 8 bit color.
+ */
+ public void automatic_color_filter ()
+ {
+ if (!has_data)
+ return;
+
+ /* already grayscale - algorithm expects color and noise and does not work well! */
+ if (n_channels == 1)
+ return;
+
+ pixels_are_being_edited = true;
+ var was_scaled_down = is_scaled_down ();
+ update_pixels (true, false);
+
+ /* Get the image as RGB */
+ var image = get_image (true, true);
+ unowned uint8[] image_pixels = image.get_pixels ();
+ var img_width = image.get_width ();
+ var img_height = image.get_height ();
+ var rowstride = image.get_rowstride ();
+
+ int n_colored = 0;
+
+ for (int y = 0; y < img_height; y++)
+ {
+ for (int x = 0; x < img_width; x++)
+ {
+ /* Get pixel values */
+ var r = image_pixels[y * rowstride + x * 3 + 0];
+ var g = image_pixels[y * rowstride + x * 3 + 1];
+ var b = image_pixels[y * rowstride + x * 3 + 2];
+
+ /* Calculate average (gray value) */
+ double gray_avg = 0.299 * r + 0.587 * g + 0.114 * b;
+ /* Calculate weighted difference from grayscale (blue differences to gray are perceived much less than green) */
+ double diff =
+ Math.fabs(r - gray_avg)
+ + Math.fabs(g - gray_avg)
+ + Math.fabs(b - gray_avg);
+
+ if (diff >= 48)
+ {
+ n_colored++;
+ }
+ }
+ }
+
+ var n_pixels = img_height * img_width;
+ /* This threshold is rather arbitrary and can be tuned */
+ bool is_mostly_grayscale = n_colored < 0.005 * n_pixels;
+ debug ("Auto Color: Detected %d colored pixels out of %d total pixels (%.2f%%)", n_colored, n_pixels, (double)n_colored / n_pixels * 100);
+
+ var gray = get_grayscale_pixels ();
+
+ int[] counts = new int[32];
+
+ foreach (int value in gray)
+ {
+ int gray_level = value / 8;
+ counts[gray_level]++;
+ }
+
+ Posix.qsort (counts, counts.length, sizeof (int), (a, b) => { return *(int*)b - *(int*)a; });
+
+ int top_n = 0;
+ int total_count = 0;
+
+ bool 2_colors_enough = false;
+ bool 4_colors_enough = false;
+
+ foreach (var p in counts) {
+ top_n++;
+ total_count += p;
+ if (top_n == 2)
+ {
+ 2_colors_enough = total_count > n_pixels * 0.85;
+ debug ("Auto Color: Top 2 gray levels cover %d pixels (%.2f%%)", total_count, (double)total_count / n_pixels * 100);
+ }
+ if (top_n == 4)
+ {
+ 4_colors_enough = total_count > n_pixels * 0.85;
+ debug ("Auto Color: Top 4 gray levels cover %d pixels (%.2f%%)", total_count, (double)total_count / n_pixels * 100);
+ break;
+ }
+ }
+
+ if (is_mostly_grayscale)
+ {
+ if (2_colors_enough)
+ {
+ debug ("Auto Color: Applying line art filter (2 main colors detected)");
+ convert_to_line_art ();
+ }
+ else if (4_colors_enough)
+ {
+ debug ("Auto Color: Applying 2-bit grayscale filter (4 main colors detected)");
+ convert_to_2bit_grayscale ();
+ }
+ else
+ {
+ debug ("Auto Color: Applying 8-bit grayscale filter");
+ convert_to_grayscale ();
+ }
+ }
+ else
+ {
+ debug ("Auto Color: Applying no filter");
+ }
+
+ pixels_are_being_edited = false;
+ if (was_scaled_down)
+ update_pixels (false, false);
+
+ image = null;
+ gray = null;
+ }
+}
diff --git a/src/preferences-dialog.vala b/src/preferences-dialog.vala
index e2b28cc0956344c069713964194e8af8e50efa22..42aaa2ae222dd79b52ad36bea2a2affa34152f71 100644
--- a/src/preferences-dialog.vala
+++ b/src/preferences-dialog.vala
@@ -1,375 +1,289 @@
-/*
- * Copyright (C) 2009-2017 Canonical Ltd.
- * Author: Robert Ancell ,
- * Eduard Gotwig
- *
- * 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.
- */
-
-private class DpiItem: Object
-{
- public int dpi;
- public string label;
-
- public DpiItem(int dpi, string label)
- {
- this.dpi = dpi;
- this.label = label;
- }
-}
-
-private class PaperSizeItem: Object
-{
- public string label;
- public int width;
- public int height;
-
- public PaperSizeItem(string label, int width, int height)
- {
- this.label = label;
- this.width = width;
- this.height = height;
- }
-}
-
-const string SIDE_ID_FRONT = "front";
-const string SIDE_ID_BACK = "back";
-const string SIDE_ID_BOTH = "both";
-
-[GtkTemplate (ui = "/org/gnome/SimpleScan/ui/preferences-dialog.ui")]
-private class PreferencesDialog : Adw.PreferencesDialog
-{
- private Settings settings;
-
- [GtkChild]
- private unowned Adw.ComboRow text_dpi_row;
- [GtkChild]
- private unowned Adw.ComboRow photo_dpi_row;
- [GtkChild]
- private unowned Adw.ComboRow paper_size_row;
- [GtkChild]
- private unowned Gtk.Scale brightness_scale;
- [GtkChild]
- private unowned Gtk.Scale contrast_scale;
- [GtkChild]
- private unowned Gtk.Scale compression_scale;
- [GtkChild]
- private unowned Adw.ToggleGroup page_delay_toggles;
- private ListStore text_dpi_model;
- private ListStore photo_dpi_model;
- [GtkChild]
- private unowned Adw.ToggleGroup scan_side_toggles;
- private ListStore paper_size_model;
- [GtkChild]
- private unowned Gtk.Adjustment brightness_adjustment;
- [GtkChild]
- private unowned Gtk.Adjustment contrast_adjustment;
- [GtkChild]
- private unowned Gtk.Adjustment compression_adjustment;
- [GtkChild]
- private unowned Gtk.Switch postproc_enable_switch;
- [GtkChild]
- private unowned Gtk.Entry postproc_script_entry;
- [GtkChild]
- private unowned Gtk.Entry postproc_args_entry;
- [GtkChild]
- private unowned Gtk.Switch postproc_keep_original_switch;
-
- static string get_dpi_label (DpiItem device) {
- return device.label;
- }
-
- static string get_page_size_label (PaperSizeItem size) {
- return size.label;
- }
-
- public PreferencesDialog (Settings settings)
- {
- this.settings = settings;
-
- paper_size_row.expression = new Gtk.CClosureExpression (
- typeof (string),
- null,
- {},
- (Callback) get_page_size_label,
- null,
- null
- );
-
- paper_size_model = new ListStore (typeof (PaperSizeItem));
- /* Combo box value for automatic paper size */
- paper_size_model.append (new PaperSizeItem (_("Automatic"), 0, 0));
- paper_size_model.append (new PaperSizeItem ("A6", 1050, 1480));
- paper_size_model.append (new PaperSizeItem ("A5", 1480, 2100));
- paper_size_model.append (new PaperSizeItem ("A4", 2100, 2970));
- paper_size_model.append (new PaperSizeItem ("A3", 2970, 4200));
- paper_size_model.append (new PaperSizeItem ("Letter", 2159, 2794));
- paper_size_model.append (new PaperSizeItem ("Legal", 2159, 3556));
- paper_size_model.append (new PaperSizeItem ("4×6", 1016, 1524));
- paper_size_row.model = paper_size_model;
-
- text_dpi_row.expression = new Gtk.CClosureExpression (
- typeof (string),
- null,
- {},
- (Callback) get_dpi_label,
- null,
- null
- );
- text_dpi_model = new ListStore (typeof (DpiItem));
- text_dpi_row.model = text_dpi_model;
-
- photo_dpi_row.expression = new Gtk.CClosureExpression (
- typeof (string),
- null,
- {},
- (Callback) get_dpi_label,
- null,
- null
- );
- photo_dpi_model = new ListStore (typeof (DpiItem));
- photo_dpi_row.model = photo_dpi_model;
-
- var dpi = settings.get_int ("text-dpi");
- if (dpi <= 0)
- dpi = DEFAULT_TEXT_DPI;
- set_dpi_combo (text_dpi_row, DEFAULT_TEXT_DPI, dpi);
- text_dpi_row.notify["selected"].connect (() => { settings.set_int ("text-dpi", get_text_dpi ()); });
- dpi = settings.get_int ("photo-dpi");
- if (dpi <= 0)
- dpi = DEFAULT_PHOTO_DPI;
- set_dpi_combo (photo_dpi_row, DEFAULT_PHOTO_DPI, dpi);
- photo_dpi_row.notify["selected"].connect (() => { settings.set_int ("photo-dpi", get_photo_dpi ()); });
-
- set_page_side ((ScanSide) settings.get_enum ("page-side"));
- scan_side_toggles.notify["active-name"].connect (() => {
- var active_side_id = scan_side_toggles.active_name;
-
- if (active_side_id == SIDE_ID_FRONT)
- settings.set_enum ("page-side", ScanSide.FRONT);
- else if (active_side_id == SIDE_ID_BACK)
- settings.set_enum ("page-side", ScanSide.BACK);
- else
- settings.set_enum ("page-side", ScanSide.BOTH);
- });
-
- var lower = brightness_adjustment.lower;
- var darker_label = "%s".printf (_("Darker"));
- var upper = brightness_adjustment.upper;
- var lighter_label = "%s".printf (_("Lighter"));
- brightness_scale.add_mark (lower, Gtk.PositionType.BOTTOM, darker_label);
- brightness_scale.add_mark (0, Gtk.PositionType.BOTTOM, null);
- brightness_scale.add_mark (upper, Gtk.PositionType.BOTTOM, lighter_label);
- brightness_adjustment.value = settings.get_int ("brightness");
- brightness_adjustment.value_changed.connect (() => { settings.set_int ("brightness", get_brightness ()); });
-
- lower = contrast_adjustment.lower;
- var less_label = "%s".printf (_("Less"));
- upper = contrast_adjustment.upper;
- var more_label = "%s".printf (_("More"));
- contrast_scale.add_mark (lower, Gtk.PositionType.BOTTOM, less_label);
- contrast_scale.add_mark (0, Gtk.PositionType.BOTTOM, null);
- contrast_scale.add_mark (upper, Gtk.PositionType.BOTTOM, more_label);
- contrast_adjustment.value = settings.get_int ("contrast");
- contrast_adjustment.value_changed.connect (() => { settings.set_int ("contrast", get_contrast ()); });
-
- var minimum_size_label = "%s".printf (_("Minimum size"));
- compression_scale.add_mark (compression_adjustment.lower, Gtk.PositionType.BOTTOM, minimum_size_label);
- compression_scale.add_mark (75, Gtk.PositionType.BOTTOM, null);
- compression_scale.add_mark (90, Gtk.PositionType.BOTTOM, null);
- var full_detail_label = "%s".printf (_("Full detail"));
- compression_scale.add_mark (compression_adjustment.upper, Gtk.PositionType.BOTTOM, full_detail_label);
- compression_adjustment.value = settings.get_int ("jpeg-quality");
- compression_adjustment.value_changed.connect (() => { settings.set_int ("jpeg-quality", (int) compression_adjustment.value); });
-
- var paper_width = settings.get_int ("paper-width");
- var paper_height = settings.get_int ("paper-height");
- set_paper_size (paper_width, paper_height);
- paper_size_row.notify["selected"].connect (() =>
- {
- int w, h;
- get_paper_size (out w, out h);
- settings.set_int ("paper-width", w);
- settings.set_int ("paper-height", h);
- });
-
- set_page_delay (settings.get_int ("page-delay"));
- page_delay_toggles.notify["active-name"].connect(() => {
- settings.set_int ("page-delay", page_delay_toggles.active_name.to_int() * 1000);
- });
-
- // Postprocessing settings
- var postproc_enabled = settings.get_boolean ("postproc-enabled");
- postproc_enable_switch.set_state(postproc_enabled);
- toggle_postproc_visibility (postproc_enabled);
- postproc_enable_switch.state_set.connect ((is_active) => { toggle_postproc_visibility (is_active);
- settings.set_boolean("postproc-enabled", is_active);
- return true; });
-
- var postproc_script = settings.get_string("postproc-script");
- postproc_script_entry.set_text(postproc_script);
- postproc_script_entry.changed.connect (() => { settings.set_string("postproc-script", postproc_script_entry.get_text()); });
-
- var postproc_arguments = settings.get_string("postproc-arguments");
- postproc_args_entry.set_text(postproc_arguments);
- postproc_args_entry.changed.connect (() => { settings.set_string("postproc-arguments", postproc_args_entry.get_text()); });
-
- var postproc_keep_original = settings.get_boolean ("postproc-keep-original");
- postproc_keep_original_switch.set_state(postproc_keep_original);
- postproc_keep_original_switch.state_set.connect ((is_active) => { settings.set_boolean("postproc-keep-original", is_active);
- return true; });
- }
-
- private void toggle_postproc_visibility(bool enabled) {
- postproc_script_entry.get_parent ().get_parent ().get_parent ().get_parent ().set_visible(enabled);
- postproc_args_entry.get_parent ().get_parent ().get_parent ().get_parent ().set_visible(enabled);
- postproc_keep_original_switch.get_parent ().get_parent ().get_parent ().get_parent ().set_visible(enabled);
- }
-
- private void set_page_side (ScanSide page_side)
- {
- string active_side_id;
-
- switch (page_side)
- {
- case ScanSide.FRONT:
- active_side_id = SIDE_ID_FRONT;
- break;
- case ScanSide.BACK:
- active_side_id = SIDE_ID_BACK;
- break;
- default:
- case ScanSide.BOTH:
- active_side_id = SIDE_ID_BOTH;
- break;
- }
-
- scan_side_toggles.active_name = active_side_id;
- }
-
- public ScanSide get_page_side ()
- {
- var active_side_id = scan_side_toggles.active_name;
-
- if (active_side_id == SIDE_ID_FRONT)
- return ScanSide.FRONT;
- else if (active_side_id == SIDE_ID_BACK)
- return ScanSide.BACK;
- else
- return ScanSide.BOTH;
- }
-
- public void set_paper_size (int width, int height)
- {
- for (uint i = 0; i < paper_size_model.n_items; i++)
- {
- var item = paper_size_model.get_item (i) as PaperSizeItem;
- if (item.width == width && item.height == height)
- {
- paper_size_row.selected = i;
- break;
- }
- }
- }
-
- public int get_text_dpi ()
- {
- if (text_dpi_row.selected != Gtk.INVALID_LIST_POSITION)
- {
- var item = text_dpi_model.get_item (text_dpi_row.selected) as DpiItem;
- return item.dpi;
- }
-
- return DEFAULT_TEXT_DPI;
- }
-
- public int get_photo_dpi ()
- {
- if (photo_dpi_row.selected != Gtk.INVALID_LIST_POSITION)
- {
- var item = photo_dpi_model.get_item (photo_dpi_row.selected) as DpiItem;
- return item.dpi;
- }
-
- return DEFAULT_PHOTO_DPI;
- }
-
- public bool get_paper_size (out int width, out int height)
- {
- width = height = 0;
- if (paper_size_row.selected != Gtk.INVALID_LIST_POSITION)
- {
- var item = paper_size_model.get_item (paper_size_row.selected) as PaperSizeItem;
- width = item.width;
- height = item.height;
- return true;
- }
-
- return false;
- }
-
- public int get_brightness ()
- {
- return (int) brightness_adjustment.value;
- }
-
- public void set_brightness (int brightness)
- {
- brightness_adjustment.value = brightness;
- }
-
- public int get_contrast ()
- {
- return (int) contrast_adjustment.value;
- }
-
- public void set_contrast (int contrast)
- {
- contrast_adjustment.value = contrast;
- }
-
- public int get_page_delay ()
- {
- return page_delay_toggles.active_name.to_int() * 1000;
- }
-
- public void set_page_delay (int page_delay)
- {
- page_delay_toggles.active_name = (page_delay / 1000).to_string();
- }
-
- private void set_dpi_combo (Adw.ComboRow combo, int default_dpi, int current_dpi)
- {
- var model = combo.model as ListStore;
- int[] scan_resolutions = {75, 150, 200, 300, 600, 1200, 2400};
-
- for (var i = 0; i < scan_resolutions.length; i++)
- {
- var dpi = scan_resolutions[i];
-
- string label;
- if (dpi == default_dpi)
- /* Preferences dialog: Label for default resolution in resolution list */
- label = _("%d dpi (default)").printf (dpi);
- else if (dpi == 75)
- /* Preferences dialog: Label for minimum resolution in resolution list */
- label = _("%d dpi (draft)").printf (dpi);
- else if (dpi == 1200)
- /* Preferences dialog: Label for maximum resolution in resolution list */
- label = _("%d dpi (high resolution)").printf (dpi);
- else
- /* Preferences dialog: Label for resolution value in resolution list (dpi = dots per inch) */
- label = _("%d dpi").printf (dpi);
-
- model.append (new DpiItem (dpi, label));
-
- if (dpi == current_dpi)
- combo.selected = i;
-
- }
- }
-}
+/*
+ * Copyright (C) 2009-2017 Canonical Ltd.
+ * Author: Robert Ancell ,
+ * Eduard Gotwig
+ *
+ * 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.
+ */
+
+const string ORIENTATION_ID_TOP = "top";
+const string ORIENTATION_ID_BOTTOM = "bottom";
+const string ORIENTATION_ID_LEFT = "left";
+const string ORIENTATION_ID_RIGHT = "right";
+
+[GtkTemplate (ui = "/org/gnome/SimpleScan/ui/preferences-dialog.ui")]
+private class PreferencesDialog : Adw.PreferencesDialog
+{
+ private Settings settings;
+
+ [GtkChild]
+ private unowned Gtk.Scale brightness_scale;
+ [GtkChild]
+ private unowned Gtk.Scale contrast_scale;
+ [GtkChild]
+ private unowned Gtk.Scale compression_scale;
+ [GtkChild]
+ private unowned Gtk.Scale empty_page_threshold_scale;
+ [GtkChild]
+ private unowned Adw.ToggleGroup page_delay_toggles;
+ [GtkChild]
+ private unowned Adw.ToggleGroup page_orientation_toggles;
+ [GtkChild]
+ private unowned Adw.SwitchRow auto_rotation_switch;
+ [GtkChild]
+ private unowned Adw.SwitchRow auto_color_reduction_switch;
+ [GtkChild]
+ private unowned Adw.SwitchRow alternating_page_orientation_switch;
+ [GtkChild]
+ private unowned Gtk.Adjustment brightness_adjustment;
+ [GtkChild]
+ private unowned Gtk.Adjustment contrast_adjustment;
+ [GtkChild]
+ private unowned Gtk.Adjustment empty_page_threshold_adjustment;
+ [GtkChild]
+ private unowned Gtk.Adjustment compression_adjustment;
+ [GtkChild]
+ private unowned Adw.SwitchRow postproc_enable_switch;
+ [GtkChild]
+ private unowned Gtk.Entry postproc_script_entry;
+ [GtkChild]
+ private unowned Gtk.Entry postproc_args_entry;
+ [GtkChild]
+ private unowned Adw.SwitchRow postproc_keep_original_switch;
+
+ public PreferencesDialog (Settings settings)
+ {
+ this.settings = settings;
+
+ set_page_orientation ((PageOrientation) settings.get_enum ("page-orientation"));
+ page_orientation_toggles.notify["active-name"].connect (() => {
+ var active_orientation_id = page_orientation_toggles.active_name;
+
+ if (active_orientation_id == ORIENTATION_ID_RIGHT)
+ settings.set_enum ("page-orientation", PageOrientation.RIGHT);
+ else if (active_orientation_id == ORIENTATION_ID_BOTTOM)
+ settings.set_enum ("page-orientation", PageOrientation.BOTTOM);
+ else if (active_orientation_id == ORIENTATION_ID_LEFT)
+ settings.set_enum ("page-orientation", PageOrientation.LEFT);
+ else
+ settings.set_enum ("page-orientation", PageOrientation.TOP);
+ });
+
+ var lower = brightness_adjustment.lower;
+ var darker_label = "%s".printf (_("Darker"));
+ var upper = brightness_adjustment.upper;
+ var lighter_label = "%s".printf (_("Lighter"));
+ brightness_scale.add_mark (lower, Gtk.PositionType.BOTTOM, darker_label);
+ brightness_scale.add_mark (0, Gtk.PositionType.BOTTOM, null);
+ brightness_scale.add_mark (upper, Gtk.PositionType.BOTTOM, lighter_label);
+ brightness_adjustment.value = settings.get_int ("brightness");
+ brightness_adjustment.value_changed.connect (() => { settings.set_int ("brightness", get_brightness ()); });
+
+ lower = contrast_adjustment.lower;
+ var less_label = "%s".printf (_("Less"));
+ upper = contrast_adjustment.upper;
+ var more_label = "%s".printf (_("More"));
+ contrast_scale.add_mark (lower, Gtk.PositionType.BOTTOM, less_label);
+ contrast_scale.add_mark (0, Gtk.PositionType.BOTTOM, null);
+ contrast_scale.add_mark (upper, Gtk.PositionType.BOTTOM, more_label);
+ contrast_adjustment.value = settings.get_int ("contrast");
+ contrast_adjustment.value_changed.connect (() => { settings.set_int ("contrast", get_contrast ()); });
+
+ var minimum_size_label = "%s".printf (_("Minimum size"));
+ compression_scale.add_mark (compression_adjustment.lower, Gtk.PositionType.BOTTOM, minimum_size_label);
+ compression_scale.add_mark (75, Gtk.PositionType.BOTTOM, null);
+ compression_scale.add_mark (90, Gtk.PositionType.BOTTOM, null);
+ var full_detail_label = "%s".printf (_("Full detail"));
+ compression_scale.add_mark (compression_adjustment.upper, Gtk.PositionType.BOTTOM, full_detail_label);
+ compression_adjustment.value = settings.get_int ("jpeg-quality");
+ compression_adjustment.value_changed.connect (() => { settings.set_int ("jpeg-quality", (int) compression_adjustment.value); });
+
+ set_page_delay (settings.get_int ("page-delay"));
+ page_delay_toggles.notify["active-name"].connect(() => {
+ settings.set_int ("page-delay", int.parse (page_delay_toggles.active_name) * 1000);
+ });
+
+ set_empty_page_threshold (settings.get_int ("empty-page-threshold"));
+ empty_page_threshold_adjustment.value_changed.connect (() => { settings.set_int ("empty-page-threshold", (int) empty_page_threshold_adjustment.value); });
+ var empty_page_0_label = "%s".printf (_("off"));
+ empty_page_threshold_scale.add_mark (0, Gtk.PositionType.BOTTOM, empty_page_0_label);
+ var empty_page_sparse_label = "%s".printf (_("balanced"));
+ empty_page_threshold_scale.add_mark (15, Gtk.PositionType.BOTTOM, empty_page_sparse_label);
+ var empty_page_max_label = "%s".printf (_("eager"));
+ empty_page_threshold_scale.add_mark (40, Gtk.PositionType.BOTTOM, empty_page_max_label);
+
+ set_auto_rotation_correction (settings.get_boolean ("auto-rotation-correction"));
+ auto_rotation_switch.notify["active"].connect (() => {
+ settings.set_boolean ("auto-rotation-correction", auto_rotation_switch.active);
+ });
+
+ set_auto_color_reduction (settings.get_boolean ("auto-color-reduction"));
+ auto_color_reduction_switch.notify["active"].connect (() => {
+ settings.set_boolean ("auto-color-reduction", auto_color_reduction_switch.active);
+ });
+
+ set_alternating_page_orientation (settings.get_boolean ("alternating-page-orientation"));
+ alternating_page_orientation_switch.notify["active"].connect (() => {
+ settings.set_boolean ("alternating-page-orientation", alternating_page_orientation_switch.active);
+ });
+
+ // Postprocessing settings
+ var postproc_enabled = settings.get_boolean ("postproc-enabled");
+ postproc_enable_switch.active = postproc_enabled;
+ toggle_postproc_visibility (postproc_enabled);
+ postproc_enable_switch.notify["active"].connect (() => {
+ toggle_postproc_visibility (postproc_enable_switch.active);
+ settings.set_boolean("postproc-enabled", postproc_enable_switch.active);
+ });
+
+ var postproc_script = settings.get_string("postproc-script");
+ postproc_script_entry.set_text(postproc_script);
+ postproc_script_entry.changed.connect (() => { settings.set_string("postproc-script", postproc_script_entry.get_text()); });
+
+ var postproc_arguments = settings.get_string("postproc-arguments");
+ postproc_args_entry.set_text(postproc_arguments);
+ postproc_args_entry.changed.connect (() => { settings.set_string("postproc-arguments", postproc_args_entry.get_text()); });
+
+ var postproc_keep_original = settings.get_boolean ("postproc-keep-original");
+ postproc_keep_original_switch.active = postproc_keep_original;
+ postproc_keep_original_switch.notify["active"].connect (() => {
+ settings.set_boolean("postproc-keep-original", postproc_keep_original_switch.active);
+ });
+ }
+
+ private void toggle_postproc_visibility(bool enabled) {
+ postproc_script_entry.get_parent().get_parent ().get_parent ().get_parent().set_visible(enabled);
+ postproc_args_entry.get_parent ().get_parent ().get_parent ().get_parent().set_visible(enabled);
+ postproc_keep_original_switch.set_visible(enabled);
+ }
+
+ public void set_page_side (ScanSide page_side)
+ {
+ settings.set_enum ("page-side", page_side);
+ }
+
+ public ScanSide get_page_side ()
+ {
+ return (ScanSide) settings.get_enum ("page-side");
+ }
+
+ public void set_page_orientation (PageOrientation page_orientation)
+ {
+ string active_orientation_id;
+
+ switch (page_orientation)
+ {
+ case PageOrientation.RIGHT:
+ active_orientation_id = ORIENTATION_ID_RIGHT;
+ break;
+ case PageOrientation.BOTTOM:
+ active_orientation_id = ORIENTATION_ID_BOTTOM;
+ break;
+ case PageOrientation.LEFT:
+ active_orientation_id = ORIENTATION_ID_LEFT;
+ break;
+ default:
+ case PageOrientation.TOP:
+ active_orientation_id = ORIENTATION_ID_TOP;
+ break;
+ }
+
+ page_orientation_toggles.active_name = active_orientation_id;
+ }
+
+ public PageOrientation get_page_orientation ()
+ {
+ var active_orientation_id = page_orientation_toggles.active_name;
+
+ if (active_orientation_id == ORIENTATION_ID_TOP)
+ return PageOrientation.TOP;
+ else if (active_orientation_id == ORIENTATION_ID_BOTTOM)
+ return PageOrientation.BOTTOM;
+ else if (active_orientation_id == ORIENTATION_ID_LEFT)
+ return PageOrientation.LEFT;
+ else
+ return PageOrientation.RIGHT;
+ }
+
+ public bool get_paper_size (out int width, out int height, out string size_label)
+ {
+ width = settings.get_int ("paper-width");
+ height = settings.get_int ("paper-height");
+ size_label = settings.get_string ("page-size");
+ return true;
+ }
+
+ public int get_brightness ()
+ {
+ return (int) brightness_adjustment.value;
+ }
+
+ public void set_brightness (int brightness)
+ {
+ brightness_adjustment.value = brightness;
+ }
+
+ public int get_contrast ()
+ {
+ return (int) contrast_adjustment.value;
+ }
+
+ public void set_contrast (int contrast)
+ {
+ contrast_adjustment.value = contrast;
+ }
+
+ public int get_page_delay ()
+ {
+ return int.parse (page_delay_toggles.active_name) * 1000;
+ }
+
+ public void set_page_delay (int page_delay)
+ {
+ page_delay_toggles.active_name = (page_delay / 1000).to_string();
+ }
+
+ public int get_empty_page_threshold ()
+ {
+ return (int) empty_page_threshold_adjustment.value;
+ }
+
+ public void set_empty_page_threshold (int threshold)
+ {
+ empty_page_threshold_adjustment.value = threshold;
+ }
+
+ public bool get_auto_rotation_correction ()
+ {
+ return auto_rotation_switch.active;
+ }
+
+ public void set_auto_rotation_correction (bool auto_rotation_correction)
+ {
+ auto_rotation_switch.active = auto_rotation_correction;
+ }
+
+ public bool get_auto_color_reduction ()
+ {
+ return auto_color_reduction_switch.active;
+ }
+
+ public void set_auto_color_reduction (bool auto_color_reduction)
+ {
+ auto_color_reduction_switch.active = auto_color_reduction;
+ }
+
+ public bool get_alternating_page_orientation ()
+ {
+ return alternating_page_orientation_switch.active;
+ }
+
+ public void set_alternating_page_orientation (bool alternating_page_orientation)
+ {
+ alternating_page_orientation_switch.active = alternating_page_orientation;
+ }
+}
diff --git a/src/scanner.vala b/src/scanner.vala
index 08894c80e34a7cb84639b36997a34843153ffa65..9e70b774a90c25480c5673dadf0367fc107b79b9 100644
--- a/src/scanner.vala
+++ b/src/scanner.vala
@@ -1,1836 +1,2031 @@
-/*
- * 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.
- */
-
-/* TODO: Could indicate the start of the next page immediately after the last page is received (i.e. before the sane_cancel()) */
-
-public class ScanDevice : Object
-{
- public string name;
- public string label;
-}
-
-public class ScanPageInfo : Object
-{
- /* Width, height in pixels */
- public int width;
- public int height;
-
- /* Bit depth */
- public int depth;
-
- /* Number of colour channels */
- public int n_channels;
-
- /* Resolution */
- public double dpi;
-
- /* The device this page came from */
- public string device;
-}
-
-public class ScanLine : Object
-{
- /* Line number */
- public int number;
-
- /* Number of lines in this packet */
- public int n_lines;
-
- /* Width in pixels and format */
- public int width;
- public int depth;
-
- /* Channel for this line or -1 for all channels */
- public int channel;
-
- /* Raw line data */
- public uchar[] data;
- public int data_length;
-}
-
-public enum ScanMode
-{
- DEFAULT,
- COLOR,
- GRAY,
- LINEART
-}
-
-public enum ScanType
-{
- SINGLE,
- ADF,
- BATCH
-}
-
-public enum ScanSide
-{
- FRONT,
- BACK,
- BOTH
-}
-
-public class ScanOptions : Object
-{
- public int dpi;
- public ScanMode scan_mode;
- public int depth;
- public ScanType type;
- public ScanSide side;
- public int paper_width;
- public int paper_height;
- public int brightness;
- public int contrast;
- public int page_delay;
-}
-
-private class ScanJob : Object
-{
- public int id;
- public string device;
- public double dpi;
- public ScanMode scan_mode;
- public int depth;
- public ScanType type;
- public ScanSide side;
- public int page_width;
- public int page_height;
- public int brightness;
- public int contrast;
- public int page_delay;
-}
-
-private class Request {}
-
-private class RequestRedetect : Request {}
-
-private class RequestCancel : Request {}
-
-private class RequestStartScan : Request
-{
- public ScanJob job;
-}
-
-private class RequestQuit : Request {}
-
-private class Credentials
-{
- public string username;
- public string password;
-}
-
-private enum ScanState
-{
- IDLE = 0,
- REDETECT,
- OPEN,
- GET_OPTION,
- START,
- GET_PARAMETERS,
- READ
-}
-
-private class Notify
-{
- public virtual void run (Scanner scanner) {}
-}
-
-private class NotifyScanningChanged : Notify
-{
- public override void run (Scanner scanner) { scanner.scanning_changed (); }
-}
-
-private class NotifyUpdateDevices : Notify
-{
- public NotifyUpdateDevices (owned List devices) { this.devices = (owned) devices; }
- private List devices;
- public override void run (Scanner scanner) { scanner.update_devices (devices); }
-}
-
-private class NotifyRequestAuthorization : Notify
-{
- public NotifyRequestAuthorization (string resource) { this.resource = resource; }
- private string resource;
- public override void run (Scanner scanner) { scanner.request_authorization (resource); }
-}
-
-private class NotifyScanFailed : Notify
-{
- public NotifyScanFailed (int error_code, string error_string) { this.error_code = error_code; this.error_string = error_string; }
- private int error_code;
- private string error_string;
- public override void run (Scanner scanner) { scanner.scan_failed (error_code, error_string); }
-}
-
-private class NotifyDocumentDone : Notify
-{
- public override void run (Scanner scanner) { scanner.document_done (); }
-}
-
-private class NotifyExpectPage : Notify
-{
- public override void run (Scanner scanner) { scanner.expect_page (); }
-}
-
-private class NotifyGotPageInfo : Notify
-{
- public NotifyGotPageInfo (int job_id, ScanPageInfo info) { this.job_id = job_id; this.info = info; }
- private int job_id;
- private ScanPageInfo info;
- public override void run (Scanner scanner)
- {
- if (job_id >= scanner.first_job_id && job_id < scanner.job_id)
- scanner.got_page_info (info);
- }
-}
-
-private class NotifyPageDone : Notify
-{
- public NotifyPageDone (int job_id) { this.job_id = job_id; }
- private int job_id;
- public override void run (Scanner scanner)
- {
- if (job_id >= scanner.first_job_id && job_id < scanner.job_id)
- scanner.page_done ();
- }
-}
-
-private class NotifyGotLine : Notify
-{
- public NotifyGotLine (int job_id, ScanLine line) { this.job_id = job_id; this.line = line; }
- private int job_id;
- private ScanLine line;
- public override void run (Scanner scanner)
- {
- if (job_id >= scanner.first_job_id && job_id < scanner.job_id)
- scanner.got_line (line);
- }
-}
-
-public class Scanner : Object
-{
- /* Singleton object */
- private static Scanner scanner_object = null;
-
- /* Thread communicating with SANE */
- private Thread thread;
-
- /* Queue of requests from main thread */
- private AsyncQueue request_queue;
-
- /* Queue of events to notify in main queue */
- private AsyncQueue notify_queue;
-
- /* Queue of responses to authorization requests */
- private AsyncQueue authorize_queue;
-
- /* ID for the current job */
- public int first_job_id;
- public int job_id;
-
- private string? default_device;
-
- private ScanState state;
- private bool need_redetect;
-
- private List job_queue;
-
- /* Handle to SANE device */
- private Sane.Handle handle;
- private bool have_handle;
- private string? current_device;
-
- private Sane.Parameters parameters;
-
- /* Last option read */
- private Sane.Int option_index;
-
- /* Table of options */
- private HashTable options;
-
- /* Buffer for received line */
- private uchar[] buffer;
- private int n_used;
-
- //private int bytes_remaining;
- private int line_count;
- private int pass_number;
- private int page_number;
- private int notified_page;
-
- private bool scanning;
-
- public signal void update_devices (List devices);
- public signal void request_authorization (string resource);
- public signal void expect_page ();
- public signal void got_page_info (ScanPageInfo info);
- public signal void got_line (ScanLine line);
- public signal void scan_failed (int error_code, string error_string);
- public signal void page_done ();
- public signal void document_done ();
- public signal void scanning_changed ();
-
- private Scanner ()
- {
- request_queue = new AsyncQueue ();
- notify_queue = new AsyncQueue ();
- authorize_queue = new AsyncQueue ();
- }
-
- public static Scanner get_instance ()
- {
- if (scanner_object == null)
- scanner_object = new Scanner ();
- return scanner_object;
- }
-
- private bool notify_idle_cb ()
- {
- var notification = notify_queue.pop ();
- notification.run (this);
- return false;
- }
-
- private void notify_event (Notify notification)
- {
- notify_queue.push (notification);
- Idle.add (notify_idle_cb);
- }
-
- private void set_scanning (bool is_scanning)
- {
- if ((scanning && !is_scanning) || (!scanning && is_scanning))
- {
- scanning = is_scanning;
- notify_event (new NotifyScanningChanged ());
- }
- }
-
- private static int get_device_weight (string device)
- {
- /* NOTE: This is using trends in the naming of SANE devices, SANE should be able to provide this information better */
-
- /* Use webcams as a last resort */
- if (device.has_prefix ("vfl:"))
- return 2;
-
- /* Use locally connected devices first */
- if (device.contains ("usb"))
- return 0;
-
- return 1;
- }
-
- private static int compare_devices (ScanDevice device1, ScanDevice device2)
- {
- /* TODO: Should do some fuzzy matching on the last selected device and set that to the default */
-
- var weight1 = get_device_weight (device1.name);
- var weight2 = get_device_weight (device2.name);
- if (weight1 != weight2)
- return weight1 - weight2;
-
- return strcmp (device1.label, device2.label);
- }
-
- private void do_redetect ()
- {
- unowned Sane.Device[] device_list = null;
- var status = Sane.get_devices (out device_list, false);
- debug ("sane_get_devices () -> %s", Sane.status_to_string (status));
- if (status != Sane.Status.GOOD)
- {
- warning ("Unable to get SANE devices: %s", Sane.strstatus(status));
- need_redetect = false;
- state = ScanState.IDLE;
- return;
- }
-
- /* Determine the number of each model to additionally display the name if the model names are the same. */
- var seen = new HashTable (str_hash, str_equal);
- for (var i = 0; device_list[i] != null; i++)
- {
- if (seen.contains(device_list[i].model))
- seen.set(device_list[i].model, seen.get(device_list[i].model) + 1);
- else
- seen.set(device_list[i].model, 1);
- }
-
- var devices = new List ();
- for (var i = 0; device_list[i] != null; i++)
- {
- /* Virtual devices tend to not be scanners. Skip them. */
- if (device_list[i].type == "virtual device")
- continue;
-
- debug ("Device: name=\"%s\" vendor=\"%s\" model=\"%s\" type=\"%s\"",
- device_list[i].name, device_list[i].vendor, device_list[i].model, device_list[i].type);
-
- var scan_device = new ScanDevice ();
- scan_device.name = device_list[i].name;
-
- /* Abbreviate HP as it is a long string and does not match what is on the physical scanner */
- var vendor = device_list[i].vendor;
- if (vendor == "Hewlett-Packard")
- vendor = "HP";
-
- /* Don't repeat vendor name */
- if (device_list[i].model.down().has_prefix (vendor.down()))
- scan_device.label = device_list[i].model;
- else
- scan_device.label = "%s %s".printf (vendor, device_list[i].model);
-
- /* Replace underscores in name */
- scan_device.label = scan_device.label.replace ("_", " ");
-
- /* Additionally add the device name to the label if there are several identical models. */
- if (seen.get(device_list[i].model) > 1)
- scan_device.label = "%s on %s".printf (scan_device.label, device_list[i].name);
-
-
- devices.append (scan_device);
- }
-
- /* Sort devices by priority */
- devices.sort (compare_devices);
-
- need_redetect = false;
- state = ScanState.IDLE;
-
- if (devices != null)
- {
- var device = devices.nth_data (0);
- default_device = device.name;
- }
- else
- default_device = null;
-
- notify_event (new NotifyUpdateDevices ((owned) devices));
- }
-
- private double scale_fixed (int source_min, int source_max, Sane.OptionDescriptor option, int value)
- {
- var v = (double) value;
-
- return_val_if_fail (option.type == Sane.ValueType.FIXED, value);
- if (option.constraint_type == Sane.ConstraintType.RANGE && option.range.max != option.range.min)
- {
- v -= (double) source_min;
- v *= Sane.UNFIX (option.range.max) - Sane.UNFIX (option.range.min);
- v /= (double) (source_max - source_min);
- v += Sane.UNFIX (option.range.min);
- debug ("scale_fixed: scaling %d [min: %d, max: %d] to %f [min: %f, max: %f]",
- value, source_min, source_max, v, Sane.UNFIX (option.range.min), Sane.UNFIX (option.range.max));
- }
-
- return v;
- }
-
- private int scale_int (int source_min, int source_max, Sane.OptionDescriptor option, int value)
- {
- var v = value;
-
- return_val_if_fail (option.type == Sane.ValueType.INT, value);
-
- if (option.constraint_type == Sane.ConstraintType.RANGE && option.range.max != option.range.min)
- {
- v -= source_min;
- v *= (int) (option.range.max - option.range.min);
- v /= (source_max - source_min);
- v += (int) option.range.min;
- debug ("scale_int: scaling %d [min: %d, max: %d] to %d [min: %d, max: %d]",
- value, source_min, source_max, v, (int) option.range.min, (int) option.range.max);
- }
-
- return v;
- }
-
- private bool set_default_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index)
- {
- /* Check if supports automatic option */
- if ((option.cap & Sane.Capability.AUTOMATIC) == 0)
- return false;
-
- var status = Sane.control_option (handle, option_index, Sane.Action.SET_AUTO, null, null);
- debug ("sane_control_option (%d, SANE_ACTION_SET_AUTO, %s=auto) -> %s", (int) option_index, option.name, Sane.status_to_string (status));
- if (status != Sane.Status.GOOD)
- warning ("Error setting default option %s: %s", option.name, Sane.strstatus(status));
-
- return status == Sane.Status.GOOD;
- }
-
- private void set_bool_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, bool value, out bool result)
- {
- return_if_fail (option.type == Sane.ValueType.BOOL);
-
- Sane.Bool v = (Sane.Bool) value;
- var status = Sane.control_option (handle, option_index, Sane.Action.SET_VALUE, &v, null);
- result = (bool) v;
- debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=%s) -> (%s, %s)", (int) option_index, option.name, value ? "SANE_TRUE" : "SANE_FALSE", Sane.status_to_string (status), result ? "SANE_TRUE" : "SANE_FALSE");
- }
-
- private void set_int_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, int value, out int result)
- {
- return_if_fail (option.type == Sane.ValueType.INT);
-
- Sane.Int v = (Sane.Int) value;
- if (option.constraint_type == Sane.ConstraintType.RANGE)
- {
- if (option.range.quant != 0)
- v *= option.range.quant;
- if (v < option.range.min)
- v = option.range.min;
- if (v > option.range.max)
- v = option.range.max;
- }
- else if (option.constraint_type == Sane.ConstraintType.WORD_LIST)
- {
- int distance = int.MAX, nearest = 0;
-
- /* Find nearest value to requested */
- for (var i = 0; i < option.word_list[0]; i++)
- {
- var x = (int) option.word_list[i+1];
- var d = (int) (x - v);
- d = d.abs ();
- if (d < distance)
- {
- distance = d;
- nearest = x;
- }
- }
- v = (Sane.Int) nearest;
- }
-
- var status = Sane.control_option (handle, option_index, Sane.Action.SET_VALUE, &v, null);
- debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=%d) -> (%s, %d)", (int) option_index, option.name, value, Sane.status_to_string (status), (int) v);
- result = (int) v;
- }
-
- private void set_fixed_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, double value, out double result)
- {
- double v = value;
- Sane.Fixed v_fixed;
-
- return_if_fail (option.type == Sane.ValueType.FIXED);
-
- if (option.constraint_type == Sane.ConstraintType.RANGE)
- {
- double min = Sane.UNFIX (option.range.min);
- double max = Sane.UNFIX (option.range.max);
-
- if (v < min)
- v = min;
- if (v > max)
- v = max;
- }
- else if (option.constraint_type == Sane.ConstraintType.WORD_LIST)
- {
- double distance = double.MAX, nearest = 0.0;
-
- /* Find nearest value to requested */
- for (var i = 0; i < option.word_list[0]; i++)
- {
- double x = Sane.UNFIX (option.word_list[i+1]);
- if (Math.fabs (x - v) < distance)
- {
- distance = Math.fabs (x - v);
- nearest = x;
- }
- }
- v = nearest;
- }
-
- v_fixed = Sane.FIX (v);
- var status = Sane.control_option (handle, option_index, Sane.Action.SET_VALUE, &v_fixed, null);
- debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=%f) -> (%s, %f)", (int) option_index, option.name, value, Sane.status_to_string (status), Sane.UNFIX (v_fixed));
-
- result = Sane.UNFIX (v_fixed);
- }
-
- private void set_fixed_or_int_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, double value, out double result)
- {
- if (option.type == Sane.ValueType.FIXED)
- set_fixed_option (handle, option, option_index, value, out result);
- else if (option.type == Sane.ValueType.INT)
- {
- int r;
- set_int_option (handle, option, option_index, (int) Math.round (value), out r);
- result = r;
- }
- else
- {
- result = 0.0;
- warning ("Unable to set unsupported option type");
- }
- }
-
- private void set_option_to_max (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index)
- {
- if (option.constraint_type != Sane.ConstraintType.RANGE)
- return;
-
- var status = Sane.control_option (handle, option_index, Sane.Action.SET_VALUE, &option.range.max, null);
-
- if (option.type == Sane.ValueType.FIXED)
- debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=option.range.max=%f) -> (%s)", (int) option_index, option.name, Sane.UNFIX (option.range.max), Sane.status_to_string (status));
- else
- debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=option.range.max=%d) -> (%s)", (int) option_index, option.name, (int) option.range.max, Sane.status_to_string (status));
- }
-
- private bool set_string_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, string value, out string result)
- {
- result = "";
-
- return_val_if_fail (option.type == Sane.ValueType.STRING, false);
-
- var s = new char[option.size];
- var i = 0;
- for (; i < (option.size - 1) && value[i] != '\0'; i++)
- s[i] = value[i];
- s[i] = '\0';
- var status = Sane.control_option (handle, option_index, Sane.Action.SET_VALUE, s, null);
- result = (string) s;
- debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=\"%s\") -> (%s, \"%s\")", (int) option_index, option.name, value, Sane.status_to_string (status), result);
-
- return status == Sane.Status.GOOD;
- }
-
- private bool set_constrained_string_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, string[] values, out string result)
- {
- return_val_if_fail (option.type == Sane.ValueType.STRING, false);
- return_val_if_fail (option.constraint_type == Sane.ConstraintType.STRING_LIST, false);
-
- for (var i = 0; values[i] != null; i++)
- {
- var j = 0;
- for (; option.string_list[j] != null; j++)
- {
- if (values[i] == option.string_list[j])
- break;
- }
-
- if (option.string_list[j] != null)
- return set_string_option (handle, option, option_index, values[i], out result);
- }
-
- result = "";
- return false;
- }
-
- private void log_option (Sane.Int index, Sane.OptionDescriptor option)
- {
- var s = "Option %d:".printf ((int) index);
-
- if (option.name != "")
- s += " name='%s'".printf (option.name);
-
- if (option.title != "")
- s += " title='%s'".printf (option.title);
-
- switch (option.type)
- {
- case Sane.ValueType.BOOL:
- s += " type=bool";
- break;
- case Sane.ValueType.INT:
- s += " type=int";
- break;
- case Sane.ValueType.FIXED:
- s += " type=fixed";
- break;
- case Sane.ValueType.STRING:
- s += " type=string";
- break;
- case Sane.ValueType.BUTTON:
- s += " type=button";
- break;
- case Sane.ValueType.GROUP:
- s += " type=group";
- break;
- default:
- s += " type=%d".printf (option.type);
- break;
- }
-
- s += " size=%d".printf ((int) option.size);
-
- switch (option.unit)
- {
- case Sane.Unit.NONE:
- break;
- case Sane.Unit.PIXEL:
- s += " unit=pixels";
- break;
- case Sane.Unit.BIT:
- s += " unit=bits";
- break;
- case Sane.Unit.MM:
- s += " unit=mm";
- break;
- case Sane.Unit.DPI:
- s += " unit=dpi";
- break;
- case Sane.Unit.PERCENT:
- s += " unit=percent";
- break;
- case Sane.Unit.MICROSECOND:
- s += " unit=microseconds";
- break;
- default:
- s += " unit=%d".printf (option.unit);
- break;
- }
-
- switch (option.constraint_type)
- {
- case Sane.ConstraintType.RANGE:
- if (option.range != null) {
- if (option.type == Sane.ValueType.FIXED)
- s += " min=%f, max=%f, quant=%d".printf (Sane.UNFIX (option.range.min), Sane.UNFIX (option.range.max), (int) option.range.quant);
- else
- s += " min=%d, max=%d, quant=%d".printf ((int) option.range.min, (int) option.range.max, (int) option.range.quant);
- }
- break;
- case Sane.ConstraintType.WORD_LIST:
- s += " values=[";
- for (var i = 0; i < option.word_list[0]; i++)
- {
- if (i != 0)
- s += ", ";
- if (option.type == Sane.ValueType.INT)
- s += "%d".printf ((int) option.word_list[i+1]);
- else
- s += "%f".printf (Sane.UNFIX (option.word_list[i+1]));
- }
- s += "]";
- break;
- case Sane.ConstraintType.STRING_LIST:
- s += " values=[";
- if (option.string_list != null) {
- for (var i = 0; option.string_list[i] != null; i++)
- {
- if (i != 0)
- s += ", ";
- s += "\"%s\"".printf (option.string_list[i]);
- }
- }
- s += "]";
- break;
- default:
- break;
- }
-
- var cap = option.cap;
- if (cap != 0)
- {
- var caps = "";
- if ((cap & Sane.Capability.SOFT_SELECT) != 0)
- {
- if (caps != "")
- caps += ",";
- caps += "soft-select";
- cap &= ~Sane.Capability.SOFT_SELECT;
- }
- if ((cap & Sane.Capability.HARD_SELECT) != 0)
- {
- if (caps != "")
- caps += ",";
- caps += "hard-select";
- cap &= ~Sane.Capability.HARD_SELECT;
- }
- if ((cap & Sane.Capability.SOFT_DETECT) != 0)
- {
- if (caps != "")
- caps += ",";
- caps += "soft-detect";
- cap &= ~Sane.Capability.SOFT_DETECT;
- }
- if ((cap & Sane.Capability.EMULATED) != 0)
- {
- if (caps != "")
- caps += ",";
- caps += "emulated";
- cap &= ~Sane.Capability.EMULATED;
- }
- if ((cap & Sane.Capability.AUTOMATIC) != 0)
- {
- if (caps != "")
- caps += ",";
- caps += "automatic";
- cap &= ~Sane.Capability.AUTOMATIC;
- }
- if ((cap & Sane.Capability.INACTIVE) != 0)
- {
- if (caps != "")
- caps += ",";
- caps += "inactive";
- cap &= ~Sane.Capability.INACTIVE;
- }
- if ((cap & Sane.Capability.ADVANCED) != 0)
- {
- if (caps != "")
- caps += ",";
- caps += "advanced";
- cap &= ~Sane.Capability.ADVANCED;
- }
- /* Unknown capabilities */
- if (cap != 0)
- {
- if (caps != "")
- caps += ",";
- caps += "%x".printf ((int) cap);
- }
- s += " cap=" + caps;
- }
-
- debug ("%s", s);
-
- if (option.desc != null)
- debug (" Description: %s", option.desc);
- }
-
- private static void authorization_cb (string resource, char[] username, char[] password)
- {
- scanner_object.notify_event (new NotifyRequestAuthorization (resource));
-
- var credentials = scanner_object.authorize_queue.pop ();
- for (var i = 0; credentials.username[i] != '\0' && i < Sane.MAX_USERNAME_LEN; i++)
- username[i] = credentials.username[i];
- for (var i = 0; credentials.password[i] != '\0' && i < Sane.MAX_PASSWORD_LEN; i++)
- password[i] = credentials.password[i];
- }
-
- public void authorize (string username, string password)
- {
- var credentials = new Credentials ();
- credentials.username = username;
- credentials.password = password;
- authorize_queue.push (credentials);
- }
-
- private void close_device ()
- {
- if (have_handle)
- {
- Sane.cancel (handle);
- debug ("sane_cancel ()");
-
- Sane.close (handle);
- debug ("sane_close ()");
- have_handle = false;
- options = null;
- }
-
- buffer = null;
- job_queue = null;
-
- set_scanning (false);
- }
-
- private void fail_scan (int error_code, string error_string)
- {
- close_device ();
- state = ScanState.IDLE;
- notify_event (new NotifyScanFailed (error_code, error_string));
- }
-
- private bool handle_requests ()
- {
- /* Redetect when idle */
- if (state == ScanState.IDLE && need_redetect)
- state = ScanState.REDETECT;
-
- /* Process all requests */
- int request_count = 0;
- while (true)
- {
- Request request;
- if ((state == ScanState.IDLE && request_count == 0) ||
- request_queue.length () > 0)
- request = request_queue.pop ();
- else
- return true;
-
- debug ("Processing request");
- request_count++;
-
- if (request is RequestStartScan)
- {
- var r = (RequestStartScan) request;
- job_queue.append (r.job);
- }
- else if (request is RequestCancel)
- {
- fail_scan (Sane.Status.CANCELLED, "Scan cancelled - do not report this error");
- }
- else if (request is RequestQuit)
- {
- close_device ();
- return false;
- }
- }
- }
-
- private void do_open ()
- {
- var job = (ScanJob) job_queue.data;
-
- line_count = 0;
- pass_number = 0;
- page_number = 0;
- notified_page = -1;
- option_index = 0;
-
- if (job.device == null && default_device != null)
- job.device = default_device;
-
- if (job.device == null)
- {
- warning ("No scan device available");
- fail_scan (0,
- /* Error displayed when no scanners to scan with */
- _("No scanners available. Please connect a scanner."));
- return;
- }
-
- /* See if we can use the already open device */
- if (have_handle)
- {
- if (current_device == job.device)
- {
- state = ScanState.GET_OPTION;
- return;
- }
-
- Sane.close (handle);
- debug ("sane_close ()");
- have_handle = false;
- }
-
- current_device = null;
-
- have_handle = false;
- options = new HashTable (str_hash, str_equal);
- var status = Sane.open (job.device, out handle);
- debug ("sane_open (\"%s\") -> %s", job.device, Sane.status_to_string (status));
-
- if (status != Sane.Status.GOOD)
- {
- warning ("Unable to open device: %s", Sane.strstatus (status));
- fail_scan (status,
- /* Error displayed when cannot connect to scanner */
- _("Unable to connect to scanner"));
- return;
- }
- have_handle = true;
-
- current_device = job.device;
- state = ScanState.GET_OPTION;
- }
-
- private void set_adf (ScanJob job, Sane.OptionDescriptor option, Sane.Int index)
- {
- string[] adf_sources =
- {
- "Automatic Document Feeder",
- Sane.I18N ("Automatic Document Feeder"),
- "ADF",
- "Automatic Document Feeder(centrally aligned)", /* Seen in the proprietary brother3 driver */
- "Automatic Document Feeder(center aligned)", /* Seen in Brother's proprietary brscan5 driver */
- "Automatic Document Feeder(left aligned)", /* Seen in the proprietary brother3 driver */
- "ADF Simplex" /* Samsung unified driver. LP: # 892915 */
- };
-
- string[] adf_front_sources =
- {
- "ADF Front",
- Sane.I18N ("ADF Front")
- };
-
- string[] adf_back_sources =
- {
- "ADF Back",
- Sane.I18N ("ADF Back")
- };
-
- string[] adf_duplex_sources =
- {
- "ADF Duplex",
- "Duplex ADF", /* Brother DS-720, #157 */
- Sane.I18N ("ADF Duplex"),
- "ADF Duplex - Long-Edge Binding", /* Samsung unified driver. LP: # 892915 */
- "ADF Duplex - Short-Edge Binding",
- "Duplex", /* HP duplex scan support. LP: #1353599 */
- "Automatic Document Feeder(centrally aligned,Duplex)", /* Brother duplex scan support. LP: #1343773 */
- "Automatic Document Feeder(left aligned,Duplex)"
- };
-
- if (job.side == ScanSide.FRONT)
- {
- if (!set_constrained_string_option (handle, option, index, adf_front_sources, null))
- if (!set_constrained_string_option (handle, option, index, adf_sources, null))
- warning ("Unable to set front ADF source, please file a bug");
- }
- else if (job.side == ScanSide.BACK)
- {
- if (!set_constrained_string_option (handle, option, index, adf_back_sources, null))
- if (!set_constrained_string_option (handle, option, index, adf_sources, null))
- warning ("Unable to set back ADF source, please file a bug");
- }
- else if (job.side == ScanSide.BOTH)
- {
- if (!set_constrained_string_option (handle, option, index, adf_duplex_sources, null))
- if (!set_constrained_string_option (handle, option, index, adf_sources, null))
- warning ("Unable to set duplex ADF source, please file a bug");
- }
- }
-
- private void do_get_option ()
- {
- var job = (ScanJob) job_queue.data;
-
- var option = Sane.get_option_descriptor (handle, option_index);
- debug ("sane_get_option_descriptor (%d)", (int) option_index);
- var index = option_index;
- option_index++;
-
- /* Options complete, apply settings */
- if (option == null)
- {
- /* Pick source */
- option = get_option_by_name (handle, Sane.NAME_SCAN_SOURCE, out index);
- if (option == null)
- {
- option = get_option_by_name (handle, "doc-source", out index); /* Samsung unified driver. LP: #892915 */
- }
- if (option != null)
- {
- string[] flatbed_sources =
- {
- "Auto",
- Sane.I18N ("Auto"),
- "Flatbed",
- Sane.I18N ("Flatbed"),
- "FlatBed",
- "Normal",
- Sane.I18N ("Normal"),
- "Document Table" /* Epson scanners, eg. ET-3760 */
- };
-
- switch (job.type)
- {
- case ScanType.SINGLE:
- case ScanType.BATCH:
- if (!set_default_option (handle, option, index))
- if (!set_constrained_string_option (handle, option, index, flatbed_sources, null))
- {
- warning ("Unable to set single page source, trying to set ADF instead");
- warning ("If Flatbed is existing and it is not set, please file a bug");
- set_adf (job, option, index);
- }
- break;
- case ScanType.ADF:
- set_adf (job, option, index);
- break;
- }
- }
-
- /* Scan mode (before resolution as it tends to affect that */
- option = get_option_by_name (handle, Sane.NAME_SCAN_MODE, out index);
- if (option != null)
- {
- /* The names of scan modes often used in drivers, as taken from the sane-backends source */
- string[] color_scan_modes =
- {
- Sane.VALUE_SCAN_MODE_COLOR,
- "Color",
- "24bit Color[Fast]", /* brother4 driver, Brother DCP-1622WE, #134 */
- "24bit Color", /* Seen in the proprietary brother3 driver */
- "24-bit Color", /* #161 Lexmark CX310dn */
- "24 bit Color", /* brscanads2200ads2700w */
- "Color - 16 Million Colors" /* Samsung unified driver. LP: 892915 */
- };
- string[] gray_scan_modes =
- {
- Sane.VALUE_SCAN_MODE_GRAY,
- "Gray",
- "Grayscale",
- Sane.I18N ("Grayscale"),
- "8-bit Grayscale", /* #161 Lexmark CX310dn */
- "True Gray", /* Seen in the proprietary brother3 driver */
- "Grayscale - 256 Levels" /* Samsung unified driver. LP: 892915 */
- };
- string[] lineart_scan_modes =
- {
- Sane.VALUE_SCAN_MODE_LINEART,
- "Lineart",
- "LineArt",
- Sane.I18N ("LineArt"),
- "Black & White",
- Sane.I18N ("Black & White"),
- "Binary", /* Epson PM-A820 */
- Sane.I18N ("Binary"),
- "Thresholded",
- Sane.VALUE_SCAN_MODE_GRAY,
- "Gray",
- "Grayscale",
- Sane.I18N ("Grayscale"),
- "True Gray", /* Seen in the proprietary brother3 driver */
- "1-bit Black & White", /* #161 Lexmark CX310dn */
- "Black and White - Line Art", /* Samsung unified driver. LP: 892915 */
- "Black and White - Halftone",
- "Monochrome" /* Epson */
- };
-
- switch (job.scan_mode)
- {
- case ScanMode.COLOR:
- if (!set_constrained_string_option (handle, option, index, color_scan_modes, null))
- warning ("Unable to set Color mode, please file a bug");
- break;
- case ScanMode.GRAY:
- if (!set_constrained_string_option (handle, option, index, gray_scan_modes, null))
- warning ("Unable to set Gray mode, please file a bug");
- break;
- case ScanMode.LINEART:
- if (!set_constrained_string_option (handle, option, index, lineart_scan_modes, null))
- warning ("Unable to set Lineart mode, please file a bug");
- break;
- default:
- break;
- }
- }
-
- /* Duplex */
- option = get_option_by_name (handle, "duplex", out index);
- if (option == null) /* #161 Lexmark CX310dn Duplex */
- option = get_option_by_name (handle, "scan-both-sides", out index);
- if (option != null)
- {
- if (option.type == Sane.ValueType.BOOL)
- set_bool_option (handle, option, index, job.side == ScanSide.BOTH, null);
- }
-
- /* Non-standard Epson GT-S50 ADF options */
- option = get_option_by_name (handle, "adf-mode", out index);
-
- /* Support Canon DR-C240 ADF_BOTH options */
- if (option == null)
- option = get_option_by_name (handle, "ScanMode", out index);
- if (option != null)
- {
- string[] adf_simplex_modes =
- {
- "Simplex"
- };
- string[] adf_duplex_modes =
- {
- "Duplex"
- };
- if (job.side == ScanSide.BOTH)
- set_constrained_string_option (handle, option, index, adf_duplex_modes, null);
- else
- set_constrained_string_option (handle, option, index, adf_simplex_modes, null);
- }
- option = get_option_by_name (handle, "adf-auto-scan", out index);
- if (option != null)
- {
- if (option.type == Sane.ValueType.BOOL)
- set_bool_option (handle, option, index, true, null);
- }
-
- /* Multi-page options */
- option = get_option_by_name (handle, "batch-scan", out index);
- if (option != null)
- {
- if (option.type == Sane.ValueType.BOOL)
- set_bool_option (handle, option, index, (job.type != ScanType.SINGLE) && (job.type != ScanType.BATCH), null);
- }
-
- /* Set resolution and bit depth */
- /* Epson may have separate resolution settings for x and y axes, which is preferable options to set */
- option = get_option_by_name (handle, Sane.NAME_SCAN_X_RESOLUTION, out index);
- if (option != null && (0 != (option.cap & Sane.Capability.SOFT_SELECT))) // L4160 has non-selectable separate options
- {
- set_fixed_or_int_option (handle, option, index, job.dpi, out job.dpi);
- option = get_option_by_name (handle, Sane.NAME_SCAN_Y_RESOLUTION, out index);
- }
- else
- option = get_option_by_name (handle, Sane.NAME_SCAN_RESOLUTION, out index);
- if (option == null) /* #161 Lexmark CX310dn Duplex */
- option = get_option_by_name (handle, "scan-resolution", out index);
- if (option != null)
- {
- set_fixed_or_int_option (handle, option, index, job.dpi, out job.dpi);
- option = get_option_by_name (handle, Sane.NAME_BIT_DEPTH, out index);
- if (option != null)
- {
- if (job.depth > 0)
- set_int_option (handle, option, index, job.depth, null);
- }
- }
-
- /* Set scan area */
- option = get_option_by_name (handle, Sane.NAME_SCAN_BR_X, out index);
- if (option != null)
- {
- if (job.page_width > 0)
- set_fixed_or_int_option (handle, option, index, convert_page_size (option, job.page_width, job.dpi), null);
- else
- set_option_to_max (handle, option, index);
- }
- option = get_option_by_name (handle, Sane.NAME_SCAN_BR_Y, out index);
- if (option != null)
- {
- if (job.page_height > 0)
- set_fixed_or_int_option (handle, option, index, convert_page_size (option, job.page_height, job.dpi), null);
- else
- set_option_to_max (handle, option, index);
- }
- if (job.page_width == 0)
- {
- /* #90 Fix automatic mode for Epson scanners */
- option = get_option_by_name (handle, "scan-area", out index);
- if (option != null)
- set_string_option (handle, option, index, "Maximum", null);
-
- /* #264 Enable automatic document size for Brother scanners */
- option = get_option_by_name (handle, "AutoDocumentSize", out index);
- if (option != null)
- set_bool_option (handle, option, index, true, null);
- }
- /* Set page size */
- option = get_option_by_name (handle, Sane.NAME_PAGE_WIDTH, out index);
- if (option != null && job.page_width > 0.0)
- set_fixed_or_int_option (handle, option, index, convert_page_size (option, job.page_width, job.dpi), null);
- option = get_option_by_name (handle, Sane.NAME_PAGE_HEIGHT, out index);
- if (option != null && job.page_height > 0.0)
- set_fixed_or_int_option (handle, option, index, convert_page_size (option, job.page_height, job.dpi), null);
-
- option = get_option_by_name (handle, Sane.NAME_BRIGHTNESS, out index);
- if (option != null)
- {
- if (option.type == Sane.ValueType.FIXED)
- {
- var brightness = scale_fixed (-100, 100, option, job.brightness);
- set_fixed_option (handle, option, index, brightness, null);
- }
- else if (option.type == Sane.ValueType.INT)
- {
- var brightness = scale_int (-100, 100, option, job.brightness);
- set_int_option (handle, option, index, brightness, null);
- }
- else
- warning ("Unable to set brightness, please file a bug");
- }
- option = get_option_by_name (handle, Sane.NAME_CONTRAST, out index);
- if (option != null)
- {
- if (option.type == Sane.ValueType.FIXED)
- {
- var contrast = scale_fixed (-100, 100, option, job.contrast);
- set_fixed_option (handle, option, index, contrast, null);
- }
- else if (option.type == Sane.ValueType.INT)
- {
- var contrast = scale_int (-100, 100, option, job.contrast);
- set_int_option (handle, option, index, contrast, null);
- }
- else
- warning ("Unable to set contrast, please file a bug");
- }
-
- /* Test scanner options (hoping will not effect other scanners...) */
- if (current_device == "test")
- {
- option = get_option_by_name (handle, "hand-scanner", out index);
- if (option != null)
- set_bool_option (handle, option, index, false, null);
- option = get_option_by_name (handle, "three-pass", out index);
- if (option != null)
- set_bool_option (handle, option, index, false, null);
- option = get_option_by_name (handle, "test-picture", out index);
- if (option != null)
- set_string_option (handle, option, index, "Color pattern", null);
- option = get_option_by_name (handle, "read-delay", out index);
- if (option != null)
- set_bool_option (handle, option, index, true, null);
- option = get_option_by_name (handle, "read-delay-duration", out index);
- if (option != null)
- set_int_option (handle, option, index, 200000, null);
- }
-
- state = ScanState.START;
- return;
- }
-
- log_option (index, option);
-
- /* Ignore groups */
- if (option.type == Sane.ValueType.GROUP)
- return;
-
- /* Some options are unnamed (e.g. Option 0) */
- if (option.name == null)
- return;
-
- options.insert (option.name, (int) index);
- }
-
- private double convert_page_size (Sane.OptionDescriptor option, double size, double dpi)
- {
- if (option.unit == Sane.Unit.PIXEL)
- return dpi * size / 254.0;
- else if (option.unit == Sane.Unit.MM)
- return size / 10.0;
- else
- {
- warning ("Unable to set unsupported unit type");
- return 0.0f;
- }
- }
-
- private Sane.OptionDescriptor? get_option_by_name (Sane.Handle handle, string name, out int index)
- {
- index = options.lookup (name);
- if (index == 0)
- return null;
-
- var option_descriptor = Sane.get_option_descriptor (handle, index);
- /*
- The Sane.Capability.INACTIVE capability indicates that
- the option is not currently active (e.g., because it's meaningful
- only if another option is set to some other value).
- */
- if ((option_descriptor.cap & Sane.Capability.INACTIVE) != 0)
- {
- warning ("The option %s (%d) is inactive and can't be set, please file a bug", name, index);
- return null;
- }
- return option_descriptor;
- }
-
- private void do_complete_document ()
- {
- Sane.cancel (handle);
- debug ("sane_cancel ()");
-
- job_queue.remove_link (job_queue);
-
- state = ScanState.IDLE;
-
- /* Continue onto the next job */
- if (job_queue != null)
- {
- state = ScanState.OPEN;
- return;
- }
-
- /* Trigger timeout to close */
- // TODO
-
- notify_event (new NotifyDocumentDone ());
- set_scanning (false);
- }
-
- private void do_start ()
- {
- Sane.Status status;
-
- notify_event (new NotifyExpectPage ());
-
- status = Sane.start (handle);
- debug ("sane_start (page=%d, pass=%d) -> %s", page_number, pass_number, Sane.status_to_string (status));
- if (status == Sane.Status.GOOD)
- state = ScanState.GET_PARAMETERS;
- else if (status == Sane.Status.NO_DOCS)
- {
- var job = (ScanJob) job_queue.data;
- if (job.type == ScanType.BATCH)
- {
- /* In BATCH mode ("Multiple Pages from Flatbed") some scanners return
- NO_DOCS after the first page is scanned. Try cancelling and restarting
- to continue scanning more pages. */
- debug ("sane_start received NO_DOCS in BATCH mode -> cancelling and restarting");
-
- Sane.cancel(handle);
-
- state = ScanState.START;
- return;
- }
- else
- {
- do_complete_document ();
- if (page_number == 0)
- fail_scan (status,
- /* Error displayed when no documents at the start of scanning */
- _("Document feeder empty"));
- }
- }
- else if (status == Sane.Status.NO_MEM)
- {
- fail_scan (status,
- /* Out of memory error message with help instruction.
- Message written in Pango text markup language,
- A carriage return makes a line break, tag makes a monospace font */
- _("Insufficient memory to perform scan.\n" +
- "Try to decrease Resolution or Page Size in Preferences menu. " +
- "For some scanners when scanning in high resolution, the scan size is restricted."));
- }
- else if (status == Sane.Status.DEVICE_BUSY)
- {
- /* If device is busy don't interrupt, but keep waiting for scanner */
- }
- else
- {
- warning ("Unable to start device: %s", Sane.strstatus (status));
- fail_scan (status,
- /* Error display when unable to start scan */
- _("Unable to start scan"));
- }
- }
-
- private void do_get_parameters ()
- {
- var status = Sane.get_parameters (handle, out parameters);
- debug ("sane_get_parameters () -> %s", Sane.status_to_string (status));
- if (status != Sane.Status.GOOD)
- {
- warning ("Unable to get device parameters: %s", Sane.strstatus (status));
- fail_scan (status,
- /* Error displayed when communication with scanner broken */
- _("Error communicating with scanner"));
- return;
- }
-
- var job = (ScanJob) job_queue.data;
-
- debug ("Parameters: format=%s last_frame=%s bytes_per_line=%d pixels_per_line=%d lines=%d depth=%d",
- Sane.frame_to_string (parameters.format),
- parameters.last_frame ? "SANE_TRUE" : "SANE_FALSE",
- parameters.bytes_per_line,
- parameters.pixels_per_line,
- parameters.lines,
- parameters.depth);
-
- var info = new ScanPageInfo ();
- info.width = parameters.pixels_per_line;
- info.height = parameters.lines;
- info.depth = parameters.depth;
- /* Reduce bit depth if requested lower than received */
- // FIXME: This a hack and only works on 8 bit gray to 2 bit gray
- if (parameters.depth == 8 && parameters.format == Sane.Frame.GRAY && job.depth == 2 && job.scan_mode == ScanMode.GRAY)
- info.depth = job.depth;
- info.n_channels = parameters.format == Sane.Frame.GRAY ? 1 : 3;
- info.dpi = job.dpi; // FIXME: This is the requested DPI, not the actual DPI
- info.device = current_device;
-
- if (page_number != notified_page)
- {
- notify_event (new NotifyGotPageInfo (job.id, info));
- notified_page = page_number;
- }
-
- /* Prepare for read */
- var buffer_size = parameters.bytes_per_line + 1; /* Use +1 so buffer is not resized if driver returns one line per read */
- buffer = new uchar[buffer_size];
- n_used = 0;
- line_count = 0;
- pass_number = 0;
- state = ScanState.READ;
- }
-
- private void do_complete_page ()
- {
- var job = (ScanJob) job_queue.data;
-
- notify_event (new NotifyPageDone (job.id));
-
- /* If multi-pass then scan another page */
- if (!parameters.last_frame)
- {
- pass_number++;
- state = ScanState.START;
- return;
- }
-
- /* Go back for another page */
- if (job.type != ScanType.SINGLE)
- {
- if (job.type == ScanType.BATCH)
- Thread.usleep (job.page_delay * 1000);
-
- page_number++;
- pass_number = 0;
- state = ScanState.START;
- return;
- }
-
- do_complete_document ();
- }
-
- private void do_read ()
- {
- var job = (ScanJob) job_queue.data;
-
- /* Read as many bytes as we expect */
- var n_to_read = buffer.length - n_used;
-
- Sane.Int n_read;
- var b = (uchar *) buffer;
- var status = Sane.read (handle, (uint8[]) (b + n_used), (Sane.Int) n_to_read, out n_read);
- debug ("sane_read (%d) -> (%s, %d)", n_to_read, Sane.status_to_string (status), (int) n_read);
-
- /* Completed read */
- if (status == Sane.Status.EOF)
- {
- if (parameters.lines > 0 && line_count != parameters.lines)
- warning ("Scan completed with %d lines, expected %d lines", line_count, parameters.lines);
- if (n_used > 0)
- warning ("Scan complete with %d bytes of unused data", n_used);
- do_complete_page ();
- return;
- }
-
- /* Some ADF scanners only return NO_DOCS after a read */
- if (status == Sane.Status.NO_DOCS)
- {
- do_complete_document ();
- if (page_number == 0)
- fail_scan (status,
- /* Error displayed when no documents at the start of scanning */
- _("Document feeder empty"));
- return;
- }
-
- /* Communication error */
- if (status != Sane.Status.GOOD)
- {
- warning ("Unable to read frame from device: %s", Sane.strstatus (status));
- fail_scan (status,
- /* Error displayed when communication with scanner broken */
- _("Error communicating with scanner"));
- return;
- }
-
- bool full_read = false;
- if (n_used == 0 && n_read == buffer.length)
- full_read = true;
- n_used += (int) n_read;
-
- /* Feed out lines */
- if (n_used >= parameters.bytes_per_line)
- {
- var line = new ScanLine ();
- switch (parameters.format)
- {
- case Sane.Frame.GRAY:
- line.channel = 0;
- break;
- case Sane.Frame.RGB:
- line.channel = -1;
- break;
- case Sane.Frame.RED:
- line.channel = 0;
- break;
- case Sane.Frame.GREEN:
- line.channel = 1;
- break;
- case Sane.Frame.BLUE:
- line.channel = 2;
- break;
- }
- line.width = parameters.pixels_per_line;
- line.depth = parameters.depth;
- line.data = (owned) buffer;
- line.data_length = parameters.bytes_per_line;
- line.number = line_count;
- line.n_lines = n_used / line.data_length;
-
- line_count += line.n_lines;
-
- /* Increase buffer size if did full read */
- var buffer_size = line.data.length;
- if (full_read)
- buffer_size += parameters.bytes_per_line;
-
- buffer = new uchar[buffer_size];
- var n_remaining = n_used - (line.n_lines * line.data_length);
- n_used = 0;
- for (var i = 0; i < n_remaining; i++)
- {
- buffer[i] = line.data[i + (line.n_lines * line.data_length)];
- n_used++;
- }
-
- /* Reduce bit depth if requested lower than received */
- // FIXME: This a hack and only works on 8 bit gray to 2 bit gray
- if (parameters.depth == 8 && parameters.format == Sane.Frame.GRAY &&
- job.depth == 2 && job.scan_mode == ScanMode.GRAY)
- {
- uchar block = 0;
- var write_offset = 0;
- var block_shift = 6;
- for (var i = 0; i < line.n_lines; i++)
- {
- var offset = i * line.data_length;
- for (var x = 0; x < line.width; x++)
- {
- var p = line.data[offset + x];
-
- uchar sample;
- if (p >= 192)
- sample = 3;
- else if (p >= 128)
- sample = 2;
- else if (p >= 64)
- sample = 1;
- else
- sample = 0;
-
- block |= sample << block_shift;
- if (block_shift == 0)
- {
- line.data[write_offset] = block;
- write_offset++;
- block = 0;
- block_shift = 6;
- }
- else
- block_shift -= 2;
- }
-
- /* Finish each line on a byte boundary */
- if (block_shift != 6)
- {
- line.data[write_offset] = block;
- write_offset++;
- block = 0;
- block_shift = 6;
- }
- }
-
- line.data_length = (line.width * 2 + 7) / 8;
- }
-
- notify_event (new NotifyGotLine (job.id, line));
- }
- }
-
- private void* scan_thread ()
- {
- state = ScanState.IDLE;
-
- Sane.Int version_code;
- var status = Sane.init (out version_code, authorization_cb);
- debug ("sane_init () -> %s", Sane.status_to_string (status));
- if (status != Sane.Status.GOOD)
- {
- warning ("Unable to initialize SANE backend: %s", Sane.strstatus(status));
- return null;
- }
- debug ("SANE version %d.%d.%d",
- Sane.VERSION_MAJOR(version_code),
- Sane.VERSION_MINOR(version_code),
- Sane.VERSION_BUILD(version_code));
-
- /* Scan for devices on first start */
- redetect ();
-
- while (handle_requests ())
- {
- switch (state)
- {
- case ScanState.IDLE:
- if (job_queue != null)
- {
- set_scanning (true);
- state = ScanState.OPEN;
- }
- break;
- case ScanState.REDETECT:
- do_redetect ();
- break;
- case ScanState.OPEN:
- do_open ();
- break;
- case ScanState.GET_OPTION:
- do_get_option ();
- break;
- case ScanState.START:
- do_start ();
- break;
- case ScanState.GET_PARAMETERS:
- do_get_parameters ();
- break;
- case ScanState.READ:
- do_read ();
- break;
- }
- }
-
- return null;
- }
-
- public void start ()
- {
- try
- {
- thread = new Thread.try ("scan-thread", scan_thread);
- }
- catch (Error e)
- {
- critical ("Unable to create thread: %s", e.message);
- }
- }
-
- public void redetect ()
- {
- if (need_redetect)
- return;
- need_redetect = true;
-
- debug ("Requesting redetection of scan devices");
-
- request_queue.push (new RequestRedetect ());
- }
-
- public bool is_scanning ()
- {
- return scanning;
- }
-
- private string get_scan_mode_string (ScanMode mode)
- {
- switch (mode)
- {
- case ScanMode.DEFAULT:
- return "ScanMode.DEFAULT";
- case ScanMode.COLOR:
- return "ScanMode.COLOR";
- case ScanMode.GRAY:
- return "ScanMode.GRAY";
- case ScanMode.LINEART:
- return "ScanMode.LINEART";
- default:
- return "%d".printf (mode);
- }
- }
-
- public static string type_to_string (ScanType type)
- {
- switch (type)
- {
- case ScanType.SINGLE:
- return "single";
- case ScanType.BATCH:
- return "batch";
- case ScanType.ADF:
- return "adf";
- default:
- return "%d".printf (type);
- }
- }
-
- public static ScanType type_from_string (string type)
- {
- switch (type)
- {
- case "single":
- return ScanType.SINGLE;
- case "batch":
- return ScanType.BATCH;
- case "adf":
- return ScanType.ADF;
- default:
- warning ("Unknown ScanType: %s. Please report this error.", type);
- return ScanType.SINGLE;
- }
- }
-
- public static string side_to_string (ScanSide side)
- {
- switch (side)
- {
- case ScanSide.FRONT:
- return "front";
- case ScanSide.BACK:
- return "back";
- case ScanSide.BOTH:
- return "both";
- default:
- return "%d".printf (side);
- }
- }
-
- public static ScanSide side_from_string (string side)
- {
- switch (side)
- {
- case "front":
- return ScanSide.FRONT;
- case "back":
- return ScanSide.BACK;
- case "both":
- return ScanSide.BOTH;
- default:
- warning ("Unknown ScanSide: %s. Please report this error.", side);
- return ScanSide.FRONT;
- }
- }
-
- public void scan (string? device, ScanOptions options)
- {
- debug ("Scanner.scan (\"%s\", dpi=%d, scan_mode=%s, depth=%d, type=%s, side=%s, paper_width=%d, paper_height=%d, brightness=%d, contrast=%d, delay=%dms)",
- device != null ? device : "(null)", options.dpi, get_scan_mode_string (options.scan_mode), options.depth,
- type_to_string (options.type), side_to_string (options.side),
- options.paper_width, options.paper_height,
- options.brightness, options.contrast, options.page_delay);
- var request = new RequestStartScan ();
- request.job = new ScanJob ();
- request.job.id = job_id++;
- request.job.device = device;
- request.job.dpi = options.dpi;
- request.job.scan_mode = options.scan_mode;
- request.job.depth = options.depth;
- request.job.type = options.type;
- request.job.side = options.side;
- request.job.page_width = options.paper_width;
- request.job.page_height = options.paper_height;
- request.job.brightness = options.brightness;
- request.job.contrast = options.contrast;
- request.job.page_delay = options.page_delay;
- request_queue.push (request);
- }
-
- public void cancel ()
- {
- first_job_id = job_id;
- request_queue.push (new RequestCancel ());
- }
-
- public void free ()
- {
- debug ("Stopping scan thread");
-
- request_queue.push (new RequestQuit ());
-
- if (thread != null)
- {
- thread.join ();
- thread = null;
- }
-
- Sane.exit ();
- debug ("sane_exit ()");
- }
-}
+/*
+ * 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.
+ */
+
+/* TODO: Could indicate the start of the next page immediately after the last page is received (i.e. before the sane_cancel()) */
+
+public class ScanDevice : Object
+{
+ public string name;
+ public string label;
+}
+
+public class ScanPageInfo : Object
+{
+ /* Width, height in pixels */
+ public int width;
+ public int height;
+
+ /* Page size */
+ public string page_size;
+
+ /* Bit depth */
+ public int depth;
+
+ /* Number of colour channels */
+ public int n_channels;
+
+ /* Resolution */
+ public double dpi;
+
+ /* The device this page came from */
+ public string device;
+}
+
+public class ScanLine : Object
+{
+ /* Line number */
+ public int number;
+
+ /* Number of lines in this packet */
+ public int n_lines;
+
+ /* Width in pixels and format */
+ public int width;
+ public int depth;
+
+ /* Channel for this line or -1 for all channels */
+ public int channel;
+
+ /* Raw line data */
+ public uchar[] data;
+ public int data_length;
+}
+
+public enum ScanMode
+{
+ DEFAULT,
+ COLOR,
+ GRAY,
+ LINEART
+}
+
+public enum ScanType
+{
+ SINGLE,
+ ADF,
+ BATCH
+}
+
+public enum ScanSide
+{
+ FRONT,
+ BACK,
+ BOTH
+}
+
+public enum PageOrientation
+{
+ TOP,
+ BOTTOM,
+ LEFT,
+ RIGHT
+}
+
+public class ScanOptions : Object
+{
+ public int dpi;
+ public ScanMode scan_mode;
+ public int depth;
+ public ScanType type;
+ public ScanSide side;
+ public int paper_width;
+ public int paper_height;
+ public string page_size;
+ public int brightness;
+ public int contrast;
+ public int page_delay;
+}
+
+/* Capabilities read directly from a device via libsane. */
+public class ScanDeviceCapabilities : Object
+{
+ /* Supported resolutions in dpi, sorted ascending. Empty if unknown. */
+ public int[] resolutions = {};
+
+ /* Maximum scannable page width in tenths of a millimetre (0 if unknown). */
+ public int max_width = 0;
+
+ /* Maximum scannable page height in tenths of a millimetre (0 if unknown). */
+ public int max_height = 0;
+}
+
+private class ScanJob : Object
+{
+ public int id;
+ public string device;
+ public double dpi;
+ public ScanMode scan_mode;
+ public int depth;
+ public ScanType type;
+ public ScanSide side;
+ public int page_width;
+ public int page_height;
+ public string page_size;
+ public int brightness;
+ public int contrast;
+ public int page_delay;
+}
+
+private class Request {}
+
+private class RequestRedetect : Request {}
+
+private class RequestGetCapabilities : Request
+{
+ public RequestGetCapabilities (string device) { this.device = device; }
+ public string device;
+}
+
+private class RequestCancel : Request {}
+
+private class RequestStartScan : Request
+{
+ public ScanJob job;
+}
+
+private class RequestQuit : Request {}
+
+private class Credentials
+{
+ public string username;
+ public string password;
+}
+
+private enum ScanState
+{
+ IDLE = 0,
+ REDETECT,
+ OPEN,
+ GET_OPTION,
+ START,
+ GET_PARAMETERS,
+ READ
+}
+
+private class Notify
+{
+ public virtual void run (Scanner scanner) {}
+}
+
+private class NotifyScanningChanged : Notify
+{
+ public override void run (Scanner scanner) { scanner.scanning_changed (); }
+}
+
+private class NotifyUpdateDevices : Notify
+{
+ public NotifyUpdateDevices (owned List devices) { this.devices = (owned) devices; }
+ private List devices;
+ public override void run (Scanner scanner) { scanner.update_devices (devices); }
+}
+
+private class NotifyRequestAuthorization : Notify
+{
+ public NotifyRequestAuthorization (string resource) { this.resource = resource; }
+ private string resource;
+ public override void run (Scanner scanner) { scanner.request_authorization (resource); }
+}
+
+private class NotifyDeviceCapabilities : Notify
+{
+ public NotifyDeviceCapabilities (string device, ScanDeviceCapabilities capabilities) { this.device = device; this.capabilities = capabilities; }
+ private string device;
+ private ScanDeviceCapabilities capabilities;
+ public override void run (Scanner scanner) { scanner.device_capabilities (device, capabilities); }
+}
+
+private class NotifyScanFailed : Notify
+{
+ public NotifyScanFailed (int error_code, string error_string) { this.error_code = error_code; this.error_string = error_string; }
+ private int error_code;
+ private string error_string;
+ public override void run (Scanner scanner) { scanner.scan_failed (error_code, error_string); }
+}
+
+private class NotifyDocumentDone : Notify
+{
+ public override void run (Scanner scanner) { scanner.document_done (); }
+}
+
+private class NotifyExpectPage : Notify
+{
+ public override void run (Scanner scanner) { scanner.expect_page (); }
+}
+
+private class NotifyGotPageInfo : Notify
+{
+ public NotifyGotPageInfo (int job_id, ScanPageInfo info) { this.job_id = job_id; this.info = info; }
+ private int job_id;
+ private ScanPageInfo info;
+ public override void run (Scanner scanner)
+ {
+ if (job_id >= scanner.first_job_id && job_id < scanner.job_id)
+ scanner.got_page_info (info);
+ }
+}
+
+private class NotifyPageDone : Notify
+{
+ public NotifyPageDone (int job_id) { this.job_id = job_id; }
+ private int job_id;
+ public override void run (Scanner scanner)
+ {
+ if (job_id >= scanner.first_job_id && job_id < scanner.job_id)
+ scanner.page_done ();
+ }
+}
+
+private class NotifyGotLine : Notify
+{
+ public NotifyGotLine (int job_id, ScanLine line) { this.job_id = job_id; this.line = line; }
+ private int job_id;
+ private ScanLine line;
+ public override void run (Scanner scanner)
+ {
+ if (job_id >= scanner.first_job_id && job_id < scanner.job_id)
+ scanner.got_line (line);
+ }
+}
+
+public class Scanner : Object
+{
+ /* Singleton object */
+ private static Scanner scanner_object = null;
+
+ /* Thread communicating with SANE */
+ private Thread thread;
+
+ /* Queue of requests from main thread */
+ private AsyncQueue request_queue;
+
+ /* Queue of events to notify in main queue */
+ private AsyncQueue notify_queue;
+
+ /* Queue of responses to authorization requests */
+ private AsyncQueue authorize_queue;
+
+ /* ID for the current job */
+ public int first_job_id;
+ public int job_id;
+
+ private string? default_device;
+
+ private ScanState state;
+ private bool need_redetect;
+
+ private List job_queue;
+
+ /* Handle to SANE device */
+ private Sane.Handle handle;
+ private bool have_handle;
+ private string? current_device;
+
+ private Sane.Parameters parameters;
+
+ /* Last option read */
+ private Sane.Int option_index;
+
+ /* Table of options */
+ private HashTable options;
+
+ /* Buffer for received line */
+ private uchar[] buffer;
+ private int n_used;
+
+ //private int bytes_remaining;
+ private int line_count;
+ private int pass_number;
+ private int page_number;
+ private int notified_page;
+
+ private bool scanning;
+
+ public signal void update_devices (List devices);
+ public signal void device_capabilities (string device, ScanDeviceCapabilities capabilities);
+ public signal void request_authorization (string resource);
+ public signal void expect_page ();
+ public signal void got_page_info (ScanPageInfo info);
+ public signal void got_line (ScanLine line);
+ public signal void scan_failed (int error_code, string error_string);
+ public signal void page_done ();
+ public signal void document_done ();
+ public signal void scanning_changed ();
+ public signal void display_page_delay (int delay);
+
+ private Scanner ()
+ {
+ request_queue = new AsyncQueue ();
+ notify_queue = new AsyncQueue ();
+ authorize_queue = new AsyncQueue ();
+ }
+
+ public static Scanner get_instance ()
+ {
+ if (scanner_object == null)
+ scanner_object = new Scanner ();
+ return scanner_object;
+ }
+
+ public int get_page_number () { return page_number; }
+
+ private bool notify_idle_cb ()
+ {
+ var notification = notify_queue.pop ();
+ notification.run (this);
+ return false;
+ }
+
+ private void notify_event (Notify notification)
+ {
+ notify_queue.push (notification);
+ Idle.add (notify_idle_cb);
+ }
+
+ private void set_scanning (bool is_scanning)
+ {
+ if ((scanning && !is_scanning) || (!scanning && is_scanning))
+ {
+ scanning = is_scanning;
+ notify_event (new NotifyScanningChanged ());
+ }
+ }
+
+ private static int get_device_weight (string device)
+ {
+ /* NOTE: This is using trends in the naming of SANE devices, SANE should be able to provide this information better */
+
+ /* Use webcams as a last resort */
+ if (device.has_prefix ("vfl:"))
+ return 2;
+
+ /* Use locally connected devices first */
+ if (device.contains ("usb"))
+ return 0;
+
+ return 1;
+ }
+
+ private static int compare_devices (ScanDevice device1, ScanDevice device2)
+ {
+ /* TODO: Should do some fuzzy matching on the last selected device and set that to the default */
+
+ var weight1 = get_device_weight (device1.name);
+ var weight2 = get_device_weight (device2.name);
+ if (weight1 != weight2)
+ return weight1 - weight2;
+
+ return strcmp (device1.label, device2.label);
+ }
+
+ private void do_redetect ()
+ {
+ unowned Sane.Device[] device_list = null;
+ var status = Sane.get_devices (out device_list, false);
+ debug ("sane_get_devices () -> %s", Sane.status_to_string (status));
+ if (status != Sane.Status.GOOD)
+ {
+ warning ("Unable to get SANE devices: %s", Sane.strstatus(status));
+ need_redetect = false;
+ state = ScanState.IDLE;
+ return;
+ }
+
+ /* Determine the number of each model to additionally display the name if the model names are the same. */
+ var seen = new HashTable (str_hash, str_equal);
+ for (var i = 0; device_list[i] != null; i++)
+ {
+ if (seen.contains(device_list[i].model))
+ seen.set(device_list[i].model, seen.get(device_list[i].model) + 1);
+ else
+ seen.set(device_list[i].model, 1);
+ }
+
+ var devices = new List ();
+ for (var i = 0; device_list[i] != null; i++)
+ {
+ /* Virtual devices tend to not be scanners. Skip them. */
+ if (device_list[i].type == "virtual device")
+ continue;
+
+ debug ("Device: name=\"%s\" vendor=\"%s\" model=\"%s\" type=\"%s\"",
+ device_list[i].name, device_list[i].vendor, device_list[i].model, device_list[i].type);
+
+ var scan_device = new ScanDevice ();
+ scan_device.name = device_list[i].name;
+
+ /* Abbreviate HP as it is a long string and does not match what is on the physical scanner */
+ var vendor = device_list[i].vendor;
+ if (vendor == "Hewlett-Packard")
+ vendor = "HP";
+
+ /* Don't repeat vendor name */
+ if (device_list[i].model.down().has_prefix (vendor.down()))
+ scan_device.label = device_list[i].model;
+ else
+ scan_device.label = "%s %s".printf (vendor, device_list[i].model);
+
+ /* Replace underscores in name */
+ scan_device.label = scan_device.label.replace ("_", " ");
+
+ /* Additionally add the device name to the label if there are several identical models. */
+ if (seen.get(device_list[i].model) > 1)
+ scan_device.label = "%s on %s".printf (scan_device.label, device_list[i].name);
+
+
+ devices.append (scan_device);
+ }
+
+ /* Sort devices by priority */
+ devices.sort (compare_devices);
+
+ need_redetect = false;
+ state = ScanState.IDLE;
+
+ if (devices != null)
+ {
+ var device = devices.nth_data (0);
+ default_device = device.name;
+ }
+ else
+ default_device = null;
+
+ notify_event (new NotifyUpdateDevices ((owned) devices));
+ }
+
+ private double scale_fixed (int source_min, int source_max, Sane.OptionDescriptor option, int value)
+ {
+ var v = (double) value;
+
+ return_val_if_fail (option.type == Sane.ValueType.FIXED, value);
+ if (option.constraint_type == Sane.ConstraintType.RANGE && option.range.max != option.range.min)
+ {
+ v -= (double) source_min;
+ v *= Sane.UNFIX (option.range.max) - Sane.UNFIX (option.range.min);
+ v /= (double) (source_max - source_min);
+ v += Sane.UNFIX (option.range.min);
+ debug ("scale_fixed: scaling %d [min: %d, max: %d] to %f [min: %f, max: %f]",
+ value, source_min, source_max, v, Sane.UNFIX (option.range.min), Sane.UNFIX (option.range.max));
+ }
+
+ return v;
+ }
+
+ private int scale_int (int source_min, int source_max, Sane.OptionDescriptor option, int value)
+ {
+ var v = value;
+
+ return_val_if_fail (option.type == Sane.ValueType.INT, value);
+
+ if (option.constraint_type == Sane.ConstraintType.RANGE && option.range.max != option.range.min)
+ {
+ v -= source_min;
+ v *= (int) (option.range.max - option.range.min);
+ v /= (source_max - source_min);
+ v += (int) option.range.min;
+ debug ("scale_int: scaling %d [min: %d, max: %d] to %d [min: %d, max: %d]",
+ value, source_min, source_max, v, (int) option.range.min, (int) option.range.max);
+ }
+
+ return v;
+ }
+
+ private bool set_default_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index)
+ {
+ /* Check if supports automatic option */
+ if ((option.cap & Sane.Capability.AUTOMATIC) == 0)
+ return false;
+
+ var status = Sane.control_option (handle, option_index, Sane.Action.SET_AUTO, null, null);
+ debug ("sane_control_option (%d, SANE_ACTION_SET_AUTO, %s=auto) -> %s", (int) option_index, option.name, Sane.status_to_string (status));
+ if (status != Sane.Status.GOOD)
+ warning ("Error setting default option %s: %s", option.name, Sane.strstatus(status));
+
+ return status == Sane.Status.GOOD;
+ }
+
+ private void set_bool_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, bool value, out bool result)
+ {
+ return_if_fail (option.type == Sane.ValueType.BOOL);
+
+ Sane.Bool v = (Sane.Bool) value;
+ var status = Sane.control_option (handle, option_index, Sane.Action.SET_VALUE, &v, null);
+ result = (bool) v;
+ debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=%s) -> (%s, %s)", (int) option_index, option.name, value ? "SANE_TRUE" : "SANE_FALSE", Sane.status_to_string (status), result ? "SANE_TRUE" : "SANE_FALSE");
+ }
+
+ private void set_int_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, int value, out int result)
+ {
+ return_if_fail (option.type == Sane.ValueType.INT);
+
+ Sane.Int v = (Sane.Int) value;
+ if (option.constraint_type == Sane.ConstraintType.RANGE)
+ {
+ if (option.range.quant != 0)
+ v *= option.range.quant;
+ if (v < option.range.min)
+ v = option.range.min;
+ if (v > option.range.max)
+ v = option.range.max;
+ }
+ else if (option.constraint_type == Sane.ConstraintType.WORD_LIST)
+ {
+ int distance = int.MAX, nearest = 0;
+
+ /* Find nearest value to requested */
+ for (var i = 0; i < option.word_list[0]; i++)
+ {
+ var x = (int) option.word_list[i+1];
+ var d = (int) (x - v);
+ d = d.abs ();
+ if (d < distance)
+ {
+ distance = d;
+ nearest = x;
+ }
+ }
+ v = (Sane.Int) nearest;
+ }
+
+ var status = Sane.control_option (handle, option_index, Sane.Action.SET_VALUE, &v, null);
+ debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=%d) -> (%s, %d)", (int) option_index, option.name, value, Sane.status_to_string (status), (int) v);
+ result = (int) v;
+ }
+
+ private void set_fixed_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, double value, out double result)
+ {
+ double v = value;
+ Sane.Fixed v_fixed;
+
+ return_if_fail (option.type == Sane.ValueType.FIXED);
+
+ if (option.constraint_type == Sane.ConstraintType.RANGE)
+ {
+ double min = Sane.UNFIX (option.range.min);
+ double max = Sane.UNFIX (option.range.max);
+
+ if (v < min)
+ v = min;
+ if (v > max)
+ v = max;
+ }
+ else if (option.constraint_type == Sane.ConstraintType.WORD_LIST)
+ {
+ double distance = double.MAX, nearest = 0.0;
+
+ /* Find nearest value to requested */
+ for (var i = 0; i < option.word_list[0]; i++)
+ {
+ double x = Sane.UNFIX (option.word_list[i+1]);
+ if (Math.fabs (x - v) < distance)
+ {
+ distance = Math.fabs (x - v);
+ nearest = x;
+ }
+ }
+ v = nearest;
+ }
+
+ v_fixed = Sane.FIX (v);
+ var status = Sane.control_option (handle, option_index, Sane.Action.SET_VALUE, &v_fixed, null);
+ debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=%f) -> (%s, %f)", (int) option_index, option.name, value, Sane.status_to_string (status), Sane.UNFIX (v_fixed));
+
+ result = Sane.UNFIX (v_fixed);
+ }
+
+ private void set_fixed_or_int_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, double value, out double result)
+ {
+ if (option.type == Sane.ValueType.FIXED)
+ set_fixed_option (handle, option, option_index, value, out result);
+ else if (option.type == Sane.ValueType.INT)
+ {
+ int r;
+ set_int_option (handle, option, option_index, (int) Math.round (value), out r);
+ result = r;
+ }
+ else
+ {
+ result = 0.0;
+ warning ("Unable to set unsupported option type");
+ }
+ }
+
+ private void set_option_to_max (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index)
+ {
+ if (option.constraint_type != Sane.ConstraintType.RANGE)
+ return;
+
+ var status = Sane.control_option (handle, option_index, Sane.Action.SET_VALUE, &option.range.max, null);
+
+ if (option.type == Sane.ValueType.FIXED)
+ debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=option.range.max=%f) -> (%s)", (int) option_index, option.name, Sane.UNFIX (option.range.max), Sane.status_to_string (status));
+ else
+ debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=option.range.max=%d) -> (%s)", (int) option_index, option.name, (int) option.range.max, Sane.status_to_string (status));
+ }
+
+ private bool set_string_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, string value, out string result)
+ {
+ result = "";
+
+ return_val_if_fail (option.type == Sane.ValueType.STRING, false);
+
+ var s = new char[option.size];
+ var i = 0;
+ for (; i < (option.size - 1) && value[i] != '\0'; i++)
+ s[i] = value[i];
+ s[i] = '\0';
+ var status = Sane.control_option (handle, option_index, Sane.Action.SET_VALUE, s, null);
+ result = (string) s;
+ debug ("sane_control_option (%d, SANE_ACTION_SET_VALUE, %s=\"%s\") -> (%s, \"%s\")", (int) option_index, option.name, value, Sane.status_to_string (status), result);
+
+ return status == Sane.Status.GOOD;
+ }
+
+ private bool set_constrained_string_option (Sane.Handle handle, Sane.OptionDescriptor option, Sane.Int option_index, string[] values, out string result)
+ {
+ return_val_if_fail (option.type == Sane.ValueType.STRING, false);
+ return_val_if_fail (option.constraint_type == Sane.ConstraintType.STRING_LIST, false);
+
+ for (var i = 0; values[i] != null; i++)
+ {
+ var j = 0;
+ for (; option.string_list[j] != null; j++)
+ {
+ if (values[i] == option.string_list[j])
+ break;
+ }
+
+ if (option.string_list[j] != null)
+ return set_string_option (handle, option, option_index, values[i], out result);
+ }
+
+ result = "";
+ return false;
+ }
+
+ private void log_option (Sane.Int index, Sane.OptionDescriptor option)
+ {
+ var s = "Option %d:".printf ((int) index);
+
+ if (option.name != "")
+ s += " name='%s'".printf (option.name);
+
+ if (option.title != "")
+ s += " title='%s'".printf (option.title);
+
+ switch (option.type)
+ {
+ case Sane.ValueType.BOOL:
+ s += " type=bool";
+ break;
+ case Sane.ValueType.INT:
+ s += " type=int";
+ break;
+ case Sane.ValueType.FIXED:
+ s += " type=fixed";
+ break;
+ case Sane.ValueType.STRING:
+ s += " type=string";
+ break;
+ case Sane.ValueType.BUTTON:
+ s += " type=button";
+ break;
+ case Sane.ValueType.GROUP:
+ s += " type=group";
+ break;
+ default:
+ s += " type=%d".printf (option.type);
+ break;
+ }
+
+ s += " size=%d".printf ((int) option.size);
+
+ switch (option.unit)
+ {
+ case Sane.Unit.NONE:
+ break;
+ case Sane.Unit.PIXEL:
+ s += " unit=pixels";
+ break;
+ case Sane.Unit.BIT:
+ s += " unit=bits";
+ break;
+ case Sane.Unit.MM:
+ s += " unit=mm";
+ break;
+ case Sane.Unit.DPI:
+ s += " unit=dpi";
+ break;
+ case Sane.Unit.PERCENT:
+ s += " unit=percent";
+ break;
+ case Sane.Unit.MICROSECOND:
+ s += " unit=microseconds";
+ break;
+ default:
+ s += " unit=%d".printf (option.unit);
+ break;
+ }
+
+ switch (option.constraint_type)
+ {
+ case Sane.ConstraintType.RANGE:
+ if (option.range != null) {
+ if (option.type == Sane.ValueType.FIXED)
+ s += " min=%f, max=%f, quant=%d".printf (Sane.UNFIX (option.range.min), Sane.UNFIX (option.range.max), (int) option.range.quant);
+ else
+ s += " min=%d, max=%d, quant=%d".printf ((int) option.range.min, (int) option.range.max, (int) option.range.quant);
+ }
+ break;
+ case Sane.ConstraintType.WORD_LIST:
+ s += " values=[";
+ for (var i = 0; i < option.word_list[0]; i++)
+ {
+ if (i != 0)
+ s += ", ";
+ if (option.type == Sane.ValueType.INT)
+ s += "%d".printf ((int) option.word_list[i+1]);
+ else
+ s += "%f".printf (Sane.UNFIX (option.word_list[i+1]));
+ }
+ s += "]";
+ break;
+ case Sane.ConstraintType.STRING_LIST:
+ s += " values=[";
+ if (option.string_list != null) {
+ for (var i = 0; option.string_list[i] != null; i++)
+ {
+ if (i != 0)
+ s += ", ";
+ s += "\"%s\"".printf (option.string_list[i]);
+ }
+ }
+ s += "]";
+ break;
+ default:
+ break;
+ }
+
+ var cap = option.cap;
+ if (cap != 0)
+ {
+ var caps = "";
+ if ((cap & Sane.Capability.SOFT_SELECT) != 0)
+ {
+ if (caps != "")
+ caps += ",";
+ caps += "soft-select";
+ cap &= ~Sane.Capability.SOFT_SELECT;
+ }
+ if ((cap & Sane.Capability.HARD_SELECT) != 0)
+ {
+ if (caps != "")
+ caps += ",";
+ caps += "hard-select";
+ cap &= ~Sane.Capability.HARD_SELECT;
+ }
+ if ((cap & Sane.Capability.SOFT_DETECT) != 0)
+ {
+ if (caps != "")
+ caps += ",";
+ caps += "soft-detect";
+ cap &= ~Sane.Capability.SOFT_DETECT;
+ }
+ if ((cap & Sane.Capability.EMULATED) != 0)
+ {
+ if (caps != "")
+ caps += ",";
+ caps += "emulated";
+ cap &= ~Sane.Capability.EMULATED;
+ }
+ if ((cap & Sane.Capability.AUTOMATIC) != 0)
+ {
+ if (caps != "")
+ caps += ",";
+ caps += "automatic";
+ cap &= ~Sane.Capability.AUTOMATIC;
+ }
+ if ((cap & Sane.Capability.INACTIVE) != 0)
+ {
+ if (caps != "")
+ caps += ",";
+ caps += "inactive";
+ cap &= ~Sane.Capability.INACTIVE;
+ }
+ if ((cap & Sane.Capability.ADVANCED) != 0)
+ {
+ if (caps != "")
+ caps += ",";
+ caps += "advanced";
+ cap &= ~Sane.Capability.ADVANCED;
+ }
+ /* Unknown capabilities */
+ if (cap != 0)
+ {
+ if (caps != "")
+ caps += ",";
+ caps += "%x".printf ((int) cap);
+ }
+ s += " cap=" + caps;
+ }
+
+ debug ("%s", s);
+
+ if (option.desc != null)
+ debug (" Description: %s", option.desc);
+ }
+
+ private static void authorization_cb (string resource, char[] username, char[] password)
+ {
+ scanner_object.notify_event (new NotifyRequestAuthorization (resource));
+
+ var credentials = scanner_object.authorize_queue.pop ();
+ for (var i = 0; credentials.username[i] != '\0' && i < Sane.MAX_USERNAME_LEN; i++)
+ username[i] = credentials.username[i];
+ for (var i = 0; credentials.password[i] != '\0' && i < Sane.MAX_PASSWORD_LEN; i++)
+ password[i] = credentials.password[i];
+ }
+
+ public void authorize (string username, string password)
+ {
+ var credentials = new Credentials ();
+ credentials.username = username;
+ credentials.password = password;
+ authorize_queue.push (credentials);
+ }
+
+ private void close_device ()
+ {
+ if (have_handle)
+ {
+ Sane.cancel (handle);
+ debug ("sane_cancel ()");
+
+ Sane.close (handle);
+ debug ("sane_close ()");
+ have_handle = false;
+ options = null;
+ }
+
+ buffer = null;
+ job_queue = null;
+
+ set_scanning (false);
+ }
+
+ private void fail_scan (int error_code, string error_string)
+ {
+ close_device ();
+ state = ScanState.IDLE;
+ notify_event (new NotifyScanFailed (error_code, error_string));
+ }
+
+ private bool handle_requests ()
+ {
+ /* Redetect when idle */
+ if (state == ScanState.IDLE && need_redetect)
+ state = ScanState.REDETECT;
+
+ /* Process all requests */
+ int request_count = 0;
+ while (true)
+ {
+ Request request;
+ if ((state == ScanState.IDLE && request_count == 0) ||
+ request_queue.length () > 0)
+ request = request_queue.pop ();
+ else
+ return true;
+
+ debug ("Processing request");
+ request_count++;
+
+ if (request is RequestStartScan)
+ {
+ var r = (RequestStartScan) request;
+ job_queue.append (r.job);
+ }
+ else if (request is RequestGetCapabilities)
+ {
+ var r = (RequestGetCapabilities) request;
+ do_get_capabilities (r.device);
+ }
+ else if (request is RequestCancel)
+ {
+ fail_scan (Sane.Status.CANCELLED, "Scan cancelled - do not report this error");
+ }
+ else if (request is RequestQuit)
+ {
+ close_device ();
+ return false;
+ }
+ }
+ }
+
+ private void do_open ()
+ {
+ var job = (ScanJob) job_queue.data;
+
+ line_count = 0;
+ pass_number = 0;
+ page_number = 0;
+ notified_page = -1;
+ option_index = 0;
+
+ if (job.device == null && default_device != null)
+ job.device = default_device;
+
+ if (job.device == null)
+ {
+ warning ("No scan device available");
+ fail_scan (0,
+ /* Error displayed when no scanners to scan with */
+ _("No scanners available. Please connect a scanner."));
+ return;
+ }
+
+ /* See if we can use the already open device */
+ if (have_handle)
+ {
+ if (current_device == job.device)
+ {
+ state = ScanState.GET_OPTION;
+ return;
+ }
+
+ Sane.close (handle);
+ debug ("sane_close ()");
+ have_handle = false;
+ }
+
+ current_device = null;
+
+ have_handle = false;
+ options = new HashTable (str_hash, str_equal);
+ var status = Sane.open (job.device, out handle);
+ debug ("sane_open (\"%s\") -> %s", job.device, Sane.status_to_string (status));
+
+ if (status != Sane.Status.GOOD)
+ {
+ warning ("Unable to open device: %s", Sane.strstatus (status));
+ fail_scan (status,
+ /* Error displayed when cannot connect to scanner */
+ _("Unable to connect to scanner"));
+ return;
+ }
+ have_handle = true;
+
+ current_device = job.device;
+ state = ScanState.GET_OPTION;
+ }
+
+ private void set_adf (ScanJob job, Sane.OptionDescriptor option, Sane.Int index)
+ {
+ string[] adf_sources =
+ {
+ "Automatic Document Feeder",
+ Sane.I18N ("Automatic Document Feeder"),
+ "ADF",
+ "Automatic Document Feeder(centrally aligned)", /* Seen in the proprietary brother3 driver */
+ "Automatic Document Feeder(center aligned)", /* Seen in Brother's proprietary brscan5 driver */
+ "Automatic Document Feeder(left aligned)", /* Seen in the proprietary brother3 driver */
+ "ADF Simplex" /* Samsung unified driver. LP: # 892915 */
+ };
+
+ string[] adf_front_sources =
+ {
+ "ADF Front",
+ Sane.I18N ("ADF Front")
+ };
+
+ string[] adf_back_sources =
+ {
+ "ADF Back",
+ Sane.I18N ("ADF Back")
+ };
+
+ string[] adf_duplex_sources =
+ {
+ "ADF Duplex",
+ "Duplex ADF", /* Brother DS-720, #157 */
+ Sane.I18N ("ADF Duplex"),
+ "ADF Duplex - Long-Edge Binding", /* Samsung unified driver. LP: # 892915 */
+ "ADF Duplex - Short-Edge Binding",
+ "Duplex", /* HP duplex scan support. LP: #1353599 */
+ "Automatic Document Feeder(centrally aligned,Duplex)", /* Brother duplex scan support. LP: #1343773 */
+ "Automatic Document Feeder(left aligned,Duplex)"
+ };
+
+ if (job.side == ScanSide.FRONT)
+ {
+ if (!set_constrained_string_option (handle, option, index, adf_front_sources, null))
+ if (!set_constrained_string_option (handle, option, index, adf_sources, null))
+ warning ("Unable to set front ADF source, please file a bug");
+ }
+ else if (job.side == ScanSide.BACK)
+ {
+ if (!set_constrained_string_option (handle, option, index, adf_back_sources, null))
+ if (!set_constrained_string_option (handle, option, index, adf_sources, null))
+ warning ("Unable to set back ADF source, please file a bug");
+ }
+ else if (job.side == ScanSide.BOTH)
+ {
+ if (!set_constrained_string_option (handle, option, index, adf_duplex_sources, null))
+ if (!set_constrained_string_option (handle, option, index, adf_sources, null))
+ warning ("Unable to set duplex ADF source, please file a bug");
+ }
+ }
+
+ private void do_get_option ()
+ {
+ var job = (ScanJob) job_queue.data;
+
+ var option = Sane.get_option_descriptor (handle, option_index);
+ debug ("sane_get_option_descriptor (%d)", (int) option_index);
+ var index = option_index;
+ option_index++;
+
+ /* Options complete, apply settings */
+ if (option == null)
+ {
+ /* Pick source */
+ option = get_option_by_name (handle, Sane.NAME_SCAN_SOURCE, out index);
+ if (option == null)
+ {
+ option = get_option_by_name (handle, "doc-source", out index); /* Samsung unified driver. LP: #892915 */
+ }
+ if (option != null)
+ {
+ string[] flatbed_sources =
+ {
+ "Auto",
+ Sane.I18N ("Auto"),
+ "Flatbed",
+ Sane.I18N ("Flatbed"),
+ "FlatBed",
+ "Normal",
+ Sane.I18N ("Normal"),
+ "Document Table" /* Epson scanners, eg. ET-3760 */
+ };
+
+ switch (job.type)
+ {
+ case ScanType.SINGLE:
+ case ScanType.BATCH:
+ if (!set_default_option (handle, option, index))
+ if (!set_constrained_string_option (handle, option, index, flatbed_sources, null))
+ {
+ warning ("Unable to set single page source, trying to set ADF instead");
+ warning ("If Flatbed is existing and it is not set, please file a bug");
+ set_adf (job, option, index);
+ }
+ break;
+ case ScanType.ADF:
+ set_adf (job, option, index);
+ break;
+ }
+ }
+
+ /* Scan mode (before resolution as it tends to affect that */
+ option = get_option_by_name (handle, Sane.NAME_SCAN_MODE, out index);
+ if (option != null)
+ {
+ /* The names of scan modes often used in drivers, as taken from the sane-backends source */
+ string[] color_scan_modes =
+ {
+ Sane.VALUE_SCAN_MODE_COLOR,
+ "Color",
+ "24bit Color[Fast]", /* brother4 driver, Brother DCP-1622WE, #134 */
+ "24bit Color", /* Seen in the proprietary brother3 driver */
+ "24-bit Color", /* #161 Lexmark CX310dn */
+ "24 bit Color", /* brscanads2200ads2700w */
+ "Color - 16 Million Colors" /* Samsung unified driver. LP: 892915 */
+ };
+ string[] gray_scan_modes =
+ {
+ Sane.VALUE_SCAN_MODE_GRAY,
+ "Gray",
+ "Grayscale",
+ Sane.I18N ("Grayscale"),
+ "8-bit Grayscale", /* #161 Lexmark CX310dn */
+ "True Gray", /* Seen in the proprietary brother3 driver */
+ "Grayscale - 256 Levels" /* Samsung unified driver. LP: 892915 */
+ };
+ string[] lineart_scan_modes =
+ {
+ Sane.VALUE_SCAN_MODE_LINEART,
+ "Lineart",
+ "LineArt",
+ Sane.I18N ("LineArt"),
+ "Black & White",
+ Sane.I18N ("Black & White"),
+ "Binary", /* Epson PM-A820 */
+ Sane.I18N ("Binary"),
+ "Thresholded",
+ Sane.VALUE_SCAN_MODE_GRAY,
+ "Gray",
+ "Grayscale",
+ Sane.I18N ("Grayscale"),
+ "True Gray", /* Seen in the proprietary brother3 driver */
+ "1-bit Black & White", /* #161 Lexmark CX310dn */
+ "Black and White - Line Art", /* Samsung unified driver. LP: 892915 */
+ "Black and White - Halftone",
+ "Monochrome" /* Epson */
+ };
+
+ switch (job.scan_mode)
+ {
+ case ScanMode.COLOR:
+ if (!set_constrained_string_option (handle, option, index, color_scan_modes, null))
+ warning ("Unable to set Color mode, please file a bug");
+ break;
+ case ScanMode.GRAY:
+ if (!set_constrained_string_option (handle, option, index, gray_scan_modes, null))
+ warning ("Unable to set Gray mode, please file a bug");
+ break;
+ case ScanMode.LINEART:
+ if (!set_constrained_string_option (handle, option, index, lineart_scan_modes, null))
+ warning ("Unable to set Lineart mode, please file a bug");
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* Duplex */
+ option = get_option_by_name (handle, "duplex", out index);
+ if (option == null) /* #161 Lexmark CX310dn Duplex */
+ option = get_option_by_name (handle, "scan-both-sides", out index);
+ if (option != null)
+ {
+ if (option.type == Sane.ValueType.BOOL)
+ set_bool_option (handle, option, index, job.side == ScanSide.BOTH, null);
+ }
+
+ /* Non-standard Epson GT-S50 ADF options */
+ option = get_option_by_name (handle, "adf-mode", out index);
+
+ /* Support Canon DR-C240 ADF_BOTH options */
+ if (option == null)
+ option = get_option_by_name (handle, "ScanMode", out index);
+ if (option != null)
+ {
+ string[] adf_simplex_modes =
+ {
+ "Simplex"
+ };
+ string[] adf_duplex_modes =
+ {
+ "Duplex"
+ };
+ if (job.side == ScanSide.BOTH)
+ set_constrained_string_option (handle, option, index, adf_duplex_modes, null);
+ else
+ set_constrained_string_option (handle, option, index, adf_simplex_modes, null);
+ }
+ option = get_option_by_name (handle, "adf-auto-scan", out index);
+ if (option != null)
+ {
+ if (option.type == Sane.ValueType.BOOL)
+ set_bool_option (handle, option, index, true, null);
+ }
+
+ /* Multi-page options */
+ option = get_option_by_name (handle, "batch-scan", out index);
+ if (option != null)
+ {
+ if (option.type == Sane.ValueType.BOOL)
+ set_bool_option (handle, option, index, (job.type != ScanType.SINGLE) && (job.type != ScanType.BATCH), null);
+ }
+
+ /* Set resolution and bit depth */
+ /* Epson may have separate resolution settings for x and y axes, which is preferable options to set */
+ option = get_option_by_name (handle, Sane.NAME_SCAN_X_RESOLUTION, out index);
+ if (option != null && (0 != (option.cap & Sane.Capability.SOFT_SELECT))) // L4160 has non-selectable separate options
+ {
+ set_fixed_or_int_option (handle, option, index, job.dpi, out job.dpi);
+ option = get_option_by_name (handle, Sane.NAME_SCAN_Y_RESOLUTION, out index);
+ }
+ else
+ option = get_option_by_name (handle, Sane.NAME_SCAN_RESOLUTION, out index);
+ if (option == null) /* #161 Lexmark CX310dn Duplex */
+ option = get_option_by_name (handle, "scan-resolution", out index);
+ if (option != null)
+ {
+ set_fixed_or_int_option (handle, option, index, job.dpi, out job.dpi);
+ option = get_option_by_name (handle, Sane.NAME_BIT_DEPTH, out index);
+ if (option != null)
+ {
+ if (job.depth > 0)
+ set_int_option (handle, option, index, job.depth, null);
+ }
+ }
+
+ /* Set scan area */
+ option = get_option_by_name (handle, Sane.NAME_SCAN_BR_X, out index);
+ if (option != null)
+ {
+ if (job.page_width > 0)
+ set_fixed_or_int_option (handle, option, index, convert_page_size (option, job.page_width, job.dpi), null);
+ else
+ set_option_to_max (handle, option, index);
+ }
+ option = get_option_by_name (handle, Sane.NAME_SCAN_BR_Y, out index);
+ if (option != null)
+ {
+ if (job.page_height > 0)
+ set_fixed_or_int_option (handle, option, index, convert_page_size (option, job.page_height, job.dpi), null);
+ else
+ set_option_to_max (handle, option, index);
+ }
+ if (job.page_width == 0)
+ {
+ /* #90 Fix automatic mode for Epson scanners */
+ option = get_option_by_name (handle, "scan-area", out index);
+ if (option != null)
+ set_string_option (handle, option, index, "Maximum", null);
+
+ /* #264 Enable automatic document size for Brother scanners */
+ option = get_option_by_name (handle, "AutoDocumentSize", out index);
+ if (option != null)
+ set_bool_option (handle, option, index, true, null);
+ }
+ /* Set page size */
+ option = get_option_by_name (handle, Sane.NAME_PAGE_WIDTH, out index);
+ if (option != null && job.page_width > 0.0)
+ set_fixed_or_int_option (handle, option, index, convert_page_size (option, job.page_width, job.dpi), null);
+ option = get_option_by_name (handle, Sane.NAME_PAGE_HEIGHT, out index);
+ if (option != null && job.page_height > 0.0)
+ set_fixed_or_int_option (handle, option, index, convert_page_size (option, job.page_height, job.dpi), null);
+
+ option = get_option_by_name (handle, Sane.NAME_BRIGHTNESS, out index);
+ if (option != null)
+ {
+ if (option.type == Sane.ValueType.FIXED)
+ {
+ var brightness = scale_fixed (-100, 100, option, job.brightness);
+ set_fixed_option (handle, option, index, brightness, null);
+ }
+ else if (option.type == Sane.ValueType.INT)
+ {
+ var brightness = scale_int (-100, 100, option, job.brightness);
+ set_int_option (handle, option, index, brightness, null);
+ }
+ else
+ warning ("Unable to set brightness, please file a bug");
+ }
+ option = get_option_by_name (handle, Sane.NAME_CONTRAST, out index);
+ if (option != null)
+ {
+ if (option.type == Sane.ValueType.FIXED)
+ {
+ var contrast = scale_fixed (-100, 100, option, job.contrast);
+ set_fixed_option (handle, option, index, contrast, null);
+ }
+ else if (option.type == Sane.ValueType.INT)
+ {
+ var contrast = scale_int (-100, 100, option, job.contrast);
+ set_int_option (handle, option, index, contrast, null);
+ }
+ else
+ warning ("Unable to set contrast, please file a bug");
+ }
+
+ /* Test scanner options (hoping will not effect other scanners...) */
+ if (current_device == "test")
+ {
+ option = get_option_by_name (handle, "hand-scanner", out index);
+ if (option != null)
+ set_bool_option (handle, option, index, false, null);
+ option = get_option_by_name (handle, "three-pass", out index);
+ if (option != null)
+ set_bool_option (handle, option, index, false, null);
+ option = get_option_by_name (handle, "test-picture", out index);
+ if (option != null)
+ set_string_option (handle, option, index, "Color pattern", null);
+ option = get_option_by_name (handle, "read-delay", out index);
+ if (option != null)
+ set_bool_option (handle, option, index, true, null);
+ option = get_option_by_name (handle, "read-delay-duration", out index);
+ if (option != null)
+ set_int_option (handle, option, index, 200000, null);
+ }
+
+ state = ScanState.START;
+ return;
+ }
+
+ log_option (index, option);
+
+ /* Ignore groups */
+ if (option.type == Sane.ValueType.GROUP)
+ return;
+
+ /* Some options are unnamed (e.g. Option 0) */
+ if (option.name == null)
+ return;
+
+ options.insert (option.name, (int) index);
+ }
+
+ private double convert_page_size (Sane.OptionDescriptor option, double size, double dpi)
+ {
+ if (option.unit == Sane.Unit.PIXEL)
+ return dpi * size / 254.0;
+ else if (option.unit == Sane.Unit.MM)
+ return size / 10.0;
+ else
+ {
+ warning ("Unable to set unsupported unit type");
+ return 0.0f;
+ }
+ }
+
+ private Sane.OptionDescriptor? get_option_by_name (Sane.Handle handle, string name, out int index)
+ {
+ index = options.lookup (name);
+ if (index == 0)
+ return null;
+
+ var option_descriptor = Sane.get_option_descriptor (handle, index);
+ /*
+ The Sane.Capability.INACTIVE capability indicates that
+ the option is not currently active (e.g., because it's meaningful
+ only if another option is set to some other value).
+ */
+ if ((option_descriptor.cap & Sane.Capability.INACTIVE) != 0)
+ {
+ warning ("The option %s (%d) is inactive and can't be set, please file a bug", name, index);
+ return null;
+ }
+ return option_descriptor;
+ }
+
+ /* Extract the list of supported resolutions (dpi) from a resolution option. */
+ private int[] get_resolution_list (Sane.OptionDescriptor option)
+ {
+ int[] resolutions = {};
+
+ if (option.constraint_type == Sane.ConstraintType.WORD_LIST)
+ {
+ for (var i = 0; i < option.word_list[0]; i++)
+ {
+ int v;
+ if (option.type == Sane.ValueType.FIXED)
+ v = (int) Math.round (Sane.UNFIX (option.word_list[i + 1]));
+ else
+ v = (int) option.word_list[i + 1];
+ resolutions += v;
+ }
+ }
+ else if (option.constraint_type == Sane.ConstraintType.RANGE)
+ {
+ int min, max;
+ if (option.type == Sane.ValueType.FIXED)
+ {
+ min = (int) Math.round (Sane.UNFIX (option.range.min));
+ max = (int) Math.round (Sane.UNFIX (option.range.max));
+ }
+ else
+ {
+ min = (int) option.range.min;
+ max = (int) option.range.max;
+ }
+
+ if (max < min)
+ return resolutions;
+
+ /* Offer the commonly used resolutions that fall within the supported range,
+ always including the range extremes. */
+ int[] candidates = { 75, 100, 150, 200, 300, 600, 1200, 2400, 4800 };
+ resolutions += min;
+ foreach (var c in candidates)
+ {
+ if (c > min && c < max)
+ resolutions += c;
+ }
+ if (max != min)
+ resolutions += max;
+ }
+
+ return resolutions;
+ }
+
+ /* Return the maximum extent of a scan area option (br-x/br-y) in tenths of a
+ millimetre, or 0 if it cannot be determined. */
+ private int get_dimension_max (Sane.OptionDescriptor option)
+ {
+ if (option.constraint_type != Sane.ConstraintType.RANGE)
+ return 0;
+
+ /* Only millimetre units can be converted without knowing the resolution. */
+ if (option.unit != Sane.Unit.MM)
+ return 0;
+
+ double max_mm;
+ if (option.type == Sane.ValueType.FIXED)
+ max_mm = Sane.UNFIX (option.range.max);
+ else
+ max_mm = (double) option.range.max;
+
+ return (int) Math.round (max_mm * 10.0);
+ }
+
+ /* Open the device (if necessary) and read its supported resolutions and
+ maximum page size, notifying the main thread of the result. */
+ private void do_get_capabilities (string device)
+ {
+ /* Don't disturb an ongoing scan */
+ if (scanning)
+ return;
+
+ Sane.Handle caps_handle;
+ var opened_here = false;
+
+ if (have_handle && current_device == device)
+ caps_handle = handle;
+ else
+ {
+ var status = Sane.open (device, out caps_handle);
+ debug ("sane_open (\"%s\") -> %s", device, Sane.status_to_string (status));
+ if (status != Sane.Status.GOOD)
+ {
+ warning ("Unable to open device to read capabilities: %s", Sane.strstatus (status));
+ return;
+ }
+ opened_here = true;
+ }
+
+ var capabilities = new ScanDeviceCapabilities ();
+
+ for (var i = 1; ; i++)
+ {
+ var option = Sane.get_option_descriptor (caps_handle, i);
+ if (option == null)
+ break;
+ if (option.name == null)
+ continue;
+
+ if (capabilities.resolutions.length == 0 &&
+ (option.name == Sane.NAME_SCAN_RESOLUTION ||
+ option.name == Sane.NAME_SCAN_X_RESOLUTION ||
+ option.name == "scan-resolution"))
+ capabilities.resolutions = get_resolution_list (option);
+ else if (option.name == Sane.NAME_SCAN_BR_X)
+ capabilities.max_width = get_dimension_max (option);
+ else if (option.name == Sane.NAME_SCAN_BR_Y)
+ capabilities.max_height = get_dimension_max (option);
+ }
+
+ if (opened_here)
+ {
+ Sane.close (caps_handle);
+ debug ("sane_close ()");
+ }
+
+ debug ("Device capabilities: %d resolutions, max_width=%d, max_height=%d",
+ capabilities.resolutions.length, capabilities.max_width, capabilities.max_height);
+
+ notify_event (new NotifyDeviceCapabilities (device, capabilities));
+ }
+
+ private void do_complete_document ()
+ {
+ Sane.cancel (handle);
+ debug ("sane_cancel ()");
+
+ job_queue.remove_link (job_queue);
+
+ state = ScanState.IDLE;
+
+ /* Continue onto the next job */
+ if (job_queue != null)
+ {
+ state = ScanState.OPEN;
+ return;
+ }
+
+ /* Trigger timeout to close */
+ // TODO
+
+ notify_event (new NotifyDocumentDone ());
+ set_scanning (false);
+ }
+
+ private void do_start ()
+ {
+ Sane.Status status;
+
+ notify_event (new NotifyExpectPage ());
+
+ status = Sane.start (handle);
+ debug ("sane_start (page=%d, pass=%d) -> %s", page_number, pass_number, Sane.status_to_string (status));
+ if (status == Sane.Status.GOOD)
+ state = ScanState.GET_PARAMETERS;
+ else if (status == Sane.Status.NO_DOCS)
+ {
+ var job = (ScanJob) job_queue.data;
+ if (job.type == ScanType.BATCH)
+ {
+ /* In BATCH mode ("Multiple Pages from Flatbed") some scanners return
+ NO_DOCS after the first page is scanned. Try cancelling and restarting
+ to continue scanning more pages. */
+ debug ("sane_start received NO_DOCS in BATCH mode -> cancelling and restarting");
+
+ Sane.cancel(handle);
+
+ state = ScanState.START;
+ return;
+ }
+ else
+ {
+ do_complete_document ();
+ if (page_number == 0)
+ fail_scan (status,
+ /* Error displayed when no documents at the start of scanning */
+ _("Document feeder empty"));
+ }
+ }
+ else if (status == Sane.Status.NO_MEM)
+ {
+ fail_scan (status,
+ /* Out of memory error message with help instruction.
+ Message written in Pango text markup language,
+ A carriage return makes a line break, tag makes a monospace font */
+ _("Insufficient memory to perform scan.\n" +
+ "Try to decrease Resolution or Page Size in Preferences menu. " +
+ "For some scanners when scanning in high resolution, the scan size is restricted."));
+ }
+ else if (status == Sane.Status.DEVICE_BUSY)
+ {
+ /* If device is busy don't interrupt, but keep waiting for scanner */
+ }
+ else
+ {
+ warning ("Unable to start device: %s", Sane.strstatus (status));
+ fail_scan (status,
+ /* Error display when unable to start scan */
+ _("Unable to start scan"));
+ notify_event (new NotifyDocumentDone ());
+ set_scanning (false);
+ }
+ }
+
+ private void do_get_parameters ()
+ {
+ var status = Sane.get_parameters (handle, out parameters);
+ debug ("sane_get_parameters () -> %s", Sane.status_to_string (status));
+ if (status != Sane.Status.GOOD)
+ {
+ warning ("Unable to get device parameters: %s", Sane.strstatus (status));
+ fail_scan (status,
+ /* Error displayed when communication with scanner broken */
+ _("Error communicating with scanner"));
+ return;
+ }
+
+ var job = (ScanJob) job_queue.data;
+
+ debug ("Parameters: format=%s last_frame=%s bytes_per_line=%d pixels_per_line=%d lines=%d depth=%d",
+ Sane.frame_to_string (parameters.format),
+ parameters.last_frame ? "SANE_TRUE" : "SANE_FALSE",
+ parameters.bytes_per_line,
+ parameters.pixels_per_line,
+ parameters.lines,
+ parameters.depth);
+
+ var info = new ScanPageInfo ();
+ info.width = parameters.pixels_per_line;
+ info.height = parameters.lines;
+ info.page_size = job.page_size;
+ info.depth = parameters.depth;
+ /* Reduce bit depth if requested lower than received */
+ // FIXME: This a hack and only works on 8 bit gray to 2 bit gray
+ if (parameters.depth == 8 && parameters.format == Sane.Frame.GRAY && job.depth == 2 && job.scan_mode == ScanMode.GRAY)
+ info.depth = job.depth;
+ info.n_channels = parameters.format == Sane.Frame.GRAY ? 1 : 3;
+ info.dpi = job.dpi; // FIXME: This is the requested DPI, not the actual DPI
+ info.device = current_device;
+
+ if (page_number != notified_page)
+ {
+ notify_event (new NotifyGotPageInfo (job.id, info));
+ notified_page = page_number;
+ }
+
+ /* Prepare for read */
+ var buffer_size = parameters.bytes_per_line + 1; /* Use +1 so buffer is not resized if driver returns one line per read */
+ buffer = new uchar[buffer_size];
+ n_used = 0;
+ line_count = 0;
+ pass_number = 0;
+ state = ScanState.READ;
+ }
+
+ private void do_complete_page ()
+ {
+ var job = (ScanJob) job_queue.data;
+
+ notify_event (new NotifyPageDone (job.id));
+
+ /* If multi-pass then scan another page */
+ if (!parameters.last_frame)
+ {
+ pass_number++;
+ state = ScanState.START;
+ return;
+ }
+
+ /* Go back for another page */
+ if (job.type != ScanType.SINGLE)
+ {
+ if (job.type == ScanType.BATCH)
+ {
+ display_page_delay (job.page_delay);
+ Thread.usleep (job.page_delay * 1000);
+ }
+ else
+ display_page_delay (0);
+
+ page_number++;
+ pass_number = 0;
+ state = ScanState.START;
+ return;
+ }
+
+ do_complete_document ();
+ }
+
+ private void do_read ()
+ {
+ var job = (ScanJob) job_queue.data;
+
+ /* Read as many bytes as we expect */
+ var n_to_read = buffer.length - n_used;
+
+ Sane.Int n_read;
+ var b = (uchar *) buffer;
+ var status = Sane.read (handle, (uint8[]) (b + n_used), (Sane.Int) n_to_read, out n_read);
+ debug ("sane_read (%d) -> (%s, %d)", n_to_read, Sane.status_to_string (status), (int) n_read);
+
+ /* Completed read */
+ if (status == Sane.Status.EOF)
+ {
+ if (parameters.lines > 0 && line_count != parameters.lines)
+ warning ("Scan completed with %d lines, expected %d lines", line_count, parameters.lines);
+ if (n_used > 0)
+ warning ("Scan complete with %d bytes of unused data", n_used);
+ do_complete_page ();
+ return;
+ }
+
+ /* Some ADF scanners only return NO_DOCS after a read */
+ if (status == Sane.Status.NO_DOCS)
+ {
+ do_complete_document ();
+ if (page_number == 0)
+ fail_scan (status,
+ /* Error displayed when no documents at the start of scanning */
+ _("Document feeder empty"));
+ return;
+ }
+
+ /* Communication error */
+ if (status != Sane.Status.GOOD)
+ {
+ warning ("Unable to read frame from device: %s", Sane.strstatus (status));
+ fail_scan (status,
+ /* Error displayed when communication with scanner broken */
+ _("Error communicating with scanner"));
+ notify_event (new NotifyDocumentDone ());
+ set_scanning (false);
+ return;
+ }
+
+ bool full_read = false;
+ if (n_used == 0 && n_read == buffer.length)
+ full_read = true;
+ n_used += (int) n_read;
+
+ /* Feed out lines */
+ if (n_used >= parameters.bytes_per_line)
+ {
+ var line = new ScanLine ();
+ switch (parameters.format)
+ {
+ case Sane.Frame.GRAY:
+ line.channel = 0;
+ break;
+ case Sane.Frame.RGB:
+ line.channel = -1;
+ break;
+ case Sane.Frame.RED:
+ line.channel = 0;
+ break;
+ case Sane.Frame.GREEN:
+ line.channel = 1;
+ break;
+ case Sane.Frame.BLUE:
+ line.channel = 2;
+ break;
+ }
+ line.width = parameters.pixels_per_line;
+ line.depth = parameters.depth;
+ line.data = (owned) buffer;
+ line.data_length = parameters.bytes_per_line;
+ line.number = line_count;
+ line.n_lines = n_used / line.data_length;
+
+ line_count += line.n_lines;
+
+ /* Increase buffer size if did full read */
+ var buffer_size = line.data.length;
+ if (full_read)
+ buffer_size += parameters.bytes_per_line;
+
+ buffer = new uchar[buffer_size];
+ var n_remaining = n_used - (line.n_lines * line.data_length);
+ n_used = 0;
+ for (var i = 0; i < n_remaining; i++)
+ {
+ buffer[i] = line.data[i + (line.n_lines * line.data_length)];
+ n_used++;
+ }
+
+ /* Reduce bit depth if requested lower than received */
+ // FIXME: This a hack and only works on 8 bit gray to 2 bit gray
+ if (parameters.depth == 8 && parameters.format == Sane.Frame.GRAY &&
+ job.depth == 2 && job.scan_mode == ScanMode.GRAY)
+ {
+ uchar block = 0;
+ var write_offset = 0;
+ var block_shift = 6;
+ for (var i = 0; i < line.n_lines; i++)
+ {
+ var offset = i * line.data_length;
+ for (var x = 0; x < line.width; x++)
+ {
+ var p = line.data[offset + x];
+
+ uchar sample;
+ if (p >= 192)
+ sample = 3;
+ else if (p >= 128)
+ sample = 2;
+ else if (p >= 64)
+ sample = 1;
+ else
+ sample = 0;
+
+ block |= sample << block_shift;
+ if (block_shift == 0)
+ {
+ line.data[write_offset] = block;
+ write_offset++;
+ block = 0;
+ block_shift = 6;
+ }
+ else
+ block_shift -= 2;
+ }
+
+ /* Finish each line on a byte boundary */
+ if (block_shift != 6)
+ {
+ line.data[write_offset] = block;
+ write_offset++;
+ block = 0;
+ block_shift = 6;
+ }
+ }
+
+ line.data_length = (line.width * 2 + 7) / 8;
+ }
+
+ notify_event (new NotifyGotLine (job.id, line));
+ }
+ }
+
+ private void* scan_thread ()
+ {
+ state = ScanState.IDLE;
+
+ Sane.Int version_code;
+ var status = Sane.init (out version_code, authorization_cb);
+ debug ("sane_init () -> %s", Sane.status_to_string (status));
+ if (status != Sane.Status.GOOD)
+ {
+ warning ("Unable to initialize SANE backend: %s", Sane.strstatus(status));
+ return null;
+ }
+ debug ("SANE version %d.%d.%d",
+ Sane.VERSION_MAJOR(version_code),
+ Sane.VERSION_MINOR(version_code),
+ Sane.VERSION_BUILD(version_code));
+
+ /* Scan for devices on first start */
+ redetect ();
+
+ while (handle_requests ())
+ {
+ switch (state)
+ {
+ case ScanState.IDLE:
+ if (job_queue != null)
+ {
+ set_scanning (true);
+ state = ScanState.OPEN;
+ }
+ break;
+ case ScanState.REDETECT:
+ do_redetect ();
+ break;
+ case ScanState.OPEN:
+ do_open ();
+ break;
+ case ScanState.GET_OPTION:
+ do_get_option ();
+ break;
+ case ScanState.START:
+ do_start ();
+ break;
+ case ScanState.GET_PARAMETERS:
+ do_get_parameters ();
+ break;
+ case ScanState.READ:
+ do_read ();
+ break;
+ }
+ }
+
+ return null;
+ }
+
+ public void start ()
+ {
+ try
+ {
+ thread = new Thread.try ("scan-thread", scan_thread);
+ }
+ catch (Error e)
+ {
+ critical ("Unable to create thread: %s", e.message);
+ }
+ }
+
+ public void redetect ()
+ {
+ if (need_redetect)
+ return;
+ need_redetect = true;
+
+ debug ("Requesting redetection of scan devices");
+
+ request_queue.push (new RequestRedetect ());
+ }
+
+ public void get_capabilities (string device)
+ {
+ debug ("Requesting capabilities of device \"%s\"", device);
+
+ request_queue.push (new RequestGetCapabilities (device));
+ }
+
+ public bool is_scanning ()
+ {
+ return scanning;
+ }
+
+ private string get_scan_mode_string (ScanMode mode)
+ {
+ switch (mode)
+ {
+ case ScanMode.DEFAULT:
+ return "ScanMode.DEFAULT";
+ case ScanMode.COLOR:
+ return "ScanMode.COLOR";
+ case ScanMode.GRAY:
+ return "ScanMode.GRAY";
+ case ScanMode.LINEART:
+ return "ScanMode.LINEART";
+ default:
+ return "%d".printf (mode);
+ }
+ }
+
+ public static string type_to_string (ScanType type)
+ {
+ switch (type)
+ {
+ case ScanType.SINGLE:
+ return "single";
+ case ScanType.BATCH:
+ return "batch";
+ case ScanType.ADF:
+ return "adf";
+ default:
+ return "%d".printf (type);
+ }
+ }
+
+ public static ScanType type_from_string (string type)
+ {
+ switch (type)
+ {
+ case "single":
+ return ScanType.SINGLE;
+ case "batch":
+ return ScanType.BATCH;
+ case "adf":
+ return ScanType.ADF;
+ default:
+ warning ("Unknown ScanType: %s. Please report this error.", type);
+ return ScanType.SINGLE;
+ }
+ }
+
+ public static string side_to_string (ScanSide side)
+ {
+ switch (side)
+ {
+ case ScanSide.FRONT:
+ return "front";
+ case ScanSide.BACK:
+ return "back";
+ case ScanSide.BOTH:
+ return "both";
+ default:
+ return "%d".printf (side);
+ }
+ }
+
+ public static ScanSide side_from_string (string side)
+ {
+ switch (side)
+ {
+ case "front":
+ return ScanSide.FRONT;
+ case "back":
+ return ScanSide.BACK;
+ case "both":
+ return ScanSide.BOTH;
+ default:
+ warning ("Unknown ScanSide: %s. Please report this error.", side);
+ return ScanSide.FRONT;
+ }
+ }
+
+ public void scan (string? device, ScanOptions options)
+ {
+ debug ("Scanner.scan (\"%s\", dpi=%d, scan_mode=%s, depth=%d, type=%s, side=%s, paper_width=%d, paper_height=%d, page_size=%s, brightness=%d, contrast=%d, delay=%dms)",
+ device != null ? device : "(null)", options.dpi, get_scan_mode_string (options.scan_mode), options.depth,
+ type_to_string (options.type), side_to_string (options.side),
+ options.paper_width, options.paper_height, options.page_size,
+ options.brightness, options.contrast, options.page_delay);
+ var request = new RequestStartScan ();
+ request.job = new ScanJob ();
+ request.job.id = job_id++;
+ request.job.device = device;
+ request.job.dpi = options.dpi;
+ request.job.scan_mode = options.scan_mode;
+ request.job.depth = options.depth;
+ request.job.type = options.type;
+ request.job.side = options.side;
+ request.job.page_width = options.paper_width;
+ request.job.page_height = options.paper_height;
+ request.job.page_size = options.page_size;
+ request.job.brightness = options.brightness;
+ request.job.contrast = options.contrast;
+ request.job.page_delay = options.page_delay;
+ request_queue.push (request);
+ }
+
+ public void cancel ()
+ {
+ first_job_id = job_id;
+ request_queue.push (new RequestCancel ());
+ }
+
+ public void free ()
+ {
+ debug ("Stopping scan thread");
+
+ request_queue.push (new RequestQuit ());
+
+ if (thread != null)
+ {
+ thread.join ();
+ thread = null;
+ }
+
+ Sane.exit ();
+ debug ("sane_exit ()");
+ }
+}
diff --git a/src/simple-scan.vala b/src/simple-scan.vala
index 129a683b68ba4ed95826ffead56384aca2bc40c8..e26f140bd90efb95a1a012d3a9088deef4a7d846 100644
--- a/src/simple-scan.vala
+++ b/src/simple-scan.vala
@@ -1,2031 +1,2170 @@
-/*
- * 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 class SimpleScan : Adw.Application
-{
- static bool show_version;
- static bool debug_enabled;
- static string? fix_pdf_filename = null;
- const OptionEntry[] options =
- {
- { "version", 'v', 0, OptionArg.NONE, ref show_version,
- /* Help string for command line --version flag */
- N_("Show release version"), null},
- { "debug", 'd', 0, OptionArg.NONE, ref debug_enabled,
- /* Help string for command line --debug flag */
- N_("Print debugging messages"), null},
- { "fix-pdf", 0, 0, OptionArg.STRING, ref fix_pdf_filename,
- N_("Fix PDF files generated with older versions of this app"), "FILENAME…"},
- { null }
- };
- private static Timer log_timer;
- private static FileStream? log_file;
-
- private ScanDevice? default_device = null;
- private bool have_devices = false;
- private GUsb.Context usb_context;
- private AppWindow app;
- private Scanner scanner;
- private Book book;
- private Page scanned_page;
-
- public SimpleScan (ScanDevice? device = null)
- {
- Object (
- /* The inhibit () method use this */
- application_id: "org.gnome.SimpleScan",
- /* Icon resources will be looked up starting from here */
- resource_base_path: "/org/gnome/SimpleScan"
- );
- register_session = true;
-
- default_device = device;
- }
-
- public override void startup ()
- {
- base.startup ();
-
- app = new AppWindow ();
- book = app.book;
- app.start_scan.connect (scan_cb);
- app.stop_scan.connect (cancel_cb);
- app.redetect.connect (redetect_cb);
-
- scanner = Scanner.get_instance ();
- scanner.update_devices.connect (update_scan_devices_cb);
- scanner.request_authorization.connect (authorize_cb);
- scanner.expect_page.connect (scanner_new_page_cb);
- scanner.got_page_info.connect (scanner_page_info_cb);
- scanner.got_line.connect (scanner_line_cb);
- scanner.page_done.connect (scanner_page_done_cb);
- scanner.document_done.connect (scanner_document_done_cb);
- scanner.scan_failed.connect (scanner_failed_cb);
- scanner.scanning_changed.connect (scanner_scanning_changed_cb);
-
- try
- {
- usb_context = new GUsb.Context ();
- usb_context.device_added.connect (() => { scanner.redetect (); });
- usb_context.device_removed.connect (() => { scanner.redetect (); });
- }
- catch (Error e)
- {
- warning ("Failed to create USB context: %s\n", e.message);
- }
-
- if (default_device != null)
- {
- List device_list = null;
-
- device_list.append (default_device);
- app.set_scan_devices (device_list);
- app.set_selected_device (default_device.name);
- }
-
- app.start ();
- scanner.start ();
- }
-
- public override void activate ()
- {
- base.activate ();
- app.present ();
- }
-
- public override void shutdown ()
- {
- base.shutdown ();
- book = null;
- app = null;
- usb_context = null;
- scanner.free ();
- }
-
- private void update_scan_devices_cb (Scanner scanner, List devices)
- {
- var devices_copy = devices.copy_deep ((CopyFunc) Object.ref);
-
- /* If the default device is not detected add it to the list */
- if (default_device != null)
- {
- var default_in_list = false;
- foreach (var device in devices_copy)
- {
- if (device.name == default_device.name)
- {
- default_in_list = true;
- break;
- }
- }
-
- if (!default_in_list)
- devices_copy.prepend (default_device);
- }
-
- have_devices = devices_copy.length () > 0;
-
- /* If SANE doesn't see anything, see if we recognise any of the USB devices */
- string? missing_driver = null;
- if (!have_devices)
- missing_driver = suggest_driver ();
-
- app.set_scan_devices (devices_copy, missing_driver);
- }
-
- /* Taken from /usr/local/Brother/sane/Brsane.ini from brscan driver */
- private const uint32 brscan_devices[] = { 0x04f90110, 0x04f90111, 0x04f90112, 0x04f9011d, 0x04f9011e, 0x04f9011f, 0x04f9012b, 0x04f90124, 0x04f90153, 0x04f90125, 0x04f90113, 0x04f90114, 0x04f90115, 0x04f90116, 0x04f90119, 0x04f9011a, 0x04f9011b, 0x04f9011c, 0x04f9012e, 0x04f9012f, 0x04f90130, 0x04f90128, 0x04f90127, 0x04f90142, 0x04f90143, 0x04f90140, 0x04f90141, 0x04f9014e, 0x04f9014f, 0x04f90150, 0x04f90151, 0x04f9010e, 0x04f9013a, 0x04f90120, 0x04f9010f, 0x04f90121, 0x04f90122, 0x04f90132, 0x04f9013d, 0x04f9013c, 0x04f90136, 0x04f90135, 0x04f9013e, 0x04f9013f, 0x04f90144, 0x04f90146, 0x04f90148, 0x04f9014a, 0x04f9014b, 0x04f9014c, 0x04f90157, 0x04f90158, 0x04f9015d, 0x04f9015e, 0x04f9015f, 0x04f90160 };
-
- /* Taken from /usr/local/Brother/sane/models2/*.ini from brscan2 driver */
- private const uint32 brscan2_devices[] = { 0x04f901c9, 0x04f901ca, 0x04f901cb, 0x04f901cc, 0x04f901ec, 0x04f901e4, 0x04f901e3, 0x04f901e2, 0x04f901e1, 0x04f901e0, 0x04f901df, 0x04f901de, 0x04f901dd, 0x04f901dc, 0x04f901db, 0x04f901da, 0x04f901d9, 0x04f901d8, 0x04f901d7, 0x04f901d6, 0x04f901d5, 0x04f901d4, 0x04f901d3, 0x04f901d2, 0x04f901d1, 0x04f901d0, 0x04f901cf, 0x04f901ce, 0x04f9020d, 0x04f9020c, 0x04f9020a };
-
- /* Taken from /usr/local/Brother/sane/models3/*.ini from brscan3 driver */
- private const uint32 brscan3_devices[] = { 0x04f90222, 0x04f90223, 0x04f90224, 0x04f90225, 0x04f90229, 0x04f9022a, 0x04f9022c, 0x04f90228, 0x04f90236, 0x04f90227, 0x04f9022b, 0x04f9022d, 0x04f9022e, 0x04f9022f, 0x04f90230, 0x04f9021b, 0x04f9021a, 0x04f90219, 0x04f9023f, 0x04f90216, 0x04f9021d, 0x04f9021c, 0x04f90220, 0x04f9021e, 0x04f9023e, 0x04f90235, 0x04f9023a, 0x04f901c9, 0x04f901ca, 0x04f901cb, 0x04f901cc, 0x04f901ec, 0x04f9020d, 0x04f9020c, 0x04f90257, 0x04f9025d, 0x04f90254, 0x04f9025b, 0x04f9026b, 0x04f90258, 0x04f9025e, 0x04f90256, 0x04f90240, 0x04f9025f, 0x04f90260, 0x04f90261, 0x04f90278, 0x04f9026f, 0x04f9026e, 0x04f9026d, 0x04f90234, 0x04f90239, 0x04f90253, 0x04f90255, 0x04f90259, 0x04f9025a, 0x04f9025c, 0x04f90276 };
-
- /* Taken from /opt/brother/scanner/brscan4/models4/*.ini from brscan4 driver */
- private const uint32 brscan4_devices[] = {
- 0x04f90314, /* MFC-L9550CDW */
- 0x04f90313, /* MFC-L8850CDW */
- 0x04f90312, /* MFC-L8650CDW */
- 0x04f90311, /* MFC-L8600CDW */
- 0x04f90310, /* DCP-L8450CDW */
- 0x04f9030f, /* DCP-L8400CDN */
- 0x04f90366, /* MFC-J5520DW */
- 0x04f90365, /* MFC-J4520DW */
- 0x04f90364, /* MFC-J5625DW */
- 0x04f90350, /* MFC-J5620CDW */
- 0x04f9034f, /* MFC-J5820DN */
- 0x04f9034e, /* MFC-J5720CDW */
- 0x04f9034b, /* MFC-J4720N */
- 0x04f90349, /* DCP-J4220N */
- 0x04f90347, /* MFC-J5720DW */
- 0x04f90346, /* MFC-J5620DW */
- 0x04f90343, /* MFC-J5320DW */
- 0x04f90342, /* MFC-J4625DW */
- 0x04f90341, /* MFC-J2720 */
- 0x04f90340, /* MFC-J4620DW */
- 0x04f9033d, /* MFC-J4420DW */
- 0x04f9033c, /* MFC-J2320 */
- 0x04f9033a, /* MFC-J4320DW */
- 0x04f90339, /* DCP-J4120DW */
- 0x04f90392, /* MFC-L2705DW */
- 0x04f90373, /* MFC-L2700D */
- 0x04f9036e, /* MFC-7889DW */
- 0x04f9036d, /* MFC-7880DN */
- 0x04f9036c, /* MFC-7480D */
- 0x04f9036b, /* MFC-7380 */
- 0x04f9036a, /* DCP-7189DW */
- 0x04f90369, /* DCP-7180DN */
- 0x04f90368, /* DCP-7080 */
- 0x04f90367, /* DCP-7080D */
- 0x04f90338, /* MFC-L2720DN */
- 0x04f90337, /* MFC-L2720DW */
- 0x04f90335, /* FAX-L2700DN */
- 0x04f90331, /* MFC-L2700DW */
- 0x04f90330, /* HL-L2380DW */
- 0x04f90329, /* DCP-L2560DW */
- 0x04f90328, /* DCP-L2540DW */
- 0x04f90326, /* DCP-L2540DN */
- 0x04f90324, /* DCP-L2520D */
- 0x04f90322, /* DCP-L2520DW */
- 0x04f90321, /* DCP-L2500D */
- 0x04f90320, /* MFC-L2740DW */
- 0x04f90372, /* MFC-9342CDW */
- 0x04f90371, /* MFC-9332CDW */
- 0x04f90370, /* MFC-9142CDN */
- 0x04f9036f, /* DCP-9022CDW */
- 0x04f90361, /* MFC-1919NW */
- 0x04f90360, /* DCP-1618W */
- 0x04f9035e, /* MFC-1910NW */
- 0x04f9035d, /* MFC-1910W */
- 0x04f9035c, /* DCP-1610NW */
- 0x04f9035b, /* DCP-1610W */
- 0x04f90379, /* DCP-1619 */
- 0x04f90378, /* DCP-1608 */
- 0x04f90376, /* DCP-1600 */
- 0x04f9037a, /* MFC-1906 */
- 0x04f9037b, /* MFC-1908 */
- 0x04f90377, /* MFC-1900 */
- 0x04f9037f, /* ADS-2600We */
- 0x04f9037e, /* ADS-2500We */
- 0x04f9037d, /* ADS-2100e */
- 0x04f9037c, /* ADS-2000e */
- 0x04f9035a, /* MFC-J897DN */
- 0x04f90359, /* MFC-J827DN */
- 0x04f90358, /* MFC-J987DN */
- 0x04f90357, /* MFC-J727D */
- 0x04f90356, /* MFC-J877N */
- 0x04f90355, /* DCP-J957N */
- 0x04f90354, /* DCP-J757N */
- 0x04f90353, /* DCP-J557N */
- 0x04f90351, /* DCP-J137N */
- 0x04f90390, /* MFC-J5920DW */
- 0x04f903b3, /* MFC-J6925DW */
- 0x04f90396, /* MFC-T800W */
- 0x04f90395, /* DCP-T700W */
- 0x04f90394, /* DCP-T500W */
- 0x04f90393, /* DCP-T300 */
- 0x04f90380, /* DCP-J562DW */
- 0x04f90381, /* DCP-J562N */
- 0x04f903bd, /* DCP-J762N */
- 0x04f90383, /* DCP-J962N */
- 0x04f90397, /* DCP-J963N */
- 0x04f90386, /* MFC-J460DW */
- 0x04f90384, /* MFC-J480DW */
- 0x04f90385, /* MFC-J485DW */
- 0x04f90388, /* MFC-J680DW */
- 0x04f90389, /* MFC-J880DW */
- 0x04f9038b, /* MFC-J880N */
- 0x04f9038a, /* MFC-J885DW */
- 0x04f9038c, /* MFC-J730DN */
- 0x04f9038e, /* MFC-J830DN */
- 0x04f9038f, /* MFC-J900DN */
- 0x04f9038d, /* MFC-J990DN */
- 0x04f903bc, /* MFC-L2700DN */
- 0x04f903bb, /* MFC-L2680W */
- 0x04f903b6, /* MFC-J6990CDW */
- 0x04f903b5, /* MFC-J6973CDW */
- 0x04f903b4, /* MFC-J6573CDW */
- 0x04f9034a, /* DCP-J4225N */
- 0x04f9034c, /* MFC-J4725N */
- 0x04f903c5, /* MFC-9335CDW */
- 0x04f903c1, /* HL-3180CDW */
- 0x04f903c0, /* DCP-9015CDW */
- 0x04f903bf, /* DCP-9017CDW */
- 0x04f903c7, /* MFC-L5702DW */
- 0x04f903c6, /* MFC-L5700DW */
- 0x04f903b2, /* MFC-L5755DW */
- 0x04f903b1, /* MFC-L6902DW */
- 0x04f903b0, /* MFC-L6900DW */
- 0x04f903af, /* MFC-L6800DW */
- 0x04f903ae, /* MFC-L6750DW */
- 0x04f903ad, /* MFC-L6702DW */
- 0x04f903ac, /* MFC-L6700DW */
- 0x04f903ab, /* MFC-L5902DW */
- 0x04f903aa, /* MFC-L5900DW */
- 0x04f903a9, /* MFC-L5850DW */
- 0x04f903a8, /* MFC-L5802DW */
- 0x04f903a7, /* MFC-L5800DW */
- 0x04f903a6, /* MFC-8540DN */
- 0x04f903a5, /* MFC-L5750DW */
- 0x04f903a3, /* MFC-8535DN */
- 0x04f903a2, /* MFC-8530DN */
- 0x04f903a0, /* MFC-L5700DN */
- 0x04f9039f, /* DCP-L6600DW */
- 0x04f9039e, /* DCP-L5652DN */
- 0x04f9039d, /* DCP-L5650DN */
- 0x04f9039c, /* DCP-L5602DN */
- 0x04f9039b, /* DCP-L5600DN */
- 0x04f9039a, /* DCP-L5502DN */
- 0x04f90399, /* DCP-L5500DN */
- 0x04f90398, /* DCP-L5500D */
- 0x04f903ba, /* ADS-3600W */
- 0x04f903b9, /* ADS-2800W */
- 0x04f903b8, /* ADS-3000N */
- 0x04f903b7, /* ADS-2400N */
- 0x04f903ca, /* DCP-J983N */
- 0x04f903c9, /* MFC-J985DW */
- 0x04f903c8, /* DCP-J785DW */
- 0x04f903f2, /* MFC-J997DN */
- 0x04f903f1, /* MFC-J907DN */
- 0x04f903f0, /* MFC-J887N */
- 0x04f903ef, /* MFC-J837DN */
- 0x04f903ee, /* MFC-J737DN */
- 0x04f903ed, /* DCP-J968N */
- 0x04f903eb, /* DCP-J767N */
- 0x04f903ea, /* DCP-J567N */
- 0x04f903e8, /* MFC-J5830DW */
- 0x04f903e7, /* MFC-J2730DW */
- 0x04f903e6, /* MFC-J2330DW */
- 0x04f903e5, /* MFC-J5335DW */
- 0x04f903e4, /* MFC-J6535DW */
- 0x04f903e3, /* MFC-J3930DW */
- 0x04f903e2, /* MFC-J3530DW */
- 0x04f903e0, /* MFC-J6530DW */
- 0x04f903d6, /* MFC-J5930DW */
- 0x04f903d5, /* MFC-J5730DW */
- 0x04f903d3, /* MFC-J5330DW */
- 0x04f903d1, /* MFC-J6995CDW */
- 0x04f903d0, /* MFC-J6980CDW */
- 0x04f903cf, /* MFC-J6580CDW */
- 0x04f903cd, /* MFC-J6730DW */
- 0x04f903cc, /* MFC-J6935DW */
- 0x04f903cb, /* MFC-J6930DW */
- 0x04f903f7, /* DCP-L8410CDW */
- 0x04f903f6, /* MFC-L8610CDW */
- 0x04f903f5, /* MFC-L8690CDW */
- 0x04f903f4, /* MFC-L8900CDW */
- 0x04f903f3, /* MFC-L9570CDW */
- 0x04f903fa, /* MFC-L2685DW */
- 0x04f903e1, /* MFC-L2707DW */
- 0x04f90290, /* MFC-J432W */
- 0x04f9028f, /* MFC-J425W */
- 0x04f9028d, /* MFC-J835DW */
- 0x04f9028a, /* DCP-J925N */
- 0x04f90284, /* MFC-J825N */
- 0x04f90283, /* MFC-J825DW */
- 0x04f90282, /* MFC-J625DW */
- 0x04f90281, /* MFC-J430W */
- 0x04f9027e, /* MFC-J955DN */
- 0x04f9027d, /* DCP-J925DW */
- 0x04f9027c, /* DCP-J725N */
- 0x04f9027b, /* DCP-J725DW */
- 0x04f90280, /* MFC-J435W */
- 0x04f9027a, /* DCP-J525N */
- 0x04f90279, /* DCP-J525W */
- 0x04f9027f, /* MFC-J280W */
- 0x04f90285, /* MFC-J705D */
- 0x04f9029a, /* MFC-8690DW */
- 0x04f9029f, /* MFC-9325CW */
- 0x04f9029e, /* MFC-9125CN */
- 0x04f90289, /* MFC-J5910CDW */
- 0x04f90288, /* MFC-J5910DW */
- 0x04f9043d, /* DCP-L2535DW */
- 0x04f9043c, /* MFC-L2715DW */
- 0x04f9043b, /* MFC-L2770DW */
- 0x04f9043a, /* MFC-L2750DW */
- 0x04f90439, /* MFC-L2730DW */
- 0x04f90438, /* MFC-L2730DN */
- 0x04f90437, /* MFC-L2717DW */
- 0x04f90436, /* MFC-L2715D */
- 0x04f90435, /* MFC-L2713DW */
- 0x04f90434, /* MFC-L2710DW */
- 0x04f90433, /* MFC-L2710DN */
- 0x04f90432, /* MFC-L2690DW */
- 0x04f90431, /* MFC-B7720DN */
- 0x04f90430, /* MFC-B7715DW */
- 0x04f9042e, /* MFC-B7700D */
- 0x04f9042d, /* MFC-7895DW */
- 0x04f9042c, /* MFC-7890DN */
- 0x04f9042b, /* MFC-7490D */
- 0x04f9042a, /* MFC-7390 */
- 0x04f90429, /* HL-L2395DW */
- 0x04f90428, /* HL-L2390DW */
- 0x04f90427, /* FAX-L2710DN */
- 0x04f90425, /* DCP-L2551DN */
- 0x04f90424, /* DCP-L2550DW */
- 0x04f90423, /* DCP-L2550DN */
- 0x04f90422, /* DCP-L2537DW */
- 0x04f90421, /* DCP-L2535D */
- 0x04f90420, /* DCP-L2530DW */
- 0x04f9041f, /* DCP-L2510D */
- 0x04f9041e, /* DCP-B7535DW */
- 0x04f9041d, /* DCP-B7530DN */
- 0x04f9041c, /* DCP-B7520DW */
- 0x04f9041b, /* DCP-B7500D */
- 0x04f9041a, /* DCP-7195DW */
- 0x04f90419, /* DCP-7190DN */
- 0x04f90418, /* DCP-7095D */
- 0x04f90417, /* DCP-7090 */
- 0x04f90413, /* MFC-T910DW */
- 0x04f90412, /* MFC-T810W */
- 0x04f90411, /* DCP-T710W */
- 0x04f90410, /* DCP-T510W */
- 0x04f9040f, /* DCP-T310 */
- 0x04f90408, /* MFC-J893N */
- 0x04f90407, /* DCP-J973N */
- 0x04f90406, /* DCP-J972N */
- 0x04f90405, /* DCP-J572N */
- 0x04f90404, /* MFC-J690DW */
- 0x04f90403, /* MFC-J890DW */
- 0x04f90400, /* DCP-J774DW */
- 0x04f903ff, /* DCP-J772DW */
- 0x04f903f8, /* MFC-J895DW */
- 0x04f9043e, /* MFC-J775DW */
- 0x04f9040e, /* MFC-J1500N */
- 0x04f9040d, /* DCP-J988N */
- 0x04f9040b, /* MFC-J1300DW */
- 0x04f9040a, /* MFC-J995DW */
- 0x04f90409, /* DCP-J1100DW */
- 0x04f90402, /* MFC-J497DW */
- 0x04f903fe, /* DCP-J572DW */
- 0x04f903f9, /* MFC-J491DW */
- 0x04f9044b, /* DCP-L3510CDW */
- 0x04f9044a, /* HL-L3290CDW */
- 0x04f90448, /* DCP-L3550CDW */
- 0x04f90446, /* MFC-L3710CW */
- 0x04f90445, /* MFC-L3730CDN */
- 0x04f90442, /* MFC-L3745CDW */
- 0x04f90441, /* MFC-L3750CDW */
- 0x04f9043f, /* MFC-L3770CDW */
- 0x04f90454, /* MFC-T4500DW */
- 0x04f9044f, /* MFC-J6545DW */
- 0x04f9044d, /* MFC-J5845DW */
- 0x04f90462, /* MFC-J898N */
- 0x04f90461, /* DCP-J978N */
- 0x04f90460, /* DCP-J577N */
- 0x04f9044c, /* DCP-L3551CDW */
- 0x04f90443, /* MFC-L3735CDN */
- 0x04f9045f, /* HL-J6000CDW */
- 0x04f90457, /* MFC-J6999CDW */
- 0x04f90456, /* MFC-J6997CDW */
- 0x04f90453, /* HL-J6100DW */
- 0x04f90452, /* HL-J6000DW */
- 0x04f90451, /* MFC-J6947DW */
- 0x04f90450, /* MFC-J6945DW */
- 0x04f9044e, /* MFC-J5945DW */
- 0x04f90466, /* MFC-J815DW */
- 0x04f90465, /* MFC-J1605DN */
- 0x04f90464, /* MFC-J998DN */
- 0x04f90463, /* MFC-J738DN */
- 0x04f90447, /* DCP-9030CDN */
- 0x04f90444, /* MFC-9150CDN */
- 0x04f90440, /* MFC-9350CDW */
- 0x04f9045e, /* MFC-J6983CDW */
- 0x04f9045d, /* MFC-J6583CDW */
- 0x04f9045c, /* MFC-J5630CDW */
- 0x04f90470, /* MFC-J903N */
- 0x04f9046f, /* DCP-J982N */
- 0x04f9046e, /* DCP-J981N */
- 0x04f9046d, /* DCP-J582N */
- 0x04f90467, /* MFC-J805DW */
- 0x04f960a0, /* ADS-2000 */
- 0x04f960a1, /* ADS-2100 */
- 0x04f90293, /* DCP-8155DN */
- 0x04f902b7, /* DCP-8157DN */
- 0x04f90294, /* DCP-8250DN */
- 0x04f90296, /* MFC-8520DN */
- 0x04f90298, /* MFC-8910DW */
- 0x04f902ba, /* MFC-8912DW */
- 0x04f90299, /* MFC-8950DW */
- 0x04f902bb, /* MFC-8952DW */
- 0x04f902d4, /* MFC-8810DW */
- 0x04f90291, /* DCP-8110DN */
- 0x04f902ac, /* DCP-8110D */
- 0x04f902b5, /* DCP-8112DN */
- 0x04f90292, /* DCP-8150DN */
- 0x04f902b6, /* DCP-8152DN */
- 0x04f90295, /* MFC-8510DN */
- 0x04f902b8, /* MFC-8512DN */
- 0x04f9029c, /* MFC-8515DN */
- 0x04f902cb, /* MFC-8710DW */
- 0x04f902ca, /* MFC-8712DW */
- 0x04f902a6, /* FAX-2940 */
- 0x04f902a7, /* FAX-2950 */
- 0x04f902ab, /* FAX-2990 */
- 0x04f902a5, /* MFC-7240 */
- 0x04f902a8, /* MFC-7290 */
- 0x04f902a0, /* DCP-J140W */
- 0x04f902c1, /* MFC-J960DN */
- 0x04f902c0, /* DCP-J940N */
- 0x04f902bf, /* MFC-J840N */
- 0x04f902be, /* MFC-J710D */
- 0x04f902bd, /* DCP-J740N */
- 0x04f902bc, /* DCP-J540N */
- 0x04f902b2, /* MFC-J810DN */
- 0x04f90287, /* MFC-J860DN */
- 0x04f902cf, /* DCP-7057W */
- 0x04f902ce, /* DCP-7055W */
- 0x04f902cd, /* MFC-J2510 */
- 0x04f902c7, /* MFC-J4510N */
- 0x04f902c6, /* DCP-J4210N */
- 0x04f902c5, /* MFC-J4610DW */
- 0x04f902c4, /* MFC-J4410DW */
- 0x04f902b4, /* MFC-J4710DW */
- 0x04f902b3, /* MFC-J4510DW */
- 0x04f902c2, /* DCP-J4110DW */
- 0x04f960a4, /* ADS-2500W */
- 0x04f960a5, /* ADS-2600W */
- 0x04f902cc, /* MFC-J2310 */
- 0x04f902c8, /* MFC-J4910CDW */
- 0x04f902c3, /* MFC-J4310DW */
- 0x04f902d3, /* DCP-9020CDW */
- 0x04f902b1, /* DCP-9020CDN */
- 0x04f902b0, /* MFC-9340CDW */
- 0x04f902af, /* MFC-9330CDW */
- 0x04f902ae, /* MFC-9140CDN */
- 0x04f902ad, /* MFC-9130CW */
- 0x04f902d1, /* MFC-1810 */
- 0x04f902d0, /* DCP-1510 */
- 0x04f902fb, /* MFC-J875DW */
- 0x04f902f1, /* MFC-J890DN */
- 0x04f902f0, /* MFC-J980DN */
- 0x04f902ef, /* MFC-J820DN */
- 0x04f902ed, /* MFC-J870N */
- 0x04f902ec, /* MFC-J870DW */
- 0x04f902ee, /* MFC-J720D */
- 0x04f902eb, /* MFC-J650DW */
- 0x04f902e9, /* MFC-J475DW */
- 0x04f902e8, /* MFC-J470DW */
- 0x04f902fa, /* MFC-J450DW */
- 0x04f902ea, /* MFC-J285DW */
- 0x04f902e6, /* DCP-J952N */
- 0x04f902e5, /* DCP-J752N */
- 0x04f902e4, /* DCP-J752DW */
- 0x04f902e3, /* DCP-J552N */
- 0x04f902e2, /* DCP-J552DW */
- 0x04f902f9, /* DCP-J132N */
- 0x04f902de, /* DCP-J132W */
- 0x04f902e0, /* DCP-J152N */
- 0x04f902df, /* DCP-J152W */
- 0x04f902e1, /* DCP-J172W */
- 0x04f902e7, /* MFC-J245 */
- 0x04f902fc, /* DCP-J100 */
- 0x04f902fd, /* DCP-J105 */
- 0x04f902fe, /* MFC-J200 */
- 0x04f902dd, /* DCP-J4215N */
- 0x04f902c9, /* MFC-J4810DN */
- 0x04f902ff, /* MFC-J3520 */
- 0x04f90300, /* MFC-J3720 */
- 0x04f902f2, /* MFC-J6520DW */
- 0x04f902f3, /* MFC-J6570CDW */
- 0x04f902f4, /* MFC-J6720DW */
- 0x04f902f8, /* MFC-J6770CDW */
- 0x04f902f5, /* MFC-J6920DW */
- 0x04f902f6, /* MFC-J6970CDW */
- 0x04f902f7, /* MFC-J6975CDW */
- 0x04f90318, /* MFC-7365DN */
- 0x04f960a6, /* ADS-1000W */
- 0x04f960a7, /* ADS-1100W */
- 0x04f960a8, /* ADS-1500W */
- 0x04f960a9, /* ADS-1600W */
- };
-
- /* Taken from backend/pixma/pixma_mp150.c pixma_mp730.c pixma_mp750.c pixma_mp800.c in the pixma SANE backend repository */
- /* Canon Pixma IDs extracted using the following Python script
- import sys
- for f in sys.argv:
- for l in open(f, "r").readlines():
- tokens=l.split ()
- if len (tokens) >= 3 and tokens[0].startswith("#define") and tokens[1].endswith("_PID") and tokens[2].startswith("0x") and not tokens[2].endswith("ffff"):
- print ( "0x04a9" + tokens[2][2:] + ", /* " + tokens[1][:-4] + " * /")
- */
- private const uint32 pixma_devices[] = {
- 0x04a91709, /* MP150 */
- 0x04a9170a, /* MP170 */
- 0x04a9170b, /* MP450 */
- 0x04a9170c, /* MP500 */
- 0x04a91712, /* MP530 */
- 0x04a91714, /* MP160 */
- 0x04a91715, /* MP180 */
- 0x04a91716, /* MP460 */
- 0x04a91717, /* MP510 */
- 0x04a91718, /* MP600 */
- 0x04a91719, /* MP600R */
- 0x04a9172b, /* MP140 */
- 0x04a9171c, /* MX7600 */
- 0x04a91721, /* MP210 */
- 0x04a91722, /* MP220 */
- 0x04a91723, /* MP470 */
- 0x04a91724, /* MP520 */
- 0x04a91725, /* MP610 */
- 0x04a91727, /* MX300 */
- 0x04a91728, /* MX310 */
- 0x04a91729, /* MX700 */
- 0x04a9172c, /* MX850 */
- 0x04a9172e, /* MP630 */
- 0x04a9172f, /* MP620 */
- 0x04a91730, /* MP540 */
- 0x04a91731, /* MP480 */
- 0x04a91732, /* MP240 */
- 0x04a91733, /* MP260 */
- 0x04a91734, /* MP190 */
- 0x04a91735, /* MX860 */
- 0x04a91736, /* MX320 */
- 0x04a91737, /* MX330 */
- 0x04a9173a, /* MP250 */
- 0x04a9173b, /* MP270 */
- 0x04a9173c, /* MP490 */
- 0x04a9173d, /* MP550 */
- 0x04a9173e, /* MP560 */
- 0x04a9173f, /* MP640 */
- 0x04a91741, /* MX340 */
- 0x04a91742, /* MX350 */
- 0x04a91743, /* MX870 */
- 0x04a91746, /* MP280 */
- 0x04a91747, /* MP495 */
- 0x04a91748, /* MG5100 */
- 0x04a91749, /* MG5200 */
- 0x04a9174a, /* MG6100 */
- 0x04a9174d, /* MX360 */
- 0x04a9174e, /* MX410 */
- 0x04a9174f, /* MX420 */
- 0x04a91750, /* MX880 */
- 0x04a91751, /* MG2100 */
- 0x04a91752, /* MG3100 */
- 0x04a91753, /* MG4100 */
- 0x04a91754, /* MG5300 */
- 0x04a91755, /* MG6200 */
- 0x04a91757, /* MP493 */
- 0x04a91758, /* E500 */
- 0x04a91759, /* MX370 */
- 0x04a9175B, /* MX430 */
- 0x04a9175C, /* MX510 */
- 0x04a9175D, /* MX710 */
- 0x04a9175E, /* MX890 */
- 0x04a9175A, /* E600 */
- 0x04a91763, /* MG4200 */
- 0x04a9175F, /* MP230 */
- 0x04a91765, /* MG6300 */
- 0x04a91760, /* MG2200 */
- 0x04a91761, /* E510 */
- 0x04a91762, /* MG3200 */
- 0x04a91764, /* MG5400 */
- 0x04a91766, /* MX390 */
- 0x04a91767, /* E610 */
- 0x04a91768, /* MX450 */
- 0x04a91769, /* MX520 */
- 0x04a9176a, /* MX720 */
- 0x04a9176b, /* MX920 */
- 0x04a9176c, /* MG2400 */
- 0x04a9176d, /* MG2500 */
- 0x04a9176e, /* MG3500 */
- 0x04a9176f, /* MG6500 */
- 0x04a91770, /* MG6400 */
- 0x04a91771, /* MG5500 */
- 0x04a91772, /* MG7100 */
- 0x04a91774, /* MX470 */
- 0x04a91775, /* MX530 */
- 0x04a91776, /* MB5000 */
- 0x04a91777, /* MB5300 */
- 0x04a91778, /* MB2000 */
- 0x04a91779, /* MB2300 */
- 0x04a9177a, /* E400 */
- 0x04a9177b, /* E560 */
- 0x04a9177c, /* MG7500 */
- 0x04a9177e, /* MG6600 */
- 0x04a9177f, /* MG5600 */
- 0x04a91780, /* MG2900 */
- 0x04a91788, /* E460 */
- 0x04a91787, /* MX490 */
- 0x04a91789, /* E480 */
- 0x04a9178a, /* MG3600 */
- 0x04a9178b, /* MG7700 */
- 0x04a9178c, /* MG6900 */
- 0x04a9178d, /* MG6800 */
- 0x04a9178e, /* MG5700 */
- 0x04a91792, /* MB2700 */
- 0x04a91793, /* MB2100 */
- 0x04a91794, /* G3000 */
- 0x04a91795, /* G2000 */
- 0x04a9179f, /* TS9000 */
- 0x04a91800, /* TS8000 */
- 0x04a91801, /* TS6000 */
- 0x04a91802, /* TS5000 */
- 0x04a9180b, /* MG3000 */
- 0x04a9180c, /* E470 */
- 0x04a9181e, /* E410 */
- 0x04a9181d, /* G4000 */
- 0x04a91822, /* TS6100 */
- 0x04a91825, /* TS5100 */
- 0x04a91827, /* TS3100 */
- 0x04a91828, /* E3100 */
- 0x04a9178f, /* MB5400 */
- 0x04a91790, /* MB5100 */
- 0x04a91820, /* TS9100 */
- 0x04a91823, /* TR8500 */
- 0x04a91824, /* TR7500 */
- 0x04a9185c, /* TS9500 */
- 0x04a91912, /* LIDE400 */
- 0x04a91913, /* LIDE300 */
- 0x04a91821, /* TS8100 */
- 0x04a9183a, /* G2010 */
- 0x04a9183b, /* G3010 */
- 0x04a9183d, /* G4010 */
- 0x04a9183e, /* TS9180 */
- 0x04a9183f, /* TS8180 */
- 0x04a91840, /* TS6180 */
- 0x04a91841, /* TR8580 */
- 0x04a91842, /* TS8130 */
- 0x04a91843, /* TS6130 */
- 0x04a91844, /* TR8530 */
- 0x04a91845, /* TR7530 */
- 0x04a91846, /* XK50 */
- 0x04a91847, /* XK70 */
- 0x04a91854, /* TR4500 */
- 0x04a91855, /* E4200 */
- 0x04a91856, /* TS6200 */
- 0x04a91857, /* TS6280 */
- 0x04a91858, /* TS6230 */
- 0x04a91859, /* TS8200 */
- 0x04a9185a, /* TS8280 */
- 0x04a9185b, /* TS8230 */
- 0x04a9185d, /* TS9580 */
- 0x04a9185e, /* TR9530 */
- 0x04a91863, /* G7000 */
- 0x04a91865, /* G6000 */
- 0x04a91866, /* G6080 */
- 0x04a91869, /* GM4000 */
- 0x04a91873, /* XK80 */
- 0x04a9188b, /* TS5300 */
- 0x04a9188c, /* TS5380 */
- 0x04a9188d, /* TS6300 */
- 0x04a9188e, /* TS6380 */
- 0x04a9188f, /* TS7330 */
- 0x04a91890, /* TS8300 */
- 0x04a91891, /* TS8380 */
- 0x04a91892, /* TS8330 */
- 0x04a91893, /* XK60 */
- 0x04a91894, /* TS6330 */
- 0x04a918a2, /* TS3300 */
- 0x04a918a3, /* E3300 */
- 0x04a9261f, /* MP10 */
- 0x04a9262f, /* MP730 */
- 0x04a92630, /* MP700 */
- 0x04a92635, /* MP5 */
- 0x04a9263c, /* MP360 */
- 0x04a9263d, /* MP370 */
- 0x04a9263e, /* MP390 */
- 0x04a9263f, /* MP375R */
- 0x04a9264c, /* MP740 */
- 0x04a9264d, /* MP710 */
- 0x04a9265d, /* MF5730 */
- 0x04a9265e, /* MF5750 */
- 0x04a9265f, /* MF5770 */
- 0x04a92660, /* MF3110 */
- 0x04a926e6, /* IR1020 */
- 0x04a91706, /* MP750 */
- 0x04a91708, /* MP760 */
- 0x04a91707, /* MP780 */
- 0x04a9170d, /* MP800 */
- 0x04a9170e, /* MP800R */
- 0x04a91713, /* MP830 */
- 0x04a9171a, /* MP810 */
- 0x04a9171b, /* MP960 */
- 0x04a91726, /* MP970 */
- 0x04a91901, /* CS8800F */
- 0x04a9172d, /* MP980 */
- 0x04a91740, /* MP990 */
- 0x04a91908, /* CS9000F */
- 0x04a9174b, /* MG8100 */
- 0x04a91756, /* MG8200 */
- 0x04a9190d, /* CS9000F_MII */
- };
-
- /* Taken from uld/noarch/oem.conf in the Samsung SANE driver */
- private const uint32 samsung_devices[] = { 0x04e83425, 0x04e8341c, 0x04e8342a, 0x04e8343d, 0x04e83456, 0x04e8345a, 0x04e83427, 0x04e8343a, 0x04e83428, 0x04e8343b, 0x04e83455, 0x04e83421, 0x04e83439, 0x04e83444, 0x04e8343f, 0x04e8344e, 0x04e83431, 0x04e8345c, 0x04e8344d, 0x04e83462, 0x04e83464, 0x04e83461, 0x04e83460, 0x04e8340e, 0x04e83435,
- 0x04e8340f, 0x04e83441, 0x04e8344f, 0x04e83413, 0x04e8341b, 0x04e8342e, 0x04e83426, 0x04e8342b, 0x04e83433, 0x04e83440, 0x04e83434, 0x04e8345b, 0x04e83457, 0x04e8341f, 0x04e83453, 0x04e8344b, 0x04e83409, 0x04e83412, 0x04e83419, 0x04e8342c, 0x04e8343c, 0x04e83432, 0x04e8342d, 0x04e83430, 0x04e8342f,
- 0x04e83446, 0x04e8341a, 0x04e83437, 0x04e83442, 0x04e83466, 0x04e8340d, 0x04e8341d, 0x04e83420, 0x04e83429, 0x04e83443, 0x04e83438, 0x04e8344c, 0x04e8345d, 0x04e83463, 0x04e83465, 0x04e83450, 0x04e83468, 0x04e83469, 0x04e83467, 0x04e8346b, 0x04e8346a, 0x04e8346e, 0x04e83471, 0x04e83472, 0x04e8347d,
- 0x04e8347c, 0x04e8347e, 0x04e83481, 0x04e83482, 0x04e83331, 0x04e83332, 0x04e83483, 0x04e83484, 0x04e83485, 0x04e83478, 0x04e83325, 0x04e83327, 0x04e8346f, 0x04e83477, 0x04e83324, 0x04e83326, 0x04e83486, 0x04e83487, 0x04e83489
- };
-
- /* Taken from uld/noarch/oem.conf in the HP/Samsung SANE driver
- These devices are rebranded Samsung Multifunction Printers. */
- private const uint32 smfp_devices[] = { 0x03F0AA2A, 0x03F0CE2A, 0x03F0C02A, 0x03F0EB2A, 0x03F0F22A };
-
- /* Taken from /usr/share/hplip/data/models/models.dat in the HPAIO driver */
- private const uint32 hpaio_devices[] = {
- 0x04f92311, /* HP Officejet d125xi All-in-One Printer */
- 0x04f99711, /* HP Photosmart All-in-One Printer - B010 */
- 0x04f91311, /* HP Officejet v30 All-in-One Printer */
- 0x04f91011, /* HP Officejet v40xi All-in-One Printer */
- 0x04f90f11, /* HP Officejet v40 All-in-One Printer */
- 0x04f91911, /* HP Officejet v45 All-in-One Printer */
- 0x04f90011, /* HP Officejet g55 All-in-One Printer */
- 0x04f90111, /* HP Officejet g55xi All-in-One Printer */
- 0x04f90611, /* HP Officejet k60xi All-in-One Printer */
- 0x04f90511, /* HP Officejet k60 All-in-One Printer */
- 0x04f90811, /* HP Officejet k80xi All-in-One Printer */
- 0x04f90711, /* HP Officejet k80 All-in-One Printer */
- 0x04f90211, /* HP Officejet g85 All-in-One Printer */
- 0x04f90311, /* HP Officejet g85xi All-in-One Printer */
- 0x04f90411, /* HP Officejet g95 All-in-One Printer */
- 0x04f9062a, /* HP LaserJet 100 Color MFP M175 */
- 0x04f94912, /* HP Officejet 100 Mobile L411 */
- 0x04f99911, /* HP Envy 100 D410 series */
- 0x04f93802, /* HP Photosmart 100 Printer */
- 0x04f97a11, /* HP Photosmart B109A series */
- 0x04f98311, /* HP Deskjet Ink Advantage K109a Printer */
- 0x04f97b11, /* HP Photosmart Wireless All-in-One Printer - B109n */
- 0x04f9a711, /* HP Envy 110 e-All-in-One */
- 0x04f98d11, /* HP Photosmart D110 Series Printer */
- 0x04f98a11, /* HP Photosmart Wireless All-in-One Printer - B110 */
- 0x04f9bb11, /* HP Envy 120 e-All-in-One */
- 0x04f9222a, /* HP LaserJet Pro MFP M125r */
- 0x04f9322a, /* HP LaserJet Pro MFP M127fp */
- 0x04f93902, /* HP Photosmart 130 Printer */
- 0x04f91002, /* HP Photosmart 140 Compact Photo Printer */
- 0x04f9242a, /* HP Color LaserJet Pro MPF M176n */
- 0x04f9332a, /* HP Color LaserJet Pro MPF M177fw */
- 0x04f9122a, /* HP LaserJet Pro 200 color MFP M276nw */
- 0x04f90c2a, /* HP LaserJet 200 Color MFP M275s */
- 0x04f9132a, /* HP LaserJet Pro M251nw Color Printer */
- 0x04f92c2a, /* HP LaserJet Pro M201dw Printer */
- 0x04f97e11, /* HP Photosmart Plus All-in-One Printer - B209a */
- 0x04f97811, /* HP Deskjet Ink Advantage K209a All-in-One Printer */
- 0x04f98e11, /* HP Photosmart Plus B210 series */
- 0x04f92d2a, /* HP LaserJet Pro MFP M225rdn */
- 0x04f93502, /* HP Photosmart 230 Printer */
- 0x04f91102, /* HP Photosmart 240 Compact Photo Printer */
- 0x04f96112, /* HP Officejet Pro 251dw Printer */
- 0x04f96212, /* HP Officejet Pro 276dw Multifunction Printer */
- 0x04f95511, /* HP Deskjet F310 All-in-One Printer */
- 0x04f90f2a, /* HP LaserJet 300 Color M351a */
- 0x04f9082a, /* HP LaserJet 300 Color MFP M375nw */
- 0x04f97311, /* HP Photosmart Premium Fax All-in-One Printer - C309a */
- 0x04f97c11, /* HP Photosmart Premium Fax All-in-One Printer series -C309a */
- 0x04f97d11, /* HP Photosmart Premium All-in-One Printer series - C309g */
- 0x04f91d02, /* HP Photosmart A310 Compact Photo Printer */
- 0x04f91202, /* HP Photosmart 320 Compact Photo Printer */
- 0x04f91e02, /* HP Photosmart A320 Compact Photo Printer */
- 0x04f91602, /* HP Photosmart 330 Series Compact Photo Printer */
- 0x04f91302, /* HP Photosmart 370 Compact Photo Printer */
- 0x04f91702, /* HP Photosmart 385 Compact Photo Printer */
- 0x04f9152a, /* HP LaserJet 400 M401dne */
- 0x04f9142a, /* HP LaserJet 400 MFP M425dw */
- 0x04f99611, /* HP Photosmart Prem C410 series */
- 0x04f91502, /* HP Photosmart 420 Compact Photo Printer */
- 0x04f91902, /* HP Photosmart A430 Compact Photo Printer */
- 0x04f91f02, /* HP Photosmart A440 Compact Photo Printer */
- 0x04f90512, /* HP Deskjet 450ci Mobile Printer */
- 0x04f9aa11, /* HP Officejet Pro X451 Printer series */
- 0x04f9a311, /* HP Officejet Pro X451dn Printer */
- 0x04f91312, /* HP Deskjet 460c Mobile Printer */
- 0x04f91802, /* HP Photosmart 470 Series Compact Photo Printer */
- 0x04f92812, /* HP Officejet H470 Mobile Printer */
- 0x04f9bf11, /* HP Officejet Pro X476 Multifunction Printer series */
- 0x04f9c011, /* HP Officejet Pro X476dw Multifunction Printer */
- 0x04f9342a, /* HP Color Laserjet Pro MFP M476dw */
- 0x04f99e17, /* HP LaserJet Enterprise 500 MFP M525 Series */
- 0x04f99f17, /* HP LaserJet Enterprise 500 Color MFP M575 */
- 0x04f9252a, /* HP LaserJet Pro 500 color MFP M570dw */
- 0x04f9a417, /* HP LaserJet Enterprise 500 Color M551 */
- 0x04f91a02, /* HP Photosmart A510 Compact Photo Printer */
- 0x04f99e11, /* HP Photosmart Ink Adv K510 */
- 0x04f99011, /* HP PhotoSmart eStn C510 Series */
- 0x04f92602, /* HP Photosmart A522xi Compact Photo Printer */
- 0x04f9272a, /* HP LaserJet Pro M521dn Multifunction Printer */
- 0x04f92b02, /* HP Photosmart A532 Compact Photo Printer */
- 0x04f91812, /* HP Officejet Pro K550dtwn Printer */
- 0x04f9b211, /* HP Officejet Pro X551 Printer series */
- 0x04f9352a, /* HP Officejet Enterprise Color X555dn Printer */
- 0x04f92b2a, /* HP Officejet Enterprise Color X585dn Multifunction Printer */
- 0x04f9362a, /* HP Officejet Enterprise Color Flow X585z Multifunction Printer */
- 0x04f9a517, /* HP LaserJet Enterprise 600 M601n */
- 0x04f91b02, /* HP Photosmart A610 Compact Photo Printer */
- 0x04f92702, /* HP Photosmart A620 Compact Photo Printer */
- 0x04f92c02, /* HP Photosmart A636 Compact Photo Printer */
- 0x04f9282a, /* HP LaserJet Enterprise MFP M630dn */
- 0x04f92104, /* HP Deskjet 630c Printer */
- 0x04f9432a, /* HP LaserJet Enterprise Flow MFP M630z */
- 0x04f92004, /* HP Deskjet 640c Lite Printer */
- 0x04f91a2a, /* HP Color LaserJet Enterprise M651dn Printer */
- 0x04f92304, /* HP Deskjet 656c Printer */
- 0x04f91b2a, /* HP Color LaserJet Enterprise Multifunction M680dn Printer */
- 0x04f9442a, /* HP Color LaserJet Enterprise Flow Multifunction M680z Printer */
- 0x04f98904, /* HP Deskjet 694c Printer */
- 0x04f9a617, /* HP LaserJet Enterprise 700 M712n */
- 0x04f99a17, /* HP LaserJet Enterprise 700 color MFP M775dn */
- 0x04f9312a, /* HP LaserJet Pro M701a Printer */
- 0x04f9452a, /* HP LaserJet Pro M706n Printer */
- 0x04f91c02, /* HP Photosmart A712 Compact Photo Printer */
- 0x04f91811, /* HP PSC 720 All-in-One Printer */
- 0x04f99d17, /* HP LaserJet Enterprise MFP M725 series */
- 0x04f92804, /* HP Deskjet D730 Printer */
- 0x04f92904, /* HP Deskjet F735 All-in-One Printer */
- 0x04f91511, /* HP PSC 750xi All-in-One Printer */
- 0x04f91411, /* HP PSC 750 All-in-One Printer */
- 0x04f9372a, /* HP Color LaserJet Enterprise M750 Printer series */
- 0x04f90d14, /* HP Designjet T770 24-in Postscript Printer */
- 0x04f91611, /* HP PSC 780 All-in-One Printer */
- 0x04f91711, /* HP PSC 780xi All-in-One Printer */
- 0x04f90f14, /* HP Designjet T790ps 24in */
- 0x04f91f2a, /* HP LaserJet Enterprise M806 Printer Series */
- 0x04f90304, /* HP Deskjet 810c Printer */
- 0x04f90204, /* HP Deskjet 815c Printer */
- 0x04f90804, /* HP Deskjet 816 Printer */
- 0x04f92902, /* HP Photosmart A826 Home Photo Center */
- 0x04f90704, /* HP Deskjet 825cvr Printer */
- 0x04f91e2a, /* HP LaserJet Enterprise flow M830z Multifunction Printer */
- 0x04f90404, /* HP Deskjet 830c Printer */
- 0x04f90604, /* HP Deskjet 840c Printer */
- 0x04f90904, /* HP Deskjet 845c Printer */
- 0x04f91512, /* HP Officejet Pro K850 Printer */
- 0x04f91c2a, /* HP Color LaserJet Enterprise M855 Printer series */
- 0x04f90104, /* HP Deskjet 880c Printer */
- 0x04f91d2a, /* HP Color LaserJet Enterprise flow M880 Multifunction Printer series */
- 0x04f90004, /* HP Deskjet 895cse Printer */
- 0x04f92604, /* HP 910 Printer */
- 0x04f92704, /* HP 915 Inkjet All-in-One Printer */
- 0x04f91804, /* HP Deskjet 916c Printer */
- 0x04f91504, /* HP Deskjet 920c Printer */
- 0x04f91f11, /* HP PSC 920 All-in-One Printer */
- 0x04f91204, /* HP Deskjet 930c Printer */
- 0x04f91604, /* HP Deskjet 940cvr Printer */
- 0x04f91704, /* HP Deskjet 948c Printer */
- 0x04f91104, /* HP Deskjet 950c Printer */
- 0x04f91e11, /* HP PSC 950 All-in-One Printer */
- 0x04f91304, /* HP Deskjet 955c Printer */
- 0x04f91404, /* HP Deskjet 957c Printer */
- 0x04f93104, /* HP Deskjet 960cse Printer */
- 0x04f91004, /* HP Deskjet 970cxi Printer */
- 0x04f93004, /* HP Deskjet 980cxi Printer */
- 0x04f93304, /* HP Deskjet 990cxi Printer */
- 0x04f95004, /* HP Deskjet 995c Printer */
- 0x04f92e11, /* HP PSC 1000 Series */
- 0x04f90517, /* HP LaserJet 1000 Printer */
- 0x04f98811, /* HP Deskjet 1000 J110 Series */
- 0x04f91712, /* Business Inkjet 1000 Printer */
- 0x04f91317, /* HP LaserJet 1005 Printer */
- 0x04f94117, /* HP LaserJet P1005 Printer */
- 0x04f93217, /* HP LaserJet M1005 Multifunction Printer */
- 0x04f93e17, /* HP LaserJet P1009 Printer */
- 0x04f90c17, /* HP LaserJet 1010 Printer */
- 0x04f9b511, /* HP Deskjet 1010 Printer */
- 0x04f94217, /* HP Color LaserJet CM1015 Multifunction Printer */
- 0x04f94317, /* HP Color LaserJet CM1017 Multifunction Printer */
- 0x04f92b17, /* HP LaserJet 1020 Printer */
- 0x04f93017, /* HP LaserJet 1022nw Printer */
- 0x04f92d17, /* HP LaserJet 1022n Printer */
- 0x04f92c17, /* HP LaserJet 1022 Printer */
- 0x04f9112a, /* HP LaserJet Pro CP 1025nw Color Printer Series */
- 0x04f90b2a, /* HP LaserJet Pro CP1025nw Color Printer Series */
- 0x04f98911, /* HP Deskjet 1050 J410 All-in-One Printer */
- 0x04f97c04, /* HP Deskjet 1100c Printer */
- 0x04f90912, /* HP Business Inkjet 1100d Printer */
- 0x04f93011, /* HP PSC 1110 All-in-One Printer */
- 0x04f9032a, /* HP LaserJet Professional P1102w Printer */
- 0x04f9002a, /* HP Laserjet Professional P1102 Printer */
- 0x04f9102a, /* HP LaserJet Professional P 1102w Printer */
- 0x04f93402, /* HP Photosmart 1115 Printer */
- 0x04f95617, /* HP LaserJet M1120 Multifunction Printer */
- 0x04f95717, /* HP LaserJet M1120n Multifunction Printer */
- 0x04f9042a, /* HP LaserJet Professional M1132 Multifunction Printer */
- 0x04f90f17, /* HP LaserJet 1150 Printer */
- 0x04f94004, /* HP Color Inkjet cp1160 Printer */
- 0x04f91e17, /* HP LaserJet 1160 Series Printer */
- 0x04f90317, /* HP LaserJet 1200 Printer */
- 0x04f90f12, /* HP Business Inkjet 1200dtn Printer */
- 0x04f92f11, /* HP PSC 1200 All-in-One Printer */
- 0x04f9052a, /* HP LaserJet Professional M1212nf Multifunction Printer */
- 0x04f94717, /* HP Color LaserJet CP1215 Printer */
- 0x04f93202, /* HP Photosmart 1215 Printer */
- 0x04f90e2a, /* HP LaserJet Professional M1217nfW Multifunction Printer */
- 0x04f9262a, /* HP Laserjet M1210 MFP Series */
- 0x04f93302, /* HP Photosmart 1218 Printer */
- 0x04f90417, /* HP LaserJet 1220se All-in-One Printer */
- 0x04f90212, /* HP Deskjet 1220c Printer */
- 0x04f91412, /* HP Deskjet 1280 Printer */
- 0x04f91017, /* HP LaserJet 1300 Printer */
- 0x04f97804, /* HP Deskjet D1311 Printer */
- 0x04f93b11, /* HP PSC 1300 All-in-One Printer */
- 0x04f91117, /* HP LaserJet 1300n Printer */
- 0x04f93f11, /* HP PSC 1310 All-in-One Printer */
- 0x04f94f17, /* HP Color LaserJet CM1312nfi Multifunction Printer */
- 0x04f94e17, /* HP Color LaserJet CM1312 Multifunction Printer */
- 0x04f93602, /* HP Photosmart 1315 Printer */
- 0x04f95817, /* HP LaserJet M1319f Multifunction Printer */
- 0x04f91d17, /* HP LaserJet 1320 Series Printer */
- 0x04f93c11, /* HP PSC 1358 series */
- 0x04f97904, /* HP Deskjet D1415 Printer */
- 0x04f94d11, /* HP PSC 1401 All-in-One Printer */
- 0x04f9072a, /* HP LaserJet Professional CM1411fn */
- 0x04f94c11, /* HP PSC 1508 All-in-One Printer */
- 0x04f9c111, /* HP Deskjet 1510 All-in-One Printer */
- 0x04f94417, /* HP Color LaserJet CP1514n Printer */
- 0x04f95017, /* HP Color LaserJet CP1518ni Printer */
- 0x04f9022a, /* HP LaserJet Professional CP1521n */
- 0x04f9012a, /* HP LaserJet M1536dnf MFP */
- 0x04f9092a, /* HP LaserJet Professional P1566 */
- 0x04f97f11, /* HP Deskjet D1620 Printer */
- 0x04f94811, /* HP PSC 1600 All-in-One Printer */
- 0x04f93a17, /* HP Color LaserJet 1600 Printer */
- 0x04f90a2a, /* HP LaserJet Professional P1606dn Printer */
- 0x04f90312, /* HP Color Inkjet cp1700 Printer */
- 0x04f99411, /* HP Deskjet 2000 J210 series */
- 0x04f99b11, /* HP Deskjet Ink Adv 2010 K010 */
- 0x04f93917, /* HP LaserJet P2014 Printer */
- 0x04f94a17, /* HP LaserJet P2014n Printer */
- 0x04f93817, /* HP LaserJet P2015d Printer */
- 0x04f9b911, /* HP Deskjet Ink Advantage 2020HC Printer */
- 0x04f95417, /* HP Color LaserJet CP2025dn Printer */
- 0x04f95217, /* HP Color LaserJet CP2025 Printer */
- 0x04f95317, /* HP Color LaserJet CP2025n Printer */
- 0x04f95d17, /* HP LaserJet P2035n Printer */
- 0x04f98711, /* HP Deskjet 2050 J510 All-in-One Printer */
- 0x04f95c17, /* HP LaserJet P2055dn Printer */
- 0x04f99a11, /* HP Deskjet Ink Adv 2060 K110 */
- 0x04f92811, /* HP PSC 2105 All-in-One Printer */
- 0x04f97d04, /* HP Deskjet F2110 All-in-One Printer */
- 0x04f92a11, /* HP PSC 2150 All-in-One Printer */
- 0x04f92b11, /* HP PSC 2170 All-in-One Printer */
- 0x04f90217, /* HP LaserJet 2200 Series Printer */
- 0x04f92911, /* HP PSC 2200 All-in-One Printer */
- 0x04f92404, /* HP Deskjet F2210 All-in-One Printer */
- 0x04f93511, /* HP PSC 2300 Series All-in-One Printer */
- 0x04f90812, /* HP Business Inkjet 2300 Printer */
- 0x04f9c302, /* HP Deskjet D2320 Printer */
- 0x04f90b17, /* HP LaserJet 2300 Series Printer */
- 0x04f95917, /* HP Color LaserJet CM2320 Multifunction Printer */
- 0x04f95a17, /* HP Color LaserJet CM2320nf Multifunction Printer */
- 0x04f95b17, /* HP Color LaserJet CM2320fxi Multifunction Printer */
- 0x04f94911, /* HP PSC 2350 All-in-One Printer */
- 0x04f93611, /* HP PSC 2405 Photosmart All-in-One Printer */
- 0x04f97611, /* HP Deskjet F2410 All-in-One Printer */
- 0x04f97a04, /* HP Deskjet D2430 Printer */
- 0x04f92517, /* HP LaserJet 2410 Printer */
- 0x04f92917, /* HP LaserJet 2420 Printer */
- 0x04f92a17, /* HP LaserJet 2430t Printer */
- 0x04f91e04, /* HP 2500c Plus Printer */
- 0x04f90717, /* HP Color LaserJet 2500 Printer */
- 0x04f92504, /* HP Deskjet D2530 Printer */
- 0x04f93711, /* HP PSC 2500 Photosmart All-in-One Printer */
- 0x04f9ac11, /* HP Deskjet Ink Advantage 2510 All-in-One */
- 0x04f9be11, /* HP Deskjet Ink Advantage 2520HC All-in-One */
- 0x04f9c211, /* HP Deskjet 2540 All-in-One Printer */
- 0x04f91c17, /* HP Color LaserJet 2550L Printer */
- 0x04f94e11, /* HP Photosmart 2570 All-in-One Printer */
- 0x04f92e17, /* HP Color LaserJet 2600n Printer */
- 0x04f94511, /* HP Photosmart 2605 All-in-One Printer */
- 0x04f98011, /* HP Deskjet D2660 Printer */
- 0x04f90412, /* HP Business Inkjet 2600 Printer */
- 0x04f93617, /* HP Color LaserJet 2605dtn Printer */
- 0x04f92f17, /* HP Color LaserJet 2605 Printer */
- 0x04f93117, /* HP Color LaserJet 2605dn Printer */
- 0x04f9c911, /* HP Officejet 2620 All-in-One */
- 0x04f9ca11, /* HP Deskjet Ink Advantage 2645 All-in-One Printer */
- 0x04f94611, /* HP Photosmart 2710 All-in-One Printer */
- 0x04f93c17, /* HP Color LaserJet 2700n Printer */
- 0x04f93717, /* HP Color LaserJet 2700 Printer */
- 0x04f92617, /* HP Color LaserJet 2800 All-in-One Printer */
- 0x04f91112, /* HP Business Inkjet 2800 Printer */
- 0x04f90612, /* HP Business Inkjet 3000 Printer */
- 0x04f96717, /* HP Color LaserJet 3000 Printer */
- 0x04f99511, /* HP Deskjet 3000 j310 series */
- 0x04f97617, /* HP LaserJet P3004 Printer */
- 0x04f97317, /* HP LaserJet P3005 Printer */
- 0x04f98d17, /* HP LaserJet P3015 Printer */
- 0x04f91617, /* HP LaserJet 3015 All-in-One Printer */
- 0x04f97a17, /* HP LaserJet M3027 Multifunction Printer */
- 0x04f97517, /* HP LaserJet M3035 Multifunction Printer */
- 0x04f99311, /* HP Deskjet 3050 J610 series */
- 0x04f9a011, /* HP Deskjet 3050A J611 series */
- 0x04f93317, /* HP LaserJet 3052 All-in-One Printer */
- 0x04f93417, /* HP LaserJet 3055 All-in-One Printer */
- 0x04f9a211, /* HP Deskjet 3070 B611 series */
- 0x04f95611, /* HP Photosmart C3110 All-in-One Printer */
- 0x04f95011, /* HP Photosmart 3108 All-in-One Printer */
- 0x04f95111, /* HP Photosmart 3207 All-in-One Printer */
- 0x04f90117, /* HP LaserJet 3200 All-in-One Printer */
- 0x04f90817, /* HP LaserJet 3300 Multifunction Printer */
- 0x04f95211, /* HP Photosmart 3308 All-in-One Printer */
- 0x04f97004, /* HP Deskjet 3320v Color Inkjet Printer */
- 0x04f90917, /* HP LaserJet 3330 Multifunction Printer */
- 0x04f91917, /* HP LaserJet 3380 All-in-One Printer */
- 0x04f93517, /* HP LaserJet 3390 All-in-One Printer */
- 0x04f97104, /* HP Deskjet 3420 Color Inkjet Printer */
- 0x04f91517, /* HP Color LaserJet 3500n Printer */
- 0x04f93112, /* HP Officejet J3508 All-in-One Printer */
- 0x04f97817, /* HP Color LaserJet CP3505n Printer */
- 0x04f9ad11, /* HP Deskjet Ink Advantage 3515 e-All-in-One */
- 0x04f9b011, /* HP Deskjet 3520 e-All-in-One Series */
- 0x04f98517, /* HP Color LaserJet CP3525 Printer */
- 0x04f98a17, /* HP Color LaserJet CM3530 Multifunction Printer */
- 0x04f9c711, /* HP Deskjet Ink Advantage 3540 e-All-in-One Printer Series */
- 0x04f96117, /* HP Color LaserJet 3550 Printer */
- 0x04f96917, /* HP Color LaserJet 3600 Printer */
- 0x04f96812, /* HP Officejet Pro 3610 Black and White e-All-in-One */
- 0x04f96d12, /* HP Officejet Pro 3620 Black and White e-All-in-One */
- 0x04f97204, /* HP Deskjet 3650 Color Inkjet Printer */
- 0x04f90a17, /* HP Color LaserJet 3700 Printer */
- 0x04f97404, /* HP Deskjet 3740 Color Inkjet Printer */
- 0x04f96817, /* HP Color LaserJet 3800 Printer */
- 0x04f91b04, /* HP Deskjet 3810 Color Inkjet Printer */
- 0x04f91a04, /* HP Deskjet 3816 Color Inkjet Printer */
- 0x04f91c04, /* HP Deskjet 3819 Color Inkjet Printer */
- 0x04f91904, /* HP Deskjet 3820 Color Inkjet Printer */
- 0x04f97504, /* HP Deskjet 3843 Color Inkjet Printer */
- 0x04f97604, /* HP Deskjet 3900 Color Inkjet Printer */
- 0x04f90714, /* HP Designjet 4000ps */
- 0x04f99c11, /* HP Officejet 4000 K210 Printer */
- 0x04f97b17, /* HP Color LaserJet CP4005n Printer */
- 0x04f98817, /* HP Color LaserJet CP4020 Series Printer */
- 0x04f95711, /* HP Photosmart C4110 All-in-One Printer */
- 0x04f97704, /* HP Deskjet D4145 Printer */
- 0x04f97e04, /* HP Deskjet F4135 All-in-One Printer */
- 0x04f93111, /* HP OfficeJet 4100 Series All-in-One Printer */
- 0x04f96017, /* HP LaserJet 4150 Printer */
- 0x04f93d11, /* HP Officejet 4200 All-in-One Printer */
- 0x04f95c11, /* HP Photosmart C4205 All-in-One Printer */
- 0x04f97b04, /* HP Deskjet D4245 Printer */
- 0x04f96a17, /* HP LaserJet 4240n Printer */
- 0x04f92417, /* HP LaserJet 4250 Printer */
- 0x04f95411, /* HP Officejet 4308 All-in-One Printer */
- 0x04f91f04, /* HP Deskjet D4360 Printer */
- 0x04f96711, /* HP Photosmart C4340 All-in-One Printer */
- 0x04f97417, /* HP LaserJet 4345 Multifunction Printer */
- 0x04f99717, /* HP LaserJet M4349 MFP */
- 0x04f92317, /* HP LaserJet 4350 Printer */
- 0x04f96611, /* HP Photosmart C4380 All-in-One Printer */
- 0x04f96c11, /* HP Photosmart C4410 All-in-One Printer */
- 0x04f99d11, /* HP Officejet 4400 K410 All-in-One Printer */
- 0x04f97711, /* HP Deskjet F4440 All-in-One Printer */
- 0x04f94712, /* HP Officejet 4500 Desktop All-in-One Printer - G510a */
- 0x04f98c11, /* HP Deskjet F4500 All-in-One Printer Series */
- 0x04f95712, /* HP Officejet 4500 All-in-One Printer - K710 */
- 0x04f92a12, /* HP Officejet J4524 All-in-One Printer */
- 0x04f96b11, /* HP Photosmart C4540 All-in-One Printer */
- 0x04f9c511, /* HP Envy 4500 e-All-in-One */
- 0x04f92e12, /* HP Officejet 4500 G510n-z All-in-One Printer */
- 0x04f9c411, /* HP DeskJet Ink Advantage 4515 e-All-in-One Printer */
- 0x04f99917, /* HP Color LaserJet CM4540 Multifunction Printer */
- 0x04f99c17, /* HP LaserJet M4555 MFP */
- 0x04f97411, /* HP Photosmart C4640 All-in-One Printer */
- 0x04f96c17, /* HP Color LaserJet 4610n Printer */
- 0x04f95812, /* HP OfficeJet 4610 All-in-One Printer Series */
- 0x04f96512, /* HP Deskjet Ink Advantage 4610 All-in-One Printer Series */
- 0x04f96612, /* HP Deskjet Ink Advantage 4620 e-All-in-One Printer */
- 0x04f96412, /* HP OfficeJet 4620 e-All-in-One Printer */
- 0x04f9c611, /* HP Officejet 4630 e-All-in-One Printer */
- 0x04f9c811, /* HP Deskjet Ink Advantage 4640 e-All-in-One Printer series */
- 0x04f91a17, /* HP Color LaserJet 4650 Printer */
- 0x04f92b12, /* HP Officejet J4660 All-in-One Printer */
- 0x04f92c12, /* HP Officejet J4680c All-in-One Printer */
- 0x04f97511, /* HP Photosmart C4740 All-in-One Printer */
- 0x04f96217, /* HP Color LaserJet 4700 Printer */
- 0x04f97d17, /* HP Color LaserJet CM4730 Multifunction Printer */
- 0x04f96317, /* HP Color LaserJet 4730xs Multifunction Printer */
- 0x04f97917, /* HP LaserJet M5025 Multifunction Printer */
- 0x04f97217, /* HP LaserJet M5035 Multifunction Printer */
- 0x04f9a117, /* HP LaserJet M5039 Multifunction Printer */
- 0x04f9c802, /* HP Photosmart D5060 Printer */
- 0x04f95811, /* HP Photosmart C5140 All-in-One Printer */
- 0x04f9c402, /* HP Photosmart D5145 Printer */
- 0x04f92411, /* HP Officejet 5100 All-in-One Printer */
- 0x04f95d11, /* HP Photosmart C5240 All-in-One Printer */
- 0x04f96417, /* HP LaserJet 5200 Printer */
- 0x04f96617, /* HP LaserJet 5200L Printer */
- 0x04f98917, /* HP LaserJet 5200LX Printer */
- 0x04f95117, /* HP Color LaserJet CP5225 */
- 0x04f97111, /* HP Photosmart C5370 All-in-One Printer */
- 0x04f91f12, /* HP Officejet Pro K5300 Printer */
- 0x04f96811, /* HP Photosmart D5345 Printer */
- 0x04f92012, /* HP Officejet Pro K5400dn Printer */
- 0x04f98604, /* HP Deskjet 5420v Photo Printer */
- 0x04f96d11, /* HP Photosmart D5460 Printer */
- 0x04f93a11, /* HP Officejet 5505 All-in-One Printer */
- 0x04f93012, /* HP Officejet J5505 All-in-One Printer */
- 0x04f98211, /* HP Deskjet D5545 Printer */
- 0x04f97211, /* HP Photosmart C5540 All-in-One Printer */
- 0x04f9a111, /* HP Photosmart 5510 e-All-in-One */
- 0x04f9b411, /* HP Photosmart 5510d e-All-in-One */
- 0x04f99b17, /* HP Color LaserJet CP5520 Series Printer */
- 0x04f9b111, /* HP Photosmart 5520 e-All-in-One */
- 0x04f9b611, /* HP Deskjet Ink Advantage 5525 e-All-in-One */
- 0x04f9c311, /* HP ENVY 5530 e-All-in-One Printer */
- 0x04f91f17, /* HP Color LaserJet 5550n Printer */
- 0x04f96004, /* HP Deskjet 5550 Color Inkjet Printer */
- 0x04f94f11, /* HP Officejet 5600 Series All-in-One Printer */
- 0x04f9cc11, /* HP Envy 5640 e-All-in-One */
- 0x04f96104, /* HP Deskjet 5650 Color Inkjet Printer */
- 0x04f95b11, /* HP Officejet J5725 All-in-One Printer */
- 0x04f98104, /* HP Deskjet 5700 Color Inkjet Printer */
- 0x04f9cd11, /* HP Officejet 5740 e-All-in-One */
- 0x04f9a004, /* HP Deskjet 5800 Color Inkjet Printer */
- 0x04f98704, /* HP Deskjet 5938 Photo Printer */
- 0x04f94312, /* HP Officejet 6000 Wireless Printer - E609n */
- 0x04f94212, /* HP Officejet 6000 Printer - E609a */
- 0x04f96f17, /* HP Color LaserJet CP6015dn Printer */
- 0x04f97c17, /* HP Color LaserJet CM6030 Multifunction Printer */
- 0x04f99517, /* HP Color LaserJet CM6049 MFP */
- 0x04f95911, /* HP Photosmart C6150 All-in-One Printer */
- 0x04f95e12, /* HP OfficeJet 6100 ePrinter H611a */
- 0x04f90b14, /* HP Designjet z6100ps 60in photo */
- 0x04f9c502, /* HP Photosmart D6160 Printer */
- 0x04f92d11, /* HP Officejet 6105 All-in-One Printer */
- 0x04f93404, /* HP Deskjet 6120 Color Inkjet Printer */
- 0x04f94b11, /* HP Officejet 6200 All-in-One Printer */
- 0x04f91014, /* HP Designjet z6200PS 42in Photo */
- 0x04f96a11, /* HP Photosmart C6240 All-in-One Printer */
- 0x04f97312, /* HP OfficeJet Pro 6230 ePrinter */
- 0x04f97011, /* HP Photosmart C6324 All-in-One Printer */
- 0x04f95311, /* HP Officejet 6301 All-in-One Printer */
- 0x04f93312, /* HP Officejet J6405 All-in-One Printer */
- 0x04f94412, /* HP Officejet 6500 All-in-One Printer - E709a */
- 0x04f95412, /* HP Officejet 6500 E710n-z */
- 0x04f95512, /* HP Officejet 6500 E710 */
- 0x04f94512, /* HP Officejet 6500 Wireless All-in-One Printer - E709n */
- 0x04f98204, /* HP Deskjet 6500 Color Inkjet Printer */
- 0x04f9a511, /* HP Photosmart 6510 e-All-in-one */
- 0x04f9af11, /* HP Photsmart 6520 e All-in-One */
- 0x04f9ba11, /* HP Deskjet Ink Advantage 6525 e-All-in-One */
- 0x04f98504, /* HP Deskjet 6600 Series Color Inkjet Printer */
- 0x04f95d12, /* HP Officejet 6600 e-All-in-One Printer - H711a */
- 0x04f95c12, /* HP Officejet 6700 Premium e-All-in-One Printer-H711n */
- 0x04f98404, /* HP Deskjet 6800 Color Inkjet Printer */
- 0x04f97412, /* HP OfficeJet 6800 e-All-in-one */
- 0x04f97212, /* HP OfficeJet Pro 6830 e-All-in-one */
- 0x04f98804, /* HP Deskjet 6980xi Printer */
- 0x04f94612, /* HP Officejet 7000 E809 series */
- 0x04f92611, /* HP Officejet 7100 All-in-One Printer */
- 0x04f95a11, /* HP Photosmart C7150 All-in-One Printer */
- 0x04f92612, /* HP Officejet K7100 Printer */
- 0x04f9c602, /* HP Photosmart D7145 Printer */
- 0x04f96012, /* HP Officejet 7110 Wide Format ePrinter */
- 0x04f96911, /* HP Photosmart D7245 Printer */
- 0x04f9b002, /* HP Photosmart 7260w Photo Printer */
- 0x04f94111, /* HP Officejet 7205 All-in-One Printer */
- 0x04f96511, /* HP Photosmart C7250 All-in-One Printer */
- 0x04f94211, /* HP Officejet 7310 All-in-One Printer */
- 0x04f92512, /* HP Officejet Pro L7300 Series All-in-One Printer */
- 0x04f9c702, /* HP Photosmart D7345 Printer */
- 0x04f92002, /* HP Photosmart 7345 Printer */
- 0x04f94311, /* HP Officejet 7408 All-in-One Printer */
- 0x04f9b802, /* HP Photosmart 7450 Photo Printer */
- 0x04f95e11, /* HP Photosmart D7460 Printer */
- 0x04f93412, /* HP Officejet Pro L7480 All-in-One Printer */
- 0x04f92112, /* HP Officejet Pro L7500 Series All-in-One Printer */
- 0x04f94812, /* HP Officejet 7500 E910 */
- 0x04f96f11, /* HP Photosmart D7560 Printer */
- 0x04f9a611, /* HP Photosmart 7510 e-All-in-One */
- 0x04f9bc11, /* HP Photosmart 7520 e-All-in-One */
- 0x04f93e02, /* HP Photosmart 7550 Printer */
- 0x04f92212, /* HP Officejet Pro L7600 Series All-in-One Printer */
- 0x04f9b202, /* HP Photosmart 7655 Photo Printer */
- 0x04f96e12, /* HP Officejet 7610 Wide Format e-All-in-One Printer */
- 0x04f9dc11, /* HP Envy 7640 e-All-in-One */
- 0x04f92312, /* HP Officejet Pro L7700 Series All-in-One Printer */
- 0x04f9b402, /* HP Photosmart 7755 Photo Printer */
- 0x04f9c002, /* HP Photosmart 7830 Printer */
- 0x04f9b602, /* HP Photosmart 7960 Photo Printer */
- 0x04f9d011, /* HP Envy 8000 e-All-in-One */
- 0x04f95612, /* HP Officejet Pro 8000 Enterprise A811a */
- 0x04f93612, /* HP Officejet Pro 8000 Printer - A809a */
- 0x04f9c102, /* HP Photosmart 8030 Printer */
- 0x04f9de11, /* HP OfficeJet 8040 e-All-in-One */
- 0x04f97717, /* HP CM8050 Color Multifunction Printer with Edgeline Technology */
- 0x04f97117, /* HP CM8060 Color Multifunction Printer with Edgeline Technology */
- 0x04f95b12, /* HP OfficeJet Pro 8100 ePrinter-N811a */
- 0x04f96411, /* HP Photosmart C8150 All-in-One Printer */
- 0x04f9ba02, /* HP Photosmart 8150 Photo Printer */
- 0x04f9c202, /* HP Photosmart 8230 Printer */
- 0x04f9be02, /* HP Photosmart Pro B8330 Printer */
- 0x04f9bb02, /* HP Photosmart 8450gp Photo Printer */
- 0x04f93812, /* HP Officejet Pro 8500 All-in-One Printer - A909a */
- 0x04f94012, /* HP Officejet Pro 8500 Premier All-in-One Printer - A909n */
- 0x04f93912, /* HP Officejet Pro 8500 Wireless All-in-One Printer - A909g */
- 0x04f9d102, /* HP Photosmart B8550 Photo Printer */
- 0x04f95312, /* HP OfficeJet Pro 8500A Plus e-AiO Printer - A910g */
- 0x04f92712, /* HP Officejet Pro K8600 Color Printer */
- 0x04f95912, /* HP OfficeJet Pro 8600 e-AiO N911a */
- 0x04f97112, /* HP OfficeJet Pro 8610 e-All-in-One Printer */
- 0x04f97012, /* HP OfficeJet Pro 8620 e-All-in-One Printer */
- 0x04f96f12, /* HP OfficeJet Pro 8630 e-All-in-One Printer */
- 0x04f97712, /* HP OfficeJet Pro 8640 e-All-in-One Printer */
- 0x04f97612, /* HP OfficeJet Pro 8660 e-All-in-One Printer */
- 0x04f9bc02, /* HP Photosmart 8750 Professional Photo Printer */
- 0x04f9d002, /* HP Photosmart Pro B8850 Printer */
- 0x04f98417, /* HP LaserJet 9040 Multifunction Printer */
- 0x04f92017, /* HP LaserJet 9040 Printer */
- 0x04f92117, /* HP LaserJet 9050 Multifunction Printer */
- 0x04f98317, /* HP LaserJet M9050 Multifunction Printer */
- 0x04f99617, /* HP LaserJet M9059 MFP */
- 0x04f90d12, /* HP Officejet 9110 All-in-One Printer */
- 0x04f9bd02, /* HP Photosmart Pro B9180gp Photo Printer */
- 0x04f92217, /* HP Color LaserJet 9500n Printer */
- 0x04f90b12, /* HP Deskjet 9650 Printer */
- 0x04f91212, /* HP Deskjet 9800 Printer */
- 0x04f93c2a, /* HP Color LaserJet Pro M252n */
- 0x04f9382a, /* HP Color LaserJet Enterprise M553n */
- 0x04f9582a, /* HP Color LaserJet Enterprise M552dn */
- 0x04f9552a, /* HP LaserJet Enterprise M604n */
- 0x04f93e2a, /* HP LaserJet Enterprise M605n */
- 0x04f93f2a, /* HP LaserJet Enterprise M606dn */
- 0x04f9e311, /* HP DeskJet 3630 All-in-One Printer */
- 0x04f9e111, /* HP DeskJet 2130 All-in-One Printer series */
- 0x04f9df11, /* HP Deskjet 1110 Printer */
- 0x04f9e511, /* HP OfficeJet 3830 All-in-One Printer */
- 0x04f9e611, /* HP DeskJet Ink Advantage 3830 All-in-One Printer */
- 0x04f9d911, /* HP OfficeJet 4650 All-in-One Printer series */
- 0x04f9d711, /* HP ENVY 4520 All-in-One Printer series */
- 0x04f9ce11, /* HP Envy 5540 All-in-One Printer series */
- 0x04f9e811, /* HP Envy 4510 All-in-One */
- 0x04f9842a, /* HP Color LaserJet Pro MFP M274n */
- 0x04f9e211, /* HP DeskJet Ink Advantage Ultra 4720 All-in-One Printer series */
- 0x04f9db11, /* HP DeskJet Ink Advantage 5640 All-in-One Printer series */
- 0x04f9da11, /* HP DeskJet Ink Advantage 4670 All-in-One */
- 0x04f9d811, /* HP DeskJet Ink Advantage 4530 All-in-One */
- 0x04f9422a, /* HP LaserJet Enterprise M506 series */
- 0x04f9542a, /* HP LaserJet Pro M402dw */
- 0x04f9602a, /* HP LaserJet Pro M402n */
- 0x04f9522a, /* HP Color Laserjet Pro M452dn */
- 0x04f95a2a, /* HP Laserjet Pro MFP M426fdn */
- 0x04f9402a, /* HP Laserjet Enterprise MFP M527dn */
- 0x04f9412a, /* HP Laserjet Enterprise Flow MFP M527c */
- 0x04f95305, /* HP Scanjet Pro 3500 f1 Flatbed Scanner */
- 0x04f93a2a, /* HP Color LaserJet Enterprise MFP M577 Series */
- 0x04f94a2a, /* HP Color LaserJet Enterprise Flow MFP M577 Series */
- 0x04f9512a, /* HP Color Laserjet Pro MFP M477 fnw */
- 0x04f9d611, /* HP PageWide Pro 577dw Multifunction Printer */
- 0x04f9d311, /* HP PageWide Pro 552dw Printer */
- 0x04f9d211, /* HP PageWide Pro 452dw Printer */
- 0x04f9d111, /* HP PageWide Pro 452dn Printer */
- 0x04f9d511, /* HP PageWide Pro 477dw Multifunction Printer */
- 0x04f9d411, /* HP PageWide Pro 477dn Multifunction Printer */
- 0x04f9ed11, /* HP DeskJet GT 5810 All-in-One Printer */
- 0x04f9ee11, /* HP DeskJet GT 5820 All-in-One Printer */
- 0x04f9e711, /* HP OfficeJet 200 Mobile Printer */
- 0x04f97a12, /* HP OfficeJet Pro 8710 All-in-One Printer */
- 0x04f96312, /* HP OfficeJet Pro 8740 All-in-One Printer */
- 0x04f97b12, /* HP OfficeJet Pro 8720 All-in-One Printer */
- 0x04f9652a, /* HP Laserjet Pro M501n */
- 0x04f9832a, /* HP PageWide Enterprise Color MFP 586dn */
- 0x04f9822a, /* HP PageWide Enterprise Color Flow MFP 586z */
- 0x04f9fa11, /* HP PageWide Managed MFP P57750dw */
- 0x04f9f911, /* HP PageWide Managed P55250dw */
- 0x04f97d12, /* HP OfficeJet Pro 8210 */
- 0x04f9862a, /* HP Color Laserjet MFP M377 fnw */
- 0x04f9f511, /* HP DeskJet Ink Advantage Ultra 5738 All-in-One Printer */
- 0x04f91254, /* HP ENVY Photo 6200 All-in-One */
- 0x04f91154, /* HP ENVY Photo 7100 All-in-One */
- 0x04f91054, /* HP ENVY Photo 7800 All-in-One */
- 0x04f9e911, /* HP OfficeJet 250 Mobile All-in-One */
- 0x04f90d54, /* HP OfficeJet Pro 6960 All-in-One */
- 0x04f90c54, /* HP OfficeJet Pro 6970 All-in-One */
- 0x04f903f0, /* HP DeskJet 3700 All-in-One */
- 0x04f90e54, /* HP OfficeJet 6960 All-in-One */
- 0x04f9802a, /* HP PageWide Managed Color E55650dn */
- 0x04f91554, /* HP OfficeJet Pro 8732 All-in-One Printer */
- 0x04f90954, /* HP OfficeJet 8702 All-in-One */
- 0x04f9f211, /* HP PageWide MFP 377dw */
- 0x04f90154, /* HP OfficeJet Pro 7740 Wide Format All-in-One */
- 0x04f90f54, /* HP OfficeJet 6950 All-in-One */
- 0x04f95605, /* HP Scanjet Pro 3000 S3 */
- 0x04f95705, /* HP Scanjet Enterprise Flow 5000 S4 */
- 0x04f95805, /* HP Scanjet Enterprise Flow 7000 S3 */
- 0x04f9612a, /* HP LaserJet M102a */
- 0x04f91654, /* HP OfficeJet Pro 7720 Wide Format All-in-One */
- 0x04f91754, /* HP OfficeJet Pro 7730 Wide Format All-in-One */
- 0x04f90853, /* HP ENVY 5000 All-in-One */
- 0x04f90a54, /* HP DeskJet Ink Advantage 5075 All-in-One */
- 0x04f9632a, /* HP LaserJet Pro M203d */
- 0x04f9642a, /* HP LaserJet Pro MFP M227sdn */
- 0x04f9622a, /* HP LaserJet Pro MFP M132a */
- 0x04f90b53, /* HP DeskJet Ink Advantage 5275 All-in-One */
- 0x04f9b22a, /* HP Color LaserJet Managed Flow MFP E77830z */
- 0x04f9b32a, /* HP Color LaserJet Managed MFP E87640 dn */
- 0x04f9b12a, /* HP LaserJet Managed MFP E82540dn */
- 0x04f9b02a, /* HP LaserJet Managed MFP E72525dn */
- 0x04f96b2a, /* HP LaserJet Enterprise M607n */
- 0x04f96c2a, /* HP LaserJet Managed E60055dn */
- 0x04f9672a, /* HP LaserJet Enterprise MFP M631dn */
- 0x04f9682a, /* HP LaserJet Managed MFP E62555dn */
- 0x04f9a32a, /* HP Color LaserJet Enterprise M652n */
- 0x04f9a42a, /* HP Color LaserJet Managed E65050dn */
- 0x04f9a52a, /* HP Color LaserJet Enterprise MFP M681dh */
- 0x04f9a62a, /* HP Color LaserJet Managed MFP E67550dh */
- 0x04f9fe11, /* HP PageWide Pro 750dn */
- 0x04f9eb11, /* HP PageWide Pro MFP 772dw */
- 0x04f9fc11, /* HP PageWide Managed MFP P77740zs */
- 0x04f9f611, /* HP PageWide Managed P75050dn */
- 0x04f9932a, /* HP LaserJet Pro MFP M26a */
- 0x04f90753, /* HP DeskJet 2200 All-in-One */
- 0x04f90053, /* HP DeskJet 2620 All-in-One */
- 0x04f9b62a, /* HP PageWide Enterprise Color 765dn */
- 0x04f9b72a, /* HP PageWide Managed Color E75160dn */
- 0x04f90e53, /* HP AMP All-in-One */
- 0x04f9b42a, /* HP PageWide Enterprise Color MFP 780dn */
- 0x04f9b52a, /* HP PageWide Managed Color MFP E77650dn */
- 0x04f9ba2a, /* HP Scanjet Enterprise Flow N9120 fn2 Document Scanner */
- 0x04f9b92a, /* HP Digital Sender Flow 8500 fn2 Document Capture Workstation */
- 0x04f9be2a, /* HP LaserJet Pro M15w */
- 0x04f9bf2a, /* HP LaserJet Pro MFP M28w */
- 0x04f9ac2a, /* HP Color LaserJet Pro M253a */
- 0x04f9af2a, /* HP Color LaserJet Pro MFP M180nw */
- 0x04f9ad2a, /* HP Color LaserJet Pro MFP M281fdw */
- 0x04f9ae2a, /* HP Color LaserJet Pro M154a */
- 0x04f9c92a, /* HP PageWide Managed Color MFP P77440dn */
- 0x04f9c72a, /* HP PageWide Managed Color MFP P77450dn */
- 0x04f90f53, /* HP Smart Tank 350 */
- 0x04f91253, /* HP Smart Tank Wireless 450 */
- 0x04f91053, /* HP Ink Tank 310 */
- 0x04f91353, /* HP Ink Tank Wireless 410 */
- 0x04f91453, /* HP Ink Tank 115 */
- 0x04f9ef2a, /* HP PageWide 755dn */
- 0x04f9ee2a, /* HP PageWide MFP 774dn */
- 0x04f9e92a, /* HP LaserJet Pro M118dw */
- 0x04f9ec2a, /* HP LaserJet Pro MFP M148dw */
- 0x04f9ed2a, /* HP LaserJet Pro MFP M148fdw */
- 0x04f98411, /* HP Scanjet Enterprise 7500 */
- 0x04f92454, /* HP OfficeJet Pro All-in-One 9010 */
- 0x04f92354, /* HP OfficeJet Pro All-in-One 9020 */
- 0x04f92554, /* HP OfficeJet All-in-One 9010 */
- 0x04f92654, /* HP OfficeJet Pro 8030 All-in-One Printer series */
- 0x04f92854, /* HP OfficeJet 8020 All-in-One Printer series */
- 0x04f92754, /* HP OfficeJet Pro 8020 All-in-One Printer series */
- 0x04f92954, /* HP OfficeJet 8010 All-in-One Printer series */
- 0x04f91c54, /* HP Smart Tank Plus 650 */
- 0x04f91b54, /* HP Smart Tank 610 */
- 0x04f91a54, /* HP Smart Tank Plus 550 */
- 0x04f91954, /* HP Smart Tank 510 */
- 0x04f9e32a, /* HP LaserJet Managed MFP E62655dn */
- 0x04f9e02a, /* HP LaserJet Managed E60155dn */
- 0x04f9e22a, /* HP Color LaserJet Managed MFP E67650dh */
- 0x04f9e12a, /* HP Color LaserJet Managed E65150dn */
- 0x04f9f42a, /* HP Neverstop Laser MFP 1200a */
- 0x04f9f32a, /* HP Neverstop Laser 1000a */
- 0x04f9f02a, /* HP Laser NS 1020 */
- 0x04f9f12a, /* HP Laser NS MFP 1005 */
- 0x04f92b54, /* HP Smart Tank 500 series */
- 0x04f92a54, /* HP Smart Tank 530 series */
- 0x04f92d54, /* HP Smart Tank Plus 570 series */
- 0x04f9ca2a, /* HP LaserJet Enterprise M507n */
- 0x04f9d22a, /* HP Laserjet Managed E50145dn */
- 0x04f9cc2a, /* HP LaserJet Enterprise MFP M528dn */
- 0x04f9d32a, /* HP LaserJet Managed MFP E52645dn */
- 0x04f99d2a, /* HP Color LaserJet Enterprise M751n */
- 0x04f99e2a, /* HP Color LaserJet Managed E75245dn */
- 0x04f9de2a, /* HP LaserJet Pro M305d */
- 0x04f9c12a, /* HP LaserJet Pro M404d */
- 0x04f9c22a, /* HP LaserJet Pro MFP M428dw */
- 0x04f9df2a, /* HP LaserJet Pro MFP M329dn */
- 0x04f9c32a, /* HP LaserJet Pro MFP M428fdn */
- 0x04f9c42a, /* HP Color LaserJet Pro M453cdn */
- 0x04f9c62a, /* HP Color LaserJet Pro MFP M479dw */
- 0x04f9c52a, /* HP Color LaserJet Pro MFP M478fcdn */
- 0x04f99f2a, /* HP Color LaserJet Enterprise MFP M776dn */
- 0x04f9a12a, /* HP Color laserjet Enterprise M856dn */
- 0x04f9a22a, /* HP Color laserjet Managed E85055 */
- 0x04f90c70, /* HP Color LaserJet Pro M155a */
- 0x04f90a70, /* HP Color LaserJet Pro M256dn */
- 0x04f90970, /* HP Color LaserJet Pro MFP M282nw */
- 0x04f90870, /* HP Color LaserJet Pro MFP M182n */
- 0x04f95a05, /* HP Scanjet Pro 2000 S2 */
- 0x04f95e05, /* HP ScanJet Enterprise Flow N7000 snw1 */
- 0x04f95c05, /* HP ScanJet Pro N4000 snw1 */
- 0x04f95b05, /* HP ScanJet Pro 3000 s4 */
- 0x04f95d05, /* HP ScanJet Enterprise Flow 5000 s5 */
- };
-
- /* Taken from epkowa.desc from iscan-data package for Epson driver */
- private const uint32 epkowa_devices[] = { 0x04b80101, 0x04b80102, 0x04b80103, 0x04b80104, 0x04b80105, 0x04b80106, 0x04b80107, 0x04b80108, 0x04b80109, 0x04b8010a, 0x04b8010b, 0x04b8010c, 0x04b8010d, 0x04b8010e, 0x04b8010f, 0x04b80110, 0x04b80112, 0x04b80114, 0x04b80116, 0x04b80118, 0x04b80119, 0x04b8011a, 0x04b8011b, 0x04b8011c, 0x04b8011d, 0x04b8011e, 0x04b8011f, 0x04b80120, 0x04b80121, 0x04b80122, 0x04b80126, 0x04b80128, 0x04b80129, 0x04b8012a, 0x04b8012b, 0x04b8012c, 0x04b8012d, 0x04b8012e, 0x04b8012f, 0x04b80130, 0x04b80131, 0x04b80133, 0x04b80135, 0x04b80136, 0x04b80137, 0x04b80138, 0x04b8013a, 0x04b8013b, 0x04b8013c, 0x04b8013d, 0x04b80142, 0x04b80143, 0x04b80144, 0x04b80147, 0x04b8014a, 0x04b8014b, 0x04b80151, 0x04b80153, 0x04b80801, 0x04b80802, 0x04b80805, 0x04b80806, 0x04b80807, 0x04b80808, 0x04b8080a, 0x04b8080c, 0x04b8080d, 0x04b8080e, 0x04b8080f, 0x04b80810, 0x04b80811, 0x04b80813, 0x04b80814, 0x04b80815, 0x04b80817, 0x04b80818, 0x04b80819, 0x04b8081a, 0x04b8081c, 0x04b8081d, 0x04b8081f, 0x04b80820, 0x04b80821, 0x04b80827, 0x04b80828, 0x04b80829, 0x04b8082a, 0x04b8082b, 0x04b8082e, 0x04b8082f, 0x04b80830, 0x04b80831, 0x04b80833, 0x04b80834, 0x04b80835, 0x04b80836, 0x04b80837, 0x04b80838, 0x04b80839, 0x04b8083a, 0x04b8083c, 0x04b8083f, 0x04b80841, 0x04b80843, 0x04b80844, 0x04b80846, 0x04b80847, 0x04b80848, 0x04b80849, 0x04b8084a, 0x04b8084c, 0x04b8084d, 0x04b8084f, 0x04b80850, 0x04b80851, 0x04b80852, 0x04b80853, 0x04b80854, 0x04b80855, 0x04b80856, 0x04b8085c, 0x04b8085d, 0x04b8085e, 0x04b8085f, 0x04b80860, 0x04b80861, 0x04b80862, 0x04b80863, 0x04b80864, 0x04b80865, 0x04b80866, 0x04b80869, 0x04b8086a, 0x04b80870, 0x04b80871, 0x04b80872, 0x04b80873, 0x04b80878, 0x04b80879, 0x04b8087b, 0x04b8087c, 0x04b8087d, 0x04b8087e, 0x04b8087f, 0x04b80880, 0x04b80881, 0x04b80883, 0x04b80884, 0x04b80885, 0x04b8088f, 0x04b80890, 0x04b80891, 0x04b80892, 0x04b80893, 0x04b80894, 0x04b80895, 0x04b80896, 0x04b80897, 0x04b80898, 0x04b80899, 0x04b8089a, 0x04b8089b, 0x04b8089c, 0x04b8089d, 0x04b8089e, 0x04b8089f, 0x04b808a0, 0x04b808a1, 0x04b808a5, 0x04b808a6, 0x04b808a8, 0x04b808a9, 0x04b808aa, 0x04b808ab, 0x04b808ac, 0x04b808ad, 0x04b808ae, 0x04b808af, 0x04b808b0, 0x04b808b3, 0x04b808b4, 0x04b808b5, 0x04b808b6, 0x04b808b7, 0x04b808b8, 0x04b808b9, 0x04b808bd, 0x04b808be, 0x04b808bf, 0x04b808c0, 0x04b808c1, 0x04b808c3, 0x04b808c4, 0x04b808c5, 0x04b808c6, 0x04b808c7, 0x04b808c8, 0x04b808c9, 0x04b808ca, 0x04b808cd, 0x04b808d0 };
-
-
- /* Taken from /usr/local/lexmark/unix_scan_drivers/etc/lexmark_nscan.conf */
- /* Lexmark IDs extracted using command:
- * grep -r "usb .* /usr" --no-filename --only-matching | sed 's/usb //' | sed 's/ 0x//' | sed 's/ \/usr/,/'
- */
- private const uint32 lexmark_nscan_devices[] = {
- 0x043d0279,
- 0x043d027a,
- 0x043d01D6,
- 0x043d01D7,
- 0x043d01D8,
- 0x043d01DC,
- 0x043d01DE,
- 0x043d01E0,
- 0x043d01FA,
- 0x043d01FB,
- 0x043d01FC,
- 0x043d01FD,
- 0x043d01FE,
- 0x043d01FF,
- 0x043d01F4,
- 0x043d0120,
- 0x043d0121,
- 0x043d0128,
- 0x043d014F,
- 0x043d0149,
- 0x043d0152,
- 0x043d0168,
- 0x043d0169,
- 0x043d016A,
- 0x043d012D,
- 0x043d01C4,
- 0x043d01C5,
- 0x043d01C6,
- 0x043d01CF,
- 0x043d01D0,
- 0x043d01D1,
- 0x043d01DB,
- 0x043d01ED,
- 0x043d01F1,
- 0x043d01F5,
- 0x043d0222,
- 0x043d0223,
- 0x043d0227,
- 0x043d0228,
- 0x043d022A,
- 0x043d022B,
- 0x043d022F,
- 0x043d0230,
- 0x043d0231,
- 0x043d0234,
- 0x043d0235,
- 0x043d0244,
- 0x043d0245,
- 0x043d0246,
- 0x043d0247,
- 0x043d0248,
- 0x043d024A,
- 0x043d024E,
- 0x043d024F
- };
-
- /* Brother IDs extracted using the following Python
- *
- * import sys
- * for f in sys.argv:
- * for l in file (f).readlines ():
- * tokens = l.strip().split (',')
- * if len (tokens) >= 4:
- * print (' 0x%08x' % (0x04f9 << 16 | int (tokens[0], 16)) + ", /* " + tokens[3].strip("\"") + " * /")
- */
-
- /* HPAIO IDs extracted using the following Python:
- import sys
- ids = []
- for f in sys.argv:
- for l in file (f).readlines ():
- if l.startswith ('model1='):
- model=l[7:].strip ()
- elif l.startswith ('usb-pid='):
- pid = int (l[8:].strip (), 16)
- if pid == 0:
- continue
- usb_id = '0x%08x' % (0x04f9 << 16 | pid)
- if not usb_id in ids:
- ids.append (usb_id)
- print (usb_id + ", /* " + model + " * /")
- */
-
- public string? suggest_driver ()
- {
- if (usb_context == null)
- return null;
-
- var driver_map = new HashTable (direct_hash, direct_equal);
- add_devices (driver_map, brscan_devices, "brscan");
- add_devices (driver_map, brscan2_devices, "brscan2");
- add_devices (driver_map, brscan3_devices, "brscan3");
- add_devices (driver_map, brscan4_devices, "brscan4");
- add_devices (driver_map, pixma_devices, "pixma");
- add_devices (driver_map, samsung_devices, "samsung");
- add_devices (driver_map, smfp_devices, "smfp");
- add_devices (driver_map, hpaio_devices, "hpaio");
- add_devices (driver_map, epkowa_devices, "epkowa");
- add_devices (driver_map, lexmark_nscan_devices, "lexmark_nscan");
- var devices = usb_context.get_devices ();
- for (var i = 0; i < devices.length; i++)
- {
- var device = devices.data[i];
- var driver = driver_map.lookup (device.get_vid () << 16 | device.get_pid ());
- if (driver != null)
- return driver;
- }
-
- return null;
- }
-
- private void add_devices (HashTable map, uint32[] devices, string driver)
- {
- for (var i = 0; i < devices.length; i++)
- map.insert (devices[i], driver);
- }
-
- private void authorize_cb (Scanner scanner, string resource)
- {
- app.authorize.begin (resource, (obj, res) =>
- {
- var data = app.authorize.end(res);
- if (data.success)
- {
- scanner.authorize (data.username, data.password);
- }
- });
- }
-
- private Page append_page (int width = 100, int height = 100, int dpi = 100)
- {
- /* Use current page if not used */
- var page = book.get_page (-1);
- if (page != null && !page.has_data)
- {
- app.selected_page = page;
- page.start ();
- return page;
- }
-
- /* Copy info from previous page */
- var scan_direction = ScanDirection.TOP_TO_BOTTOM;
- bool do_crop = false;
- string named_crop = null;
- var cx = 0, cy = 0, cw = 0, ch = 0;
- if (page != null)
- {
- scan_direction = page.scan_direction;
- width = page.width;
- height = page.height;
- dpi = page.dpi;
-
- do_crop = page.has_crop;
- if (do_crop)
- {
- named_crop = page.crop_name;
- cx = page.crop_x;
- cy = page.crop_y;
- cw = page.crop_width;
- ch = page.crop_height;
- }
- }
-
- page = new Page (width, height, dpi, scan_direction);
- book.append_page (page);
- if (do_crop)
- {
- if (named_crop != null)
- {
- page.set_named_crop (named_crop);
- }
- else
- page.set_custom_crop (cw, ch);
- page.move_crop (cx, cy);
- }
- app.selected_page = page;
- page.start ();
-
- return page;
- }
-
- private void scanner_new_page_cb (Scanner scanner)
- {
- append_page ();
- }
-
- private string? get_profile_for_device (string device_name)
- {
-#if HAVE_COLORD
- var device_id = "sane:%s".printf (device_name);
- debug ("Getting color profile for device %s", device_name);
-
- var client = new Cd.Client ();
- try
- {
- client.connect_sync ();
- }
- catch (Error e)
- {
- debug ("Failed to connect to colord: %s", e.message);
- return null;
- }
-
- Cd.Device device;
- try
- {
- device = client.find_device_by_property_sync (Cd.DEVICE_PROPERTY_SERIAL, device_id);
- }
- catch (Error e)
- {
- debug ("Unable to find colord device %s: %s", device_name, e.message);
- return null;
- }
-
- try
- {
- device.connect_sync ();
- }
- catch (Error e)
- {
- debug ("Failed to get properties from the device %s: %s", device_name, e.message);
- return null;
- }
-
- var profile = device.get_default_profile ();
- if (profile == null)
- {
- debug ("No default color profile for device: %s", device_name);
- return null;
- }
-
- try
- {
- profile.connect_sync ();
- }
- catch (Error e)
- {
- debug ("Failed to get properties from the profile %s: %s", device_name, e.message);
- return null;
- }
-
- if (profile.filename == null)
- {
- debug ("No icc color profile for the device %s", device_name);
- return null;
- }
-
- debug ("Using color profile %s for device %s", profile.filename, device_name);
- return profile.filename;
-#else
- return null;
-#endif
- }
-
- private void scanner_page_info_cb (Scanner scanner, ScanPageInfo info)
- {
- debug ("Page is %d pixels wide, %d pixels high, %d bits per pixel",
- info.width, info.height, info.depth);
-
- /* Add a new page */
- scanned_page = append_page ();
- scanned_page.set_page_info (info);
-
- /* Get ICC color profile */
- /* FIXME: The ICC profile could change */
- /* FIXME: Don't do a D-bus call for each page, cache color profiles */
- scanned_page.color_profile = get_profile_for_device (info.device);
- }
-
- private void scanner_line_cb (Scanner scanner, ScanLine line)
- {
- scanned_page.parse_scan_line (line);
- }
-
- private void scanner_page_done_cb (Scanner scanner)
- {
- scanned_page.finish ();
- scanned_page = null;
- }
-
- private void remove_empty_page ()
- {
- var page = book.get_page ((int) book.n_pages - 1);
- if (!page.has_data)
- book.delete_page (page);
- }
-
- private void scanner_document_done_cb (Scanner scanner)
- {
- remove_empty_page ();
- }
-
- private void scanner_failed_cb (Scanner scanner, int error_code, string error_string)
- {
- remove_empty_page ();
- scanned_page = null;
- if (book.n_pages == 0)
- app.clear_document ();
- if (error_code != Sane.Status.CANCELLED)
- {
- app.show_error_dialog (/* Title of error dialog when scan failed */
- _("Failed to scan"),
- error_string);
- }
- }
-
- private uint inhibit_cookie;
- private FreedesktopScreensaver? fdss;
-
- private void scanner_scanning_changed_cb (Scanner scanner)
- {
- var is_scanning = scanner.is_scanning ();
-
- if (is_scanning)
- {
- /* Attempt to inhibit the screensaver when scanning */
- var reason = _("Scan in progress");
-
- /* This should work on Gnome, Budgie, Cinnamon, Mate, Unity, ...
- * but will not work on KDE, LXDE, XFCE, ... */
- inhibit_cookie = inhibit (app, Gtk.ApplicationInhibitFlags.IDLE, reason);
-
- if (inhibit_cookie == 0)
- {
- /* If the previous method didn't work, try the one
- * provided by Freedesktop. It should work with KDE,
- * LXDE, XFCE, and maybe others as well. */
- try
- {
- if ((fdss = FreedesktopScreensaver.get_proxy ()) != null)
- {
- inhibit_cookie = fdss.inhibit ("Simple-Scan", reason);
- }
- }
- catch (Error error) {}
- }
- }
- else
- {
- /* When finished scanning, uninhibit if inhibit was working */
- if (inhibit_cookie != 0)
- {
- if (fdss == null)
- uninhibit (inhibit_cookie);
- else
- {
- try
- {
- fdss.uninhibit (inhibit_cookie);
- }
- catch (Error error) {}
- fdss = null;
- }
-
- inhibit_cookie = 0;
- }
- }
-
- app.scanning = is_scanning;
- }
-
- private void scan_cb (AppWindow ui, string? device, ScanOptions options)
- {
- debug ("Requesting scan at %d dpi from device '%s'", options.dpi, device);
-
- if (!scanner.is_scanning ())
- // We need to add +1 to avoid visual glitches, fixes: #179
- append_page (options.paper_width + 1, options.paper_height + 1, options.dpi + 1);
-
- scanner.scan (device, options);
- }
-
- private void cancel_cb (AppWindow ui)
- {
- scanner.cancel ();
- }
-
- private void redetect_cb (AppWindow ui)
- {
- scanner.redetect ();
- }
-
-
- private static void log_cb (string? log_domain, LogLevelFlags log_level, string message)
- {
- string prefix;
-
- switch (log_level & LogLevelFlags.LEVEL_MASK)
- {
- case LogLevelFlags.LEVEL_ERROR:
- prefix = "ERROR:";
- break;
- case LogLevelFlags.LEVEL_CRITICAL:
- prefix = "CRITICAL:";
- break;
- case LogLevelFlags.LEVEL_WARNING:
- prefix = "WARNING:";
- break;
- case LogLevelFlags.LEVEL_MESSAGE:
- prefix = "MESSAGE:";
- break;
- case LogLevelFlags.LEVEL_INFO:
- prefix = "INFO:";
- break;
- case LogLevelFlags.LEVEL_DEBUG:
- prefix = "DEBUG:";
- break;
- default:
- prefix = "LOG:";
- break;
- }
-
- log_file.printf ("[%+.2fs] %s %s\n", log_timer.elapsed (), prefix, message);
- if (debug_enabled)
- stderr.printf ("[%+.2fs] %s %s\n", log_timer.elapsed (), prefix, message);
- }
-
- private static void fix_pdf (string filename) throws Error
- {
- uint8[] data;
- FileUtils.get_data (filename, out data);
-
- var fixed_file = FileStream.open (filename + ".fixed", "w");
-
- var offset = 0;
- var line_number = 0;
- var xref_offset = 0;
- var xref_line = -1;
- var startxref_line = -1;
- var fixed_size = -1;
- var line = new StringBuilder ();
- while (offset < data.length)
- {
- var end_offset = offset;
- line.assign ("");
- while (end_offset < data.length)
- {
- var c = data[end_offset];
- line.append_c ((char) c);
- end_offset++;
- if (c == '\n')
- break;
- }
-
- if (line.str == "startxref\n")
- startxref_line = line_number;
-
- if (line.str == "xref\n")
- xref_line = line_number;
-
- /* Fix PDF header and binary comment */
- if (line_number < 2 && line.str.has_prefix ("%%"))
- {
- xref_offset--;
- fixed_file.printf ("%s", line.str.substring (1));
- }
-
- /* Fix xref subsection count */
- else if (line_number == xref_line + 1 && line.str.has_prefix ("1 "))
- {
- fixed_size = int.parse (line.str.substring (2)) + 1;
- fixed_file.printf ("0 %d\n", fixed_size);
- fixed_file.printf ("0000000000 65535 f \n");
- }
-
- /* Fix xref format */
- else if (line_number > xref_line && line.str.has_suffix (" 0000 n\n"))
- fixed_file.printf ("%010d 00000 n \n", int.parse (line.str) + xref_offset);
-
- /* Fix xref offset */
- else if (startxref_line > 0 && line_number == startxref_line + 1)
- fixed_file.printf ("%d\n".printf (int.parse (line.str) + xref_offset));
-
- else if (fixed_size > 0 && line.str.has_prefix ("/Size "))
- fixed_file.printf ("/Size %d\n".printf (fixed_size));
-
- /* Fix EOF marker */
- else if (line_number == startxref_line + 2 && line.str.has_prefix ("%%%%"))
- fixed_file.printf ("%s", line.str.substring (2));
-
- else
- for (var i = offset; i < end_offset; i++)
- fixed_file.putc ((char) data[i]);
-
- line_number++;
- offset = end_offset;
- }
-
- if (FileUtils.rename (filename, filename + "~") >= 0)
- FileUtils.rename (filename + ".fixed", filename);
- }
-
- public static int main (string[] args)
- {
- Intl.setlocale (LocaleCategory.ALL, "");
- Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALE_DIR);
- Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
- Intl.textdomain (GETTEXT_PACKAGE);
-
- var c = new OptionContext (/* Arguments and description for --help text */
- _("[DEVICE…] — Scanning utility"));
- c.add_main_entries (options, GETTEXT_PACKAGE);
- try
- {
- c.parse (ref args);
- }
- catch (Error e)
- {
- stderr.printf ("%s\n", e.message);
- stderr.printf (/* Text printed out when an unknown command-line argument provided */
- _("Run “%s --help” to see a full list of available command line options."), args[0]);
- stderr.printf ("\n");
- return Posix.EXIT_FAILURE;
- }
- if (show_version)
- {
- /* Note, not translated so can be easily parsed */
- stderr.printf ("simple-scan %s\n", VERSION);
- return Posix.EXIT_SUCCESS;
- }
- if (fix_pdf_filename != null)
- {
- try
- {
- fix_pdf (fix_pdf_filename);
- for (var i = 1; i < args.length; i++)
- fix_pdf (args[i]);
- }
- catch (Error e)
- {
- stderr.printf ("Error fixing PDF file: %s\n", e.message);
- return Posix.EXIT_FAILURE;
- }
- return Posix.EXIT_SUCCESS;
- }
-
- ScanDevice? device = null;
- if (args.length > 1)
- {
- device = new ScanDevice ();
- device.name = args[1];
- device.label = args[1];
- }
-
- /* Log to a file */
- log_timer = new Timer ();
- var path = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", null);
- DirUtils.create_with_parents (path, 0700);
- path = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "simple-scan.log", null);
- log_file = FileStream.open (path, "w");
- if (log_file == null )
- {
- stderr.printf ("Error: Unable to open %s file for writing\n", path);
- return Posix.EXIT_FAILURE;
- }
- Log.set_default_handler (log_cb);
-
- debug ("Starting %s %s, PID=%i", args[0], VERSION, Posix.getpid ());
-
- Gtk.init ();
-
- var app = new SimpleScan (device);
- return app.run ();
- }
-}
+/*
+ * 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 class SimpleScan : Adw.Application
+{
+ static bool show_version;
+ static bool debug_enabled;
+ static string? fix_pdf_filename = null;
+ const OptionEntry[] options =
+ {
+ { "version", 'v', 0, OptionArg.NONE, ref show_version,
+ /* Help string for command line --version flag */
+ N_("Show release version"), null},
+ { "debug", 'd', 0, OptionArg.NONE, ref debug_enabled,
+ /* Help string for command line --debug flag */
+ N_("Print debugging messages"), null},
+ { "fix-pdf", 0, 0, OptionArg.STRING, ref fix_pdf_filename,
+ N_("Fix PDF files generated with older versions of this app"), "FILENAME…"},
+ { null }
+ };
+ private static Timer log_timer;
+ private static FileStream? log_file;
+
+ private ScanDevice? default_device = null;
+ private bool have_devices = false;
+ private GUsb.Context usb_context;
+ private AppWindow app;
+ private Scanner scanner;
+ private Book book;
+ private Page scanned_page;
+ private ThreadPool postprocessing_pool;
+
+ public SimpleScan (ScanDevice? device = null)
+ {
+ Object (
+ /* The inhibit () method use this */
+ application_id: "org.gnome.SimpleScan",
+ /* Icon resources will be looked up starting from here */
+ resource_base_path: "/org/gnome/SimpleScan"
+ );
+ register_session = true;
+
+ default_device = device;
+ }
+
+ public override void startup ()
+ {
+ base.startup ();
+
+ app = new AppWindow ();
+ book = app.book;
+ app.start_scan.connect (scan_cb);
+ app.stop_scan.connect (cancel_cb);
+ app.redetect.connect (redetect_cb);
+ app.device_selected.connect (device_selected_cb);
+
+ scanner = Scanner.get_instance ();
+ scanner.update_devices.connect (update_scan_devices_cb);
+ scanner.device_capabilities.connect (device_capabilities_cb);
+ scanner.request_authorization.connect (authorize_cb);
+ scanner.expect_page.connect (scanner_new_page_cb);
+ scanner.got_page_info.connect (scanner_page_info_cb);
+ scanner.got_line.connect (scanner_line_cb);
+ scanner.page_done.connect (scanner_page_done_cb);
+ scanner.document_done.connect (scanner_document_done_cb);
+ scanner.scan_failed.connect (scanner_failed_cb);
+ scanner.scanning_changed.connect (scanner_scanning_changed_cb);
+ scanner.display_page_delay.connect (display_page_delay_cb);
+
+ try
+ {
+ usb_context = new GUsb.Context ();
+ usb_context.device_added.connect (() => { scanner.redetect (); });
+ usb_context.device_removed.connect (() => { scanner.redetect (); });
+ }
+ catch (Error e)
+ {
+ warning ("Failed to create USB context: %s\n", e.message);
+ }
+
+ if (default_device != null)
+ {
+ List device_list = null;
+
+ device_list.append (default_device);
+ app.set_scan_devices (device_list);
+ app.set_selected_device (default_device.name);
+ }
+
+ app.start ();
+
+ /* Initialize thread pool for post-processing */
+ try {
+ postprocessing_pool = new ThreadPool.with_owned_data (
+ (PostProcessingTask) => PostProcessingTask.run (),
+ int.max((int) GLib.get_num_processors () - 1, 1),
+ true
+ );
+ } catch (Error e) {
+ warning ("Failed to create thread pool: %s\n", e.message);
+ }
+
+ scanner.start ();
+ }
+
+ public override void activate ()
+ {
+ base.activate ();
+ app.present ();
+ }
+
+ public override void shutdown ()
+ {
+ base.shutdown ();
+ book = null;
+ app = null;
+ usb_context = null;
+ scanner.free ();
+ }
+
+ private void device_selected_cb (AppWindow window, string device)
+ {
+ /* Query the device so the resolution and paper size menus can be adjusted */
+ scanner.get_capabilities (device);
+ }
+
+ private void device_capabilities_cb (Scanner scanner, string device, ScanDeviceCapabilities capabilities)
+ {
+ app.set_device_capabilities (device, capabilities);
+ }
+
+ private void update_scan_devices_cb (Scanner scanner, List devices)
+ {
+ var devices_copy = devices.copy_deep ((CopyFunc) Object.ref);
+
+ /* If the default device is not detected add it to the list */
+ if (default_device != null)
+ {
+ var default_in_list = false;
+ foreach (var device in devices_copy)
+ {
+ if (device.name == default_device.name)
+ {
+ default_in_list = true;
+ break;
+ }
+ }
+
+ if (!default_in_list)
+ devices_copy.prepend (default_device);
+ }
+
+ have_devices = devices_copy.length () > 0;
+
+ /* If SANE doesn't see anything, see if we recognise any of the USB devices */
+ string? missing_driver = null;
+ if (!have_devices)
+ missing_driver = suggest_driver ();
+
+ app.set_scan_devices (devices_copy, missing_driver);
+ }
+
+ /* Taken from /usr/local/Brother/sane/Brsane.ini from brscan driver */
+ private const uint32 brscan_devices[] = { 0x04f90110, 0x04f90111, 0x04f90112, 0x04f9011d, 0x04f9011e, 0x04f9011f, 0x04f9012b, 0x04f90124, 0x04f90153, 0x04f90125, 0x04f90113, 0x04f90114, 0x04f90115, 0x04f90116, 0x04f90119, 0x04f9011a, 0x04f9011b, 0x04f9011c, 0x04f9012e, 0x04f9012f, 0x04f90130, 0x04f90128, 0x04f90127, 0x04f90142, 0x04f90143, 0x04f90140, 0x04f90141, 0x04f9014e, 0x04f9014f, 0x04f90150, 0x04f90151, 0x04f9010e, 0x04f9013a, 0x04f90120, 0x04f9010f, 0x04f90121, 0x04f90122, 0x04f90132, 0x04f9013d, 0x04f9013c, 0x04f90136, 0x04f90135, 0x04f9013e, 0x04f9013f, 0x04f90144, 0x04f90146, 0x04f90148, 0x04f9014a, 0x04f9014b, 0x04f9014c, 0x04f90157, 0x04f90158, 0x04f9015d, 0x04f9015e, 0x04f9015f, 0x04f90160 };
+
+ /* Taken from /usr/local/Brother/sane/models2/*.ini from brscan2 driver */
+ private const uint32 brscan2_devices[] = { 0x04f901c9, 0x04f901ca, 0x04f901cb, 0x04f901cc, 0x04f901ec, 0x04f901e4, 0x04f901e3, 0x04f901e2, 0x04f901e1, 0x04f901e0, 0x04f901df, 0x04f901de, 0x04f901dd, 0x04f901dc, 0x04f901db, 0x04f901da, 0x04f901d9, 0x04f901d8, 0x04f901d7, 0x04f901d6, 0x04f901d5, 0x04f901d4, 0x04f901d3, 0x04f901d2, 0x04f901d1, 0x04f901d0, 0x04f901cf, 0x04f901ce, 0x04f9020d, 0x04f9020c, 0x04f9020a };
+
+ /* Taken from /usr/local/Brother/sane/models3/*.ini from brscan3 driver */
+ private const uint32 brscan3_devices[] = { 0x04f90222, 0x04f90223, 0x04f90224, 0x04f90225, 0x04f90229, 0x04f9022a, 0x04f9022c, 0x04f90228, 0x04f90236, 0x04f90227, 0x04f9022b, 0x04f9022d, 0x04f9022e, 0x04f9022f, 0x04f90230, 0x04f9021b, 0x04f9021a, 0x04f90219, 0x04f9023f, 0x04f90216, 0x04f9021d, 0x04f9021c, 0x04f90220, 0x04f9021e, 0x04f9023e, 0x04f90235, 0x04f9023a, 0x04f901c9, 0x04f901ca, 0x04f901cb, 0x04f901cc, 0x04f901ec, 0x04f9020d, 0x04f9020c, 0x04f90257, 0x04f9025d, 0x04f90254, 0x04f9025b, 0x04f9026b, 0x04f90258, 0x04f9025e, 0x04f90256, 0x04f90240, 0x04f9025f, 0x04f90260, 0x04f90261, 0x04f90278, 0x04f9026f, 0x04f9026e, 0x04f9026d, 0x04f90234, 0x04f90239, 0x04f90253, 0x04f90255, 0x04f90259, 0x04f9025a, 0x04f9025c, 0x04f90276 };
+
+ /* Taken from /opt/brother/scanner/brscan4/models4/*.ini from brscan4 driver */
+ private const uint32 brscan4_devices[] = {
+ 0x04f90314, /* MFC-L9550CDW */
+ 0x04f90313, /* MFC-L8850CDW */
+ 0x04f90312, /* MFC-L8650CDW */
+ 0x04f90311, /* MFC-L8600CDW */
+ 0x04f90310, /* DCP-L8450CDW */
+ 0x04f9030f, /* DCP-L8400CDN */
+ 0x04f90366, /* MFC-J5520DW */
+ 0x04f90365, /* MFC-J4520DW */
+ 0x04f90364, /* MFC-J5625DW */
+ 0x04f90350, /* MFC-J5620CDW */
+ 0x04f9034f, /* MFC-J5820DN */
+ 0x04f9034e, /* MFC-J5720CDW */
+ 0x04f9034b, /* MFC-J4720N */
+ 0x04f90349, /* DCP-J4220N */
+ 0x04f90347, /* MFC-J5720DW */
+ 0x04f90346, /* MFC-J5620DW */
+ 0x04f90343, /* MFC-J5320DW */
+ 0x04f90342, /* MFC-J4625DW */
+ 0x04f90341, /* MFC-J2720 */
+ 0x04f90340, /* MFC-J4620DW */
+ 0x04f9033d, /* MFC-J4420DW */
+ 0x04f9033c, /* MFC-J2320 */
+ 0x04f9033a, /* MFC-J4320DW */
+ 0x04f90339, /* DCP-J4120DW */
+ 0x04f90392, /* MFC-L2705DW */
+ 0x04f90373, /* MFC-L2700D */
+ 0x04f9036e, /* MFC-7889DW */
+ 0x04f9036d, /* MFC-7880DN */
+ 0x04f9036c, /* MFC-7480D */
+ 0x04f9036b, /* MFC-7380 */
+ 0x04f9036a, /* DCP-7189DW */
+ 0x04f90369, /* DCP-7180DN */
+ 0x04f90368, /* DCP-7080 */
+ 0x04f90367, /* DCP-7080D */
+ 0x04f90338, /* MFC-L2720DN */
+ 0x04f90337, /* MFC-L2720DW */
+ 0x04f90335, /* FAX-L2700DN */
+ 0x04f90331, /* MFC-L2700DW */
+ 0x04f90330, /* HL-L2380DW */
+ 0x04f90329, /* DCP-L2560DW */
+ 0x04f90328, /* DCP-L2540DW */
+ 0x04f90326, /* DCP-L2540DN */
+ 0x04f90324, /* DCP-L2520D */
+ 0x04f90322, /* DCP-L2520DW */
+ 0x04f90321, /* DCP-L2500D */
+ 0x04f90320, /* MFC-L2740DW */
+ 0x04f90372, /* MFC-9342CDW */
+ 0x04f90371, /* MFC-9332CDW */
+ 0x04f90370, /* MFC-9142CDN */
+ 0x04f9036f, /* DCP-9022CDW */
+ 0x04f90361, /* MFC-1919NW */
+ 0x04f90360, /* DCP-1618W */
+ 0x04f9035e, /* MFC-1910NW */
+ 0x04f9035d, /* MFC-1910W */
+ 0x04f9035c, /* DCP-1610NW */
+ 0x04f9035b, /* DCP-1610W */
+ 0x04f90379, /* DCP-1619 */
+ 0x04f90378, /* DCP-1608 */
+ 0x04f90376, /* DCP-1600 */
+ 0x04f9037a, /* MFC-1906 */
+ 0x04f9037b, /* MFC-1908 */
+ 0x04f90377, /* MFC-1900 */
+ 0x04f9037f, /* ADS-2600We */
+ 0x04f9037e, /* ADS-2500We */
+ 0x04f9037d, /* ADS-2100e */
+ 0x04f9037c, /* ADS-2000e */
+ 0x04f9035a, /* MFC-J897DN */
+ 0x04f90359, /* MFC-J827DN */
+ 0x04f90358, /* MFC-J987DN */
+ 0x04f90357, /* MFC-J727D */
+ 0x04f90356, /* MFC-J877N */
+ 0x04f90355, /* DCP-J957N */
+ 0x04f90354, /* DCP-J757N */
+ 0x04f90353, /* DCP-J557N */
+ 0x04f90351, /* DCP-J137N */
+ 0x04f90390, /* MFC-J5920DW */
+ 0x04f903b3, /* MFC-J6925DW */
+ 0x04f90396, /* MFC-T800W */
+ 0x04f90395, /* DCP-T700W */
+ 0x04f90394, /* DCP-T500W */
+ 0x04f90393, /* DCP-T300 */
+ 0x04f90380, /* DCP-J562DW */
+ 0x04f90381, /* DCP-J562N */
+ 0x04f903bd, /* DCP-J762N */
+ 0x04f90383, /* DCP-J962N */
+ 0x04f90397, /* DCP-J963N */
+ 0x04f90386, /* MFC-J460DW */
+ 0x04f90384, /* MFC-J480DW */
+ 0x04f90385, /* MFC-J485DW */
+ 0x04f90388, /* MFC-J680DW */
+ 0x04f90389, /* MFC-J880DW */
+ 0x04f9038b, /* MFC-J880N */
+ 0x04f9038a, /* MFC-J885DW */
+ 0x04f9038c, /* MFC-J730DN */
+ 0x04f9038e, /* MFC-J830DN */
+ 0x04f9038f, /* MFC-J900DN */
+ 0x04f9038d, /* MFC-J990DN */
+ 0x04f903bc, /* MFC-L2700DN */
+ 0x04f903bb, /* MFC-L2680W */
+ 0x04f903b6, /* MFC-J6990CDW */
+ 0x04f903b5, /* MFC-J6973CDW */
+ 0x04f903b4, /* MFC-J6573CDW */
+ 0x04f9034a, /* DCP-J4225N */
+ 0x04f9034c, /* MFC-J4725N */
+ 0x04f903c5, /* MFC-9335CDW */
+ 0x04f903c1, /* HL-3180CDW */
+ 0x04f903c0, /* DCP-9015CDW */
+ 0x04f903bf, /* DCP-9017CDW */
+ 0x04f903c7, /* MFC-L5702DW */
+ 0x04f903c6, /* MFC-L5700DW */
+ 0x04f903b2, /* MFC-L5755DW */
+ 0x04f903b1, /* MFC-L6902DW */
+ 0x04f903b0, /* MFC-L6900DW */
+ 0x04f903af, /* MFC-L6800DW */
+ 0x04f903ae, /* MFC-L6750DW */
+ 0x04f903ad, /* MFC-L6702DW */
+ 0x04f903ac, /* MFC-L6700DW */
+ 0x04f903ab, /* MFC-L5902DW */
+ 0x04f903aa, /* MFC-L5900DW */
+ 0x04f903a9, /* MFC-L5850DW */
+ 0x04f903a8, /* MFC-L5802DW */
+ 0x04f903a7, /* MFC-L5800DW */
+ 0x04f903a6, /* MFC-8540DN */
+ 0x04f903a5, /* MFC-L5750DW */
+ 0x04f903a3, /* MFC-8535DN */
+ 0x04f903a2, /* MFC-8530DN */
+ 0x04f903a0, /* MFC-L5700DN */
+ 0x04f9039f, /* DCP-L6600DW */
+ 0x04f9039e, /* DCP-L5652DN */
+ 0x04f9039d, /* DCP-L5650DN */
+ 0x04f9039c, /* DCP-L5602DN */
+ 0x04f9039b, /* DCP-L5600DN */
+ 0x04f9039a, /* DCP-L5502DN */
+ 0x04f90399, /* DCP-L5500DN */
+ 0x04f90398, /* DCP-L5500D */
+ 0x04f903ba, /* ADS-3600W */
+ 0x04f903b9, /* ADS-2800W */
+ 0x04f903b8, /* ADS-3000N */
+ 0x04f903b7, /* ADS-2400N */
+ 0x04f903ca, /* DCP-J983N */
+ 0x04f903c9, /* MFC-J985DW */
+ 0x04f903c8, /* DCP-J785DW */
+ 0x04f903f2, /* MFC-J997DN */
+ 0x04f903f1, /* MFC-J907DN */
+ 0x04f903f0, /* MFC-J887N */
+ 0x04f903ef, /* MFC-J837DN */
+ 0x04f903ee, /* MFC-J737DN */
+ 0x04f903ed, /* DCP-J968N */
+ 0x04f903eb, /* DCP-J767N */
+ 0x04f903ea, /* DCP-J567N */
+ 0x04f903e8, /* MFC-J5830DW */
+ 0x04f903e7, /* MFC-J2730DW */
+ 0x04f903e6, /* MFC-J2330DW */
+ 0x04f903e5, /* MFC-J5335DW */
+ 0x04f903e4, /* MFC-J6535DW */
+ 0x04f903e3, /* MFC-J3930DW */
+ 0x04f903e2, /* MFC-J3530DW */
+ 0x04f903e0, /* MFC-J6530DW */
+ 0x04f903d6, /* MFC-J5930DW */
+ 0x04f903d5, /* MFC-J5730DW */
+ 0x04f903d3, /* MFC-J5330DW */
+ 0x04f903d1, /* MFC-J6995CDW */
+ 0x04f903d0, /* MFC-J6980CDW */
+ 0x04f903cf, /* MFC-J6580CDW */
+ 0x04f903cd, /* MFC-J6730DW */
+ 0x04f903cc, /* MFC-J6935DW */
+ 0x04f903cb, /* MFC-J6930DW */
+ 0x04f903f7, /* DCP-L8410CDW */
+ 0x04f903f6, /* MFC-L8610CDW */
+ 0x04f903f5, /* MFC-L8690CDW */
+ 0x04f903f4, /* MFC-L8900CDW */
+ 0x04f903f3, /* MFC-L9570CDW */
+ 0x04f903fa, /* MFC-L2685DW */
+ 0x04f903e1, /* MFC-L2707DW */
+ 0x04f90290, /* MFC-J432W */
+ 0x04f9028f, /* MFC-J425W */
+ 0x04f9028d, /* MFC-J835DW */
+ 0x04f9028a, /* DCP-J925N */
+ 0x04f90284, /* MFC-J825N */
+ 0x04f90283, /* MFC-J825DW */
+ 0x04f90282, /* MFC-J625DW */
+ 0x04f90281, /* MFC-J430W */
+ 0x04f9027e, /* MFC-J955DN */
+ 0x04f9027d, /* DCP-J925DW */
+ 0x04f9027c, /* DCP-J725N */
+ 0x04f9027b, /* DCP-J725DW */
+ 0x04f90280, /* MFC-J435W */
+ 0x04f9027a, /* DCP-J525N */
+ 0x04f90279, /* DCP-J525W */
+ 0x04f9027f, /* MFC-J280W */
+ 0x04f90285, /* MFC-J705D */
+ 0x04f9029a, /* MFC-8690DW */
+ 0x04f9029f, /* MFC-9325CW */
+ 0x04f9029e, /* MFC-9125CN */
+ 0x04f90289, /* MFC-J5910CDW */
+ 0x04f90288, /* MFC-J5910DW */
+ 0x04f9043d, /* DCP-L2535DW */
+ 0x04f9043c, /* MFC-L2715DW */
+ 0x04f9043b, /* MFC-L2770DW */
+ 0x04f9043a, /* MFC-L2750DW */
+ 0x04f90439, /* MFC-L2730DW */
+ 0x04f90438, /* MFC-L2730DN */
+ 0x04f90437, /* MFC-L2717DW */
+ 0x04f90436, /* MFC-L2715D */
+ 0x04f90435, /* MFC-L2713DW */
+ 0x04f90434, /* MFC-L2710DW */
+ 0x04f90433, /* MFC-L2710DN */
+ 0x04f90432, /* MFC-L2690DW */
+ 0x04f90431, /* MFC-B7720DN */
+ 0x04f90430, /* MFC-B7715DW */
+ 0x04f9042e, /* MFC-B7700D */
+ 0x04f9042d, /* MFC-7895DW */
+ 0x04f9042c, /* MFC-7890DN */
+ 0x04f9042b, /* MFC-7490D */
+ 0x04f9042a, /* MFC-7390 */
+ 0x04f90429, /* HL-L2395DW */
+ 0x04f90428, /* HL-L2390DW */
+ 0x04f90427, /* FAX-L2710DN */
+ 0x04f90425, /* DCP-L2551DN */
+ 0x04f90424, /* DCP-L2550DW */
+ 0x04f90423, /* DCP-L2550DN */
+ 0x04f90422, /* DCP-L2537DW */
+ 0x04f90421, /* DCP-L2535D */
+ 0x04f90420, /* DCP-L2530DW */
+ 0x04f9041f, /* DCP-L2510D */
+ 0x04f9041e, /* DCP-B7535DW */
+ 0x04f9041d, /* DCP-B7530DN */
+ 0x04f9041c, /* DCP-B7520DW */
+ 0x04f9041b, /* DCP-B7500D */
+ 0x04f9041a, /* DCP-7195DW */
+ 0x04f90419, /* DCP-7190DN */
+ 0x04f90418, /* DCP-7095D */
+ 0x04f90417, /* DCP-7090 */
+ 0x04f90413, /* MFC-T910DW */
+ 0x04f90412, /* MFC-T810W */
+ 0x04f90411, /* DCP-T710W */
+ 0x04f90410, /* DCP-T510W */
+ 0x04f9040f, /* DCP-T310 */
+ 0x04f90408, /* MFC-J893N */
+ 0x04f90407, /* DCP-J973N */
+ 0x04f90406, /* DCP-J972N */
+ 0x04f90405, /* DCP-J572N */
+ 0x04f90404, /* MFC-J690DW */
+ 0x04f90403, /* MFC-J890DW */
+ 0x04f90400, /* DCP-J774DW */
+ 0x04f903ff, /* DCP-J772DW */
+ 0x04f903f8, /* MFC-J895DW */
+ 0x04f9043e, /* MFC-J775DW */
+ 0x04f9040e, /* MFC-J1500N */
+ 0x04f9040d, /* DCP-J988N */
+ 0x04f9040b, /* MFC-J1300DW */
+ 0x04f9040a, /* MFC-J995DW */
+ 0x04f90409, /* DCP-J1100DW */
+ 0x04f90402, /* MFC-J497DW */
+ 0x04f903fe, /* DCP-J572DW */
+ 0x04f903f9, /* MFC-J491DW */
+ 0x04f9044b, /* DCP-L3510CDW */
+ 0x04f9044a, /* HL-L3290CDW */
+ 0x04f90448, /* DCP-L3550CDW */
+ 0x04f90446, /* MFC-L3710CW */
+ 0x04f90445, /* MFC-L3730CDN */
+ 0x04f90442, /* MFC-L3745CDW */
+ 0x04f90441, /* MFC-L3750CDW */
+ 0x04f9043f, /* MFC-L3770CDW */
+ 0x04f90454, /* MFC-T4500DW */
+ 0x04f9044f, /* MFC-J6545DW */
+ 0x04f9044d, /* MFC-J5845DW */
+ 0x04f90462, /* MFC-J898N */
+ 0x04f90461, /* DCP-J978N */
+ 0x04f90460, /* DCP-J577N */
+ 0x04f9044c, /* DCP-L3551CDW */
+ 0x04f90443, /* MFC-L3735CDN */
+ 0x04f9045f, /* HL-J6000CDW */
+ 0x04f90457, /* MFC-J6999CDW */
+ 0x04f90456, /* MFC-J6997CDW */
+ 0x04f90453, /* HL-J6100DW */
+ 0x04f90452, /* HL-J6000DW */
+ 0x04f90451, /* MFC-J6947DW */
+ 0x04f90450, /* MFC-J6945DW */
+ 0x04f9044e, /* MFC-J5945DW */
+ 0x04f90466, /* MFC-J815DW */
+ 0x04f90465, /* MFC-J1605DN */
+ 0x04f90464, /* MFC-J998DN */
+ 0x04f90463, /* MFC-J738DN */
+ 0x04f90447, /* DCP-9030CDN */
+ 0x04f90444, /* MFC-9150CDN */
+ 0x04f90440, /* MFC-9350CDW */
+ 0x04f9045e, /* MFC-J6983CDW */
+ 0x04f9045d, /* MFC-J6583CDW */
+ 0x04f9045c, /* MFC-J5630CDW */
+ 0x04f90470, /* MFC-J903N */
+ 0x04f9046f, /* DCP-J982N */
+ 0x04f9046e, /* DCP-J981N */
+ 0x04f9046d, /* DCP-J582N */
+ 0x04f90467, /* MFC-J805DW */
+ 0x04f960a0, /* ADS-2000 */
+ 0x04f960a1, /* ADS-2100 */
+ 0x04f90293, /* DCP-8155DN */
+ 0x04f902b7, /* DCP-8157DN */
+ 0x04f90294, /* DCP-8250DN */
+ 0x04f90296, /* MFC-8520DN */
+ 0x04f90298, /* MFC-8910DW */
+ 0x04f902ba, /* MFC-8912DW */
+ 0x04f90299, /* MFC-8950DW */
+ 0x04f902bb, /* MFC-8952DW */
+ 0x04f902d4, /* MFC-8810DW */
+ 0x04f90291, /* DCP-8110DN */
+ 0x04f902ac, /* DCP-8110D */
+ 0x04f902b5, /* DCP-8112DN */
+ 0x04f90292, /* DCP-8150DN */
+ 0x04f902b6, /* DCP-8152DN */
+ 0x04f90295, /* MFC-8510DN */
+ 0x04f902b8, /* MFC-8512DN */
+ 0x04f9029c, /* MFC-8515DN */
+ 0x04f902cb, /* MFC-8710DW */
+ 0x04f902ca, /* MFC-8712DW */
+ 0x04f902a6, /* FAX-2940 */
+ 0x04f902a7, /* FAX-2950 */
+ 0x04f902ab, /* FAX-2990 */
+ 0x04f902a5, /* MFC-7240 */
+ 0x04f902a8, /* MFC-7290 */
+ 0x04f902a0, /* DCP-J140W */
+ 0x04f902c1, /* MFC-J960DN */
+ 0x04f902c0, /* DCP-J940N */
+ 0x04f902bf, /* MFC-J840N */
+ 0x04f902be, /* MFC-J710D */
+ 0x04f902bd, /* DCP-J740N */
+ 0x04f902bc, /* DCP-J540N */
+ 0x04f902b2, /* MFC-J810DN */
+ 0x04f90287, /* MFC-J860DN */
+ 0x04f902cf, /* DCP-7057W */
+ 0x04f902ce, /* DCP-7055W */
+ 0x04f902cd, /* MFC-J2510 */
+ 0x04f902c7, /* MFC-J4510N */
+ 0x04f902c6, /* DCP-J4210N */
+ 0x04f902c5, /* MFC-J4610DW */
+ 0x04f902c4, /* MFC-J4410DW */
+ 0x04f902b4, /* MFC-J4710DW */
+ 0x04f902b3, /* MFC-J4510DW */
+ 0x04f902c2, /* DCP-J4110DW */
+ 0x04f960a4, /* ADS-2500W */
+ 0x04f960a5, /* ADS-2600W */
+ 0x04f902cc, /* MFC-J2310 */
+ 0x04f902c8, /* MFC-J4910CDW */
+ 0x04f902c3, /* MFC-J4310DW */
+ 0x04f902d3, /* DCP-9020CDW */
+ 0x04f902b1, /* DCP-9020CDN */
+ 0x04f902b0, /* MFC-9340CDW */
+ 0x04f902af, /* MFC-9330CDW */
+ 0x04f902ae, /* MFC-9140CDN */
+ 0x04f902ad, /* MFC-9130CW */
+ 0x04f902d1, /* MFC-1810 */
+ 0x04f902d0, /* DCP-1510 */
+ 0x04f902fb, /* MFC-J875DW */
+ 0x04f902f1, /* MFC-J890DN */
+ 0x04f902f0, /* MFC-J980DN */
+ 0x04f902ef, /* MFC-J820DN */
+ 0x04f902ed, /* MFC-J870N */
+ 0x04f902ec, /* MFC-J870DW */
+ 0x04f902ee, /* MFC-J720D */
+ 0x04f902eb, /* MFC-J650DW */
+ 0x04f902e9, /* MFC-J475DW */
+ 0x04f902e8, /* MFC-J470DW */
+ 0x04f902fa, /* MFC-J450DW */
+ 0x04f902ea, /* MFC-J285DW */
+ 0x04f902e6, /* DCP-J952N */
+ 0x04f902e5, /* DCP-J752N */
+ 0x04f902e4, /* DCP-J752DW */
+ 0x04f902e3, /* DCP-J552N */
+ 0x04f902e2, /* DCP-J552DW */
+ 0x04f902f9, /* DCP-J132N */
+ 0x04f902de, /* DCP-J132W */
+ 0x04f902e0, /* DCP-J152N */
+ 0x04f902df, /* DCP-J152W */
+ 0x04f902e1, /* DCP-J172W */
+ 0x04f902e7, /* MFC-J245 */
+ 0x04f902fc, /* DCP-J100 */
+ 0x04f902fd, /* DCP-J105 */
+ 0x04f902fe, /* MFC-J200 */
+ 0x04f902dd, /* DCP-J4215N */
+ 0x04f902c9, /* MFC-J4810DN */
+ 0x04f902ff, /* MFC-J3520 */
+ 0x04f90300, /* MFC-J3720 */
+ 0x04f902f2, /* MFC-J6520DW */
+ 0x04f902f3, /* MFC-J6570CDW */
+ 0x04f902f4, /* MFC-J6720DW */
+ 0x04f902f8, /* MFC-J6770CDW */
+ 0x04f902f5, /* MFC-J6920DW */
+ 0x04f902f6, /* MFC-J6970CDW */
+ 0x04f902f7, /* MFC-J6975CDW */
+ 0x04f90318, /* MFC-7365DN */
+ 0x04f960a6, /* ADS-1000W */
+ 0x04f960a7, /* ADS-1100W */
+ 0x04f960a8, /* ADS-1500W */
+ 0x04f960a9, /* ADS-1600W */
+ };
+
+ /* Taken from backend/pixma/pixma_mp150.c pixma_mp730.c pixma_mp750.c pixma_mp800.c in the pixma SANE backend repository */
+ /* Canon Pixma IDs extracted using the following Python script
+ import sys
+ for f in sys.argv:
+ for l in open(f, "r").readlines():
+ tokens=l.split ()
+ if len (tokens) >= 3 and tokens[0].startswith("#define") and tokens[1].endswith("_PID") and tokens[2].startswith("0x") and not tokens[2].endswith("ffff"):
+ print ( "0x04a9" + tokens[2][2:] + ", /* " + tokens[1][:-4] + " * /")
+ */
+ private const uint32 pixma_devices[] = {
+ 0x04a91709, /* MP150 */
+ 0x04a9170a, /* MP170 */
+ 0x04a9170b, /* MP450 */
+ 0x04a9170c, /* MP500 */
+ 0x04a91712, /* MP530 */
+ 0x04a91714, /* MP160 */
+ 0x04a91715, /* MP180 */
+ 0x04a91716, /* MP460 */
+ 0x04a91717, /* MP510 */
+ 0x04a91718, /* MP600 */
+ 0x04a91719, /* MP600R */
+ 0x04a9172b, /* MP140 */
+ 0x04a9171c, /* MX7600 */
+ 0x04a91721, /* MP210 */
+ 0x04a91722, /* MP220 */
+ 0x04a91723, /* MP470 */
+ 0x04a91724, /* MP520 */
+ 0x04a91725, /* MP610 */
+ 0x04a91727, /* MX300 */
+ 0x04a91728, /* MX310 */
+ 0x04a91729, /* MX700 */
+ 0x04a9172c, /* MX850 */
+ 0x04a9172e, /* MP630 */
+ 0x04a9172f, /* MP620 */
+ 0x04a91730, /* MP540 */
+ 0x04a91731, /* MP480 */
+ 0x04a91732, /* MP240 */
+ 0x04a91733, /* MP260 */
+ 0x04a91734, /* MP190 */
+ 0x04a91735, /* MX860 */
+ 0x04a91736, /* MX320 */
+ 0x04a91737, /* MX330 */
+ 0x04a9173a, /* MP250 */
+ 0x04a9173b, /* MP270 */
+ 0x04a9173c, /* MP490 */
+ 0x04a9173d, /* MP550 */
+ 0x04a9173e, /* MP560 */
+ 0x04a9173f, /* MP640 */
+ 0x04a91741, /* MX340 */
+ 0x04a91742, /* MX350 */
+ 0x04a91743, /* MX870 */
+ 0x04a91746, /* MP280 */
+ 0x04a91747, /* MP495 */
+ 0x04a91748, /* MG5100 */
+ 0x04a91749, /* MG5200 */
+ 0x04a9174a, /* MG6100 */
+ 0x04a9174d, /* MX360 */
+ 0x04a9174e, /* MX410 */
+ 0x04a9174f, /* MX420 */
+ 0x04a91750, /* MX880 */
+ 0x04a91751, /* MG2100 */
+ 0x04a91752, /* MG3100 */
+ 0x04a91753, /* MG4100 */
+ 0x04a91754, /* MG5300 */
+ 0x04a91755, /* MG6200 */
+ 0x04a91757, /* MP493 */
+ 0x04a91758, /* E500 */
+ 0x04a91759, /* MX370 */
+ 0x04a9175B, /* MX430 */
+ 0x04a9175C, /* MX510 */
+ 0x04a9175D, /* MX710 */
+ 0x04a9175E, /* MX890 */
+ 0x04a9175A, /* E600 */
+ 0x04a91763, /* MG4200 */
+ 0x04a9175F, /* MP230 */
+ 0x04a91765, /* MG6300 */
+ 0x04a91760, /* MG2200 */
+ 0x04a91761, /* E510 */
+ 0x04a91762, /* MG3200 */
+ 0x04a91764, /* MG5400 */
+ 0x04a91766, /* MX390 */
+ 0x04a91767, /* E610 */
+ 0x04a91768, /* MX450 */
+ 0x04a91769, /* MX520 */
+ 0x04a9176a, /* MX720 */
+ 0x04a9176b, /* MX920 */
+ 0x04a9176c, /* MG2400 */
+ 0x04a9176d, /* MG2500 */
+ 0x04a9176e, /* MG3500 */
+ 0x04a9176f, /* MG6500 */
+ 0x04a91770, /* MG6400 */
+ 0x04a91771, /* MG5500 */
+ 0x04a91772, /* MG7100 */
+ 0x04a91774, /* MX470 */
+ 0x04a91775, /* MX530 */
+ 0x04a91776, /* MB5000 */
+ 0x04a91777, /* MB5300 */
+ 0x04a91778, /* MB2000 */
+ 0x04a91779, /* MB2300 */
+ 0x04a9177a, /* E400 */
+ 0x04a9177b, /* E560 */
+ 0x04a9177c, /* MG7500 */
+ 0x04a9177e, /* MG6600 */
+ 0x04a9177f, /* MG5600 */
+ 0x04a91780, /* MG2900 */
+ 0x04a91788, /* E460 */
+ 0x04a91787, /* MX490 */
+ 0x04a91789, /* E480 */
+ 0x04a9178a, /* MG3600 */
+ 0x04a9178b, /* MG7700 */
+ 0x04a9178c, /* MG6900 */
+ 0x04a9178d, /* MG6800 */
+ 0x04a9178e, /* MG5700 */
+ 0x04a91792, /* MB2700 */
+ 0x04a91793, /* MB2100 */
+ 0x04a91794, /* G3000 */
+ 0x04a91795, /* G2000 */
+ 0x04a9179f, /* TS9000 */
+ 0x04a91800, /* TS8000 */
+ 0x04a91801, /* TS6000 */
+ 0x04a91802, /* TS5000 */
+ 0x04a9180b, /* MG3000 */
+ 0x04a9180c, /* E470 */
+ 0x04a9181e, /* E410 */
+ 0x04a9181d, /* G4000 */
+ 0x04a91822, /* TS6100 */
+ 0x04a91825, /* TS5100 */
+ 0x04a91827, /* TS3100 */
+ 0x04a91828, /* E3100 */
+ 0x04a9178f, /* MB5400 */
+ 0x04a91790, /* MB5100 */
+ 0x04a91820, /* TS9100 */
+ 0x04a91823, /* TR8500 */
+ 0x04a91824, /* TR7500 */
+ 0x04a9185c, /* TS9500 */
+ 0x04a91912, /* LIDE400 */
+ 0x04a91913, /* LIDE300 */
+ 0x04a91821, /* TS8100 */
+ 0x04a9183a, /* G2010 */
+ 0x04a9183b, /* G3010 */
+ 0x04a9183d, /* G4010 */
+ 0x04a9183e, /* TS9180 */
+ 0x04a9183f, /* TS8180 */
+ 0x04a91840, /* TS6180 */
+ 0x04a91841, /* TR8580 */
+ 0x04a91842, /* TS8130 */
+ 0x04a91843, /* TS6130 */
+ 0x04a91844, /* TR8530 */
+ 0x04a91845, /* TR7530 */
+ 0x04a91846, /* XK50 */
+ 0x04a91847, /* XK70 */
+ 0x04a91854, /* TR4500 */
+ 0x04a91855, /* E4200 */
+ 0x04a91856, /* TS6200 */
+ 0x04a91857, /* TS6280 */
+ 0x04a91858, /* TS6230 */
+ 0x04a91859, /* TS8200 */
+ 0x04a9185a, /* TS8280 */
+ 0x04a9185b, /* TS8230 */
+ 0x04a9185d, /* TS9580 */
+ 0x04a9185e, /* TR9530 */
+ 0x04a91863, /* G7000 */
+ 0x04a91865, /* G6000 */
+ 0x04a91866, /* G6080 */
+ 0x04a91869, /* GM4000 */
+ 0x04a91873, /* XK80 */
+ 0x04a9188b, /* TS5300 */
+ 0x04a9188c, /* TS5380 */
+ 0x04a9188d, /* TS6300 */
+ 0x04a9188e, /* TS6380 */
+ 0x04a9188f, /* TS7330 */
+ 0x04a91890, /* TS8300 */
+ 0x04a91891, /* TS8380 */
+ 0x04a91892, /* TS8330 */
+ 0x04a91893, /* XK60 */
+ 0x04a91894, /* TS6330 */
+ 0x04a918a2, /* TS3300 */
+ 0x04a918a3, /* E3300 */
+ 0x04a9261f, /* MP10 */
+ 0x04a9262f, /* MP730 */
+ 0x04a92630, /* MP700 */
+ 0x04a92635, /* MP5 */
+ 0x04a9263c, /* MP360 */
+ 0x04a9263d, /* MP370 */
+ 0x04a9263e, /* MP390 */
+ 0x04a9263f, /* MP375R */
+ 0x04a9264c, /* MP740 */
+ 0x04a9264d, /* MP710 */
+ 0x04a9265d, /* MF5730 */
+ 0x04a9265e, /* MF5750 */
+ 0x04a9265f, /* MF5770 */
+ 0x04a92660, /* MF3110 */
+ 0x04a926e6, /* IR1020 */
+ 0x04a91706, /* MP750 */
+ 0x04a91708, /* MP760 */
+ 0x04a91707, /* MP780 */
+ 0x04a9170d, /* MP800 */
+ 0x04a9170e, /* MP800R */
+ 0x04a91713, /* MP830 */
+ 0x04a9171a, /* MP810 */
+ 0x04a9171b, /* MP960 */
+ 0x04a91726, /* MP970 */
+ 0x04a91901, /* CS8800F */
+ 0x04a9172d, /* MP980 */
+ 0x04a91740, /* MP990 */
+ 0x04a91908, /* CS9000F */
+ 0x04a9174b, /* MG8100 */
+ 0x04a91756, /* MG8200 */
+ 0x04a9190d, /* CS9000F_MII */
+ };
+
+ /* Taken from uld/noarch/oem.conf in the Samsung SANE driver */
+ private const uint32 samsung_devices[] = { 0x04e83425, 0x04e8341c, 0x04e8342a, 0x04e8343d, 0x04e83456, 0x04e8345a, 0x04e83427, 0x04e8343a, 0x04e83428, 0x04e8343b, 0x04e83455, 0x04e83421, 0x04e83439, 0x04e83444, 0x04e8343f, 0x04e8344e, 0x04e83431, 0x04e8345c, 0x04e8344d, 0x04e83462, 0x04e83464, 0x04e83461, 0x04e83460, 0x04e8340e, 0x04e83435,
+ 0x04e8340f, 0x04e83441, 0x04e8344f, 0x04e83413, 0x04e8341b, 0x04e8342e, 0x04e83426, 0x04e8342b, 0x04e83433, 0x04e83440, 0x04e83434, 0x04e8345b, 0x04e83457, 0x04e8341f, 0x04e83453, 0x04e8344b, 0x04e83409, 0x04e83412, 0x04e83419, 0x04e8342c, 0x04e8343c, 0x04e83432, 0x04e8342d, 0x04e83430, 0x04e8342f,
+ 0x04e83446, 0x04e8341a, 0x04e83437, 0x04e83442, 0x04e83466, 0x04e8340d, 0x04e8341d, 0x04e83420, 0x04e83429, 0x04e83443, 0x04e83438, 0x04e8344c, 0x04e8345d, 0x04e83463, 0x04e83465, 0x04e83450, 0x04e83468, 0x04e83469, 0x04e83467, 0x04e8346b, 0x04e8346a, 0x04e8346e, 0x04e83471, 0x04e83472, 0x04e8347d,
+ 0x04e8347c, 0x04e8347e, 0x04e83481, 0x04e83482, 0x04e83331, 0x04e83332, 0x04e83483, 0x04e83484, 0x04e83485, 0x04e83478, 0x04e83325, 0x04e83327, 0x04e8346f, 0x04e83477, 0x04e83324, 0x04e83326, 0x04e83486, 0x04e83487, 0x04e83489};
+
+ /* Taken from uld/noarch/oem.conf in the HP/Samsung SANE driver
+ These devices are rebranded Samsung Multifunction Printers. */
+ private const uint32 smfp_devices[] = { 0x03F0AA2A, 0x03F0CE2A, 0x03F0C02A, 0x03F0EB2A, 0x03F0F22A };
+
+ /* Taken from /usr/share/hplip/data/models/models.dat in the HPAIO driver */
+ private const uint32 hpaio_devices[] = {
+ 0x04f92311, /* HP Officejet d125xi All-in-One Printer */
+ 0x04f99711, /* HP Photosmart All-in-One Printer - B010 */
+ 0x04f91311, /* HP Officejet v30 All-in-One Printer */
+ 0x04f91011, /* HP Officejet v40xi All-in-One Printer */
+ 0x04f90f11, /* HP Officejet v40 All-in-One Printer */
+ 0x04f91911, /* HP Officejet v45 All-in-One Printer */
+ 0x04f90011, /* HP Officejet g55 All-in-One Printer */
+ 0x04f90111, /* HP Officejet g55xi All-in-One Printer */
+ 0x04f90611, /* HP Officejet k60xi All-in-One Printer */
+ 0x04f90511, /* HP Officejet k60 All-in-One Printer */
+ 0x04f90811, /* HP Officejet k80xi All-in-One Printer */
+ 0x04f90711, /* HP Officejet k80 All-in-One Printer */
+ 0x04f90211, /* HP Officejet g85 All-in-One Printer */
+ 0x04f90311, /* HP Officejet g85xi All-in-One Printer */
+ 0x04f90411, /* HP Officejet g95 All-in-One Printer */
+ 0x04f9062a, /* HP LaserJet 100 Color MFP M175 */
+ 0x04f94912, /* HP Officejet 100 Mobile L411 */
+ 0x04f99911, /* HP Envy 100 D410 series */
+ 0x04f93802, /* HP Photosmart 100 Printer */
+ 0x04f97a11, /* HP Photosmart B109A series */
+ 0x04f98311, /* HP Deskjet Ink Advantage K109a Printer */
+ 0x04f97b11, /* HP Photosmart Wireless All-in-One Printer - B109n */
+ 0x04f9a711, /* HP Envy 110 e-All-in-One */
+ 0x04f98d11, /* HP Photosmart D110 Series Printer */
+ 0x04f98a11, /* HP Photosmart Wireless All-in-One Printer - B110 */
+ 0x04f9bb11, /* HP Envy 120 e-All-in-One */
+ 0x04f9222a, /* HP LaserJet Pro MFP M125r */
+ 0x04f9322a, /* HP LaserJet Pro MFP M127fp */
+ 0x04f93902, /* HP Photosmart 130 Printer */
+ 0x04f91002, /* HP Photosmart 140 Compact Photo Printer */
+ 0x04f9242a, /* HP Color LaserJet Pro MPF M176n */
+ 0x04f9332a, /* HP Color LaserJet Pro MPF M177fw */
+ 0x04f9122a, /* HP LaserJet Pro 200 color MFP M276nw */
+ 0x04f90c2a, /* HP LaserJet 200 Color MFP M275s */
+ 0x04f9132a, /* HP LaserJet Pro M251nw Color Printer */
+ 0x04f92c2a, /* HP LaserJet Pro M201dw Printer */
+ 0x04f97e11, /* HP Photosmart Plus All-in-One Printer - B209a */
+ 0x04f97811, /* HP Deskjet Ink Advantage K209a All-in-One Printer */
+ 0x04f98e11, /* HP Photosmart Plus B210 series */
+ 0x04f92d2a, /* HP LaserJet Pro MFP M225rdn */
+ 0x04f93502, /* HP Photosmart 230 Printer */
+ 0x04f91102, /* HP Photosmart 240 Compact Photo Printer */
+ 0x04f96112, /* HP Officejet Pro 251dw Printer */
+ 0x04f96212, /* HP Officejet Pro 276dw Multifunction Printer */
+ 0x04f95511, /* HP Deskjet F310 All-in-One Printer */
+ 0x04f90f2a, /* HP LaserJet 300 Color M351a */
+ 0x04f9082a, /* HP LaserJet 300 Color MFP M375nw */
+ 0x04f97311, /* HP Photosmart Premium Fax All-in-One Printer - C309a */
+ 0x04f97c11, /* HP Photosmart Premium Fax All-in-One Printer series -C309a */
+ 0x04f97d11, /* HP Photosmart Premium All-in-One Printer series - C309g */
+ 0x04f91d02, /* HP Photosmart A310 Compact Photo Printer */
+ 0x04f91202, /* HP Photosmart 320 Compact Photo Printer */
+ 0x04f91e02, /* HP Photosmart A320 Compact Photo Printer */
+ 0x04f91602, /* HP Photosmart 330 Series Compact Photo Printer */
+ 0x04f91302, /* HP Photosmart 370 Compact Photo Printer */
+ 0x04f91702, /* HP Photosmart 385 Compact Photo Printer */
+ 0x04f9152a, /* HP LaserJet 400 M401dne */
+ 0x04f9142a, /* HP LaserJet 400 MFP M425dw */
+ 0x04f99611, /* HP Photosmart Prem C410 series */
+ 0x04f91502, /* HP Photosmart 420 Compact Photo Printer */
+ 0x04f91902, /* HP Photosmart A430 Compact Photo Printer */
+ 0x04f91f02, /* HP Photosmart A440 Compact Photo Printer */
+ 0x04f90512, /* HP Deskjet 450ci Mobile Printer */
+ 0x04f9aa11, /* HP Officejet Pro X451 Printer series */
+ 0x04f9a311, /* HP Officejet Pro X451dn Printer */
+ 0x04f91312, /* HP Deskjet 460c Mobile Printer */
+ 0x04f91802, /* HP Photosmart 470 Series Compact Photo Printer */
+ 0x04f92812, /* HP Officejet H470 Mobile Printer */
+ 0x04f9bf11, /* HP Officejet Pro X476 Multifunction Printer series */
+ 0x04f9c011, /* HP Officejet Pro X476dw Multifunction Printer */
+ 0x04f9342a, /* HP Color Laserjet Pro MFP M476dw */
+ 0x04f99e17, /* HP LaserJet Enterprise 500 MFP M525 Series */
+ 0x04f99f17, /* HP LaserJet Enterprise 500 Color MFP M575 */
+ 0x04f9252a, /* HP LaserJet Pro 500 color MFP M570dw */
+ 0x04f9a417, /* HP LaserJet Enterprise 500 Color M551 */
+ 0x04f91a02, /* HP Photosmart A510 Compact Photo Printer */
+ 0x04f99e11, /* HP Photosmart Ink Adv K510 */
+ 0x04f99011, /* HP PhotoSmart eStn C510 Series */
+ 0x04f92602, /* HP Photosmart A522xi Compact Photo Printer */
+ 0x04f9272a, /* HP LaserJet Pro M521dn Multifunction Printer */
+ 0x04f92b02, /* HP Photosmart A532 Compact Photo Printer */
+ 0x04f91812, /* HP Officejet Pro K550dtwn Printer */
+ 0x04f9b211, /* HP Officejet Pro X551 Printer series */
+ 0x04f9352a, /* HP Officejet Enterprise Color X555dn Printer */
+ 0x04f92b2a, /* HP Officejet Enterprise Color X585dn Multifunction Printer */
+ 0x04f9362a, /* HP Officejet Enterprise Color Flow X585z Multifunction Printer */
+ 0x04f9a517, /* HP LaserJet Enterprise 600 M601n */
+ 0x04f91b02, /* HP Photosmart A610 Compact Photo Printer */
+ 0x04f92702, /* HP Photosmart A620 Compact Photo Printer */
+ 0x04f92c02, /* HP Photosmart A636 Compact Photo Printer */
+ 0x04f9282a, /* HP LaserJet Enterprise MFP M630dn */
+ 0x04f92104, /* HP Deskjet 630c Printer */
+ 0x04f9432a, /* HP LaserJet Enterprise Flow MFP M630z */
+ 0x04f92004, /* HP Deskjet 640c Lite Printer */
+ 0x04f91a2a, /* HP Color LaserJet Enterprise M651dn Printer */
+ 0x04f92304, /* HP Deskjet 656c Printer */
+ 0x04f91b2a, /* HP Color LaserJet Enterprise Multifunction M680dn Printer */
+ 0x04f9442a, /* HP Color LaserJet Enterprise Flow Multifunction M680z Printer */
+ 0x04f98904, /* HP Deskjet 694c Printer */
+ 0x04f9a617, /* HP LaserJet Enterprise 700 M712n */
+ 0x04f99a17, /* HP LaserJet Enterprise 700 color MFP M775dn */
+ 0x04f9312a, /* HP LaserJet Pro M701a Printer */
+ 0x04f9452a, /* HP LaserJet Pro M706n Printer */
+ 0x04f91c02, /* HP Photosmart A712 Compact Photo Printer */
+ 0x04f91811, /* HP PSC 720 All-in-One Printer */
+ 0x04f99d17, /* HP LaserJet Enterprise MFP M725 series */
+ 0x04f92804, /* HP Deskjet D730 Printer */
+ 0x04f92904, /* HP Deskjet F735 All-in-One Printer */
+ 0x04f91511, /* HP PSC 750xi All-in-One Printer */
+ 0x04f91411, /* HP PSC 750 All-in-One Printer */
+ 0x04f9372a, /* HP Color LaserJet Enterprise M750 Printer series */
+ 0x04f90d14, /* HP Designjet T770 24-in Postscript Printer */
+ 0x04f91611, /* HP PSC 780 All-in-One Printer */
+ 0x04f91711, /* HP PSC 780xi All-in-One Printer */
+ 0x04f90f14, /* HP Designjet T790ps 24in */
+ 0x04f91f2a, /* HP LaserJet Enterprise M806 Printer Series */
+ 0x04f90304, /* HP Deskjet 810c Printer */
+ 0x04f90204, /* HP Deskjet 815c Printer */
+ 0x04f90804, /* HP Deskjet 816 Printer */
+ 0x04f92902, /* HP Photosmart A826 Home Photo Center */
+ 0x04f90704, /* HP Deskjet 825cvr Printer */
+ 0x04f91e2a, /* HP LaserJet Enterprise flow M830z Multifunction Printer */
+ 0x04f90404, /* HP Deskjet 830c Printer */
+ 0x04f90604, /* HP Deskjet 840c Printer */
+ 0x04f90904, /* HP Deskjet 845c Printer */
+ 0x04f91512, /* HP Officejet Pro K850 Printer */
+ 0x04f91c2a, /* HP Color LaserJet Enterprise M855 Printer series */
+ 0x04f90104, /* HP Deskjet 880c Printer */
+ 0x04f91d2a, /* HP Color LaserJet Enterprise flow M880 Multifunction Printer series */
+ 0x04f90004, /* HP Deskjet 895cse Printer */
+ 0x04f92604, /* HP 910 Printer */
+ 0x04f92704, /* HP 915 Inkjet All-in-One Printer */
+ 0x04f91804, /* HP Deskjet 916c Printer */
+ 0x04f91504, /* HP Deskjet 920c Printer */
+ 0x04f91f11, /* HP PSC 920 All-in-One Printer */
+ 0x04f91204, /* HP Deskjet 930c Printer */
+ 0x04f91604, /* HP Deskjet 940cvr Printer */
+ 0x04f91704, /* HP Deskjet 948c Printer */
+ 0x04f91104, /* HP Deskjet 950c Printer */
+ 0x04f91e11, /* HP PSC 950 All-in-One Printer */
+ 0x04f91304, /* HP Deskjet 955c Printer */
+ 0x04f91404, /* HP Deskjet 957c Printer */
+ 0x04f93104, /* HP Deskjet 960cse Printer */
+ 0x04f91004, /* HP Deskjet 970cxi Printer */
+ 0x04f93004, /* HP Deskjet 980cxi Printer */
+ 0x04f93304, /* HP Deskjet 990cxi Printer */
+ 0x04f95004, /* HP Deskjet 995c Printer */
+ 0x04f92e11, /* HP PSC 1000 Series */
+ 0x04f90517, /* HP LaserJet 1000 Printer */
+ 0x04f98811, /* HP Deskjet 1000 J110 Series */
+ 0x04f91712, /* Business Inkjet 1000 Printer */
+ 0x04f91317, /* HP LaserJet 1005 Printer */
+ 0x04f94117, /* HP LaserJet P1005 Printer */
+ 0x04f93217, /* HP LaserJet M1005 Multifunction Printer */
+ 0x04f93e17, /* HP LaserJet P1009 Printer */
+ 0x04f90c17, /* HP LaserJet 1010 Printer */
+ 0x04f9b511, /* HP Deskjet 1010 Printer */
+ 0x04f94217, /* HP Color LaserJet CM1015 Multifunction Printer */
+ 0x04f94317, /* HP Color LaserJet CM1017 Multifunction Printer */
+ 0x04f92b17, /* HP LaserJet 1020 Printer */
+ 0x04f93017, /* HP LaserJet 1022nw Printer */
+ 0x04f92d17, /* HP LaserJet 1022n Printer */
+ 0x04f92c17, /* HP LaserJet 1022 Printer */
+ 0x04f9112a, /* HP LaserJet Pro CP 1025nw Color Printer Series */
+ 0x04f90b2a, /* HP LaserJet Pro CP1025nw Color Printer Series */
+ 0x04f98911, /* HP Deskjet 1050 J410 All-in-One Printer */
+ 0x04f97c04, /* HP Deskjet 1100c Printer */
+ 0x04f90912, /* HP Business Inkjet 1100d Printer */
+ 0x04f93011, /* HP PSC 1110 All-in-One Printer */
+ 0x04f9032a, /* HP LaserJet Professional P1102w Printer */
+ 0x04f9002a, /* HP Laserjet Professional P1102 Printer */
+ 0x04f9102a, /* HP LaserJet Professional P 1102w Printer */
+ 0x04f93402, /* HP Photosmart 1115 Printer */
+ 0x04f95617, /* HP LaserJet M1120 Multifunction Printer */
+ 0x04f95717, /* HP LaserJet M1120n Multifunction Printer */
+ 0x04f9042a, /* HP LaserJet Professional M1132 Multifunction Printer */
+ 0x04f90f17, /* HP LaserJet 1150 Printer */
+ 0x04f94004, /* HP Color Inkjet cp1160 Printer */
+ 0x04f91e17, /* HP LaserJet 1160 Series Printer */
+ 0x04f90317, /* HP LaserJet 1200 Printer */
+ 0x04f90f12, /* HP Business Inkjet 1200dtn Printer */
+ 0x04f92f11, /* HP PSC 1200 All-in-One Printer */
+ 0x04f9052a, /* HP LaserJet Professional M1212nf Multifunction Printer */
+ 0x04f94717, /* HP Color LaserJet CP1215 Printer */
+ 0x04f93202, /* HP Photosmart 1215 Printer */
+ 0x04f90e2a, /* HP LaserJet Professional M1217nfW Multifunction Printer */
+ 0x04f9262a, /* HP Laserjet M1210 MFP Series */
+ 0x04f93302, /* HP Photosmart 1218 Printer */
+ 0x04f90417, /* HP LaserJet 1220se All-in-One Printer */
+ 0x04f90212, /* HP Deskjet 1220c Printer */
+ 0x04f91412, /* HP Deskjet 1280 Printer */
+ 0x04f91017, /* HP LaserJet 1300 Printer */
+ 0x04f97804, /* HP Deskjet D1311 Printer */
+ 0x04f93b11, /* HP PSC 1300 All-in-One Printer */
+ 0x04f91117, /* HP LaserJet 1300n Printer */
+ 0x04f93f11, /* HP PSC 1310 All-in-One Printer */
+ 0x04f94f17, /* HP Color LaserJet CM1312nfi Multifunction Printer */
+ 0x04f94e17, /* HP Color LaserJet CM1312 Multifunction Printer */
+ 0x04f93602, /* HP Photosmart 1315 Printer */
+ 0x04f95817, /* HP LaserJet M1319f Multifunction Printer */
+ 0x04f91d17, /* HP LaserJet 1320 Series Printer */
+ 0x04f93c11, /* HP PSC 1358 series */
+ 0x04f97904, /* HP Deskjet D1415 Printer */
+ 0x04f94d11, /* HP PSC 1401 All-in-One Printer */
+ 0x04f9072a, /* HP LaserJet Professional CM1411fn */
+ 0x04f94c11, /* HP PSC 1508 All-in-One Printer */
+ 0x04f9c111, /* HP Deskjet 1510 All-in-One Printer */
+ 0x04f94417, /* HP Color LaserJet CP1514n Printer */
+ 0x04f95017, /* HP Color LaserJet CP1518ni Printer */
+ 0x04f9022a, /* HP LaserJet Professional CP1521n */
+ 0x04f9012a, /* HP LaserJet M1536dnf MFP */
+ 0x04f9092a, /* HP LaserJet Professional P1566 */
+ 0x04f97f11, /* HP Deskjet D1620 Printer */
+ 0x04f94811, /* HP PSC 1600 All-in-One Printer */
+ 0x04f93a17, /* HP Color LaserJet 1600 Printer */
+ 0x04f90a2a, /* HP LaserJet Professional P1606dn Printer */
+ 0x04f90312, /* HP Color Inkjet cp1700 Printer */
+ 0x04f99411, /* HP Deskjet 2000 J210 series */
+ 0x04f99b11, /* HP Deskjet Ink Adv 2010 K010 */
+ 0x04f93917, /* HP LaserJet P2014 Printer */
+ 0x04f94a17, /* HP LaserJet P2014n Printer */
+ 0x04f93817, /* HP LaserJet P2015d Printer */
+ 0x04f9b911, /* HP Deskjet Ink Advantage 2020HC Printer */
+ 0x04f95417, /* HP Color LaserJet CP2025dn Printer */
+ 0x04f95217, /* HP Color LaserJet CP2025 Printer */
+ 0x04f95317, /* HP Color LaserJet CP2025n Printer */
+ 0x04f95d17, /* HP LaserJet P2035n Printer */
+ 0x04f98711, /* HP Deskjet 2050 J510 All-in-One Printer */
+ 0x04f95c17, /* HP LaserJet P2055dn Printer */
+ 0x04f99a11, /* HP Deskjet Ink Adv 2060 K110 */
+ 0x04f92811, /* HP PSC 2105 All-in-One Printer */
+ 0x04f97d04, /* HP Deskjet F2110 All-in-One Printer */
+ 0x04f92a11, /* HP PSC 2150 All-in-One Printer */
+ 0x04f92b11, /* HP PSC 2170 All-in-One Printer */
+ 0x04f90217, /* HP LaserJet 2200 Series Printer */
+ 0x04f92911, /* HP PSC 2200 All-in-One Printer */
+ 0x04f92404, /* HP Deskjet F2210 All-in-One Printer */
+ 0x04f93511, /* HP PSC 2300 Series All-in-One Printer */
+ 0x04f90812, /* HP Business Inkjet 2300 Printer */
+ 0x04f9c302, /* HP Deskjet D2320 Printer */
+ 0x04f90b17, /* HP LaserJet 2300 Series Printer */
+ 0x04f95917, /* HP Color LaserJet CM2320 Multifunction Printer */
+ 0x04f95a17, /* HP Color LaserJet CM2320nf Multifunction Printer */
+ 0x04f95b17, /* HP Color LaserJet CM2320fxi Multifunction Printer */
+ 0x04f94911, /* HP PSC 2350 All-in-One Printer */
+ 0x04f93611, /* HP PSC 2405 Photosmart All-in-One Printer */
+ 0x04f97611, /* HP Deskjet F2410 All-in-One Printer */
+ 0x04f97a04, /* HP Deskjet D2430 Printer */
+ 0x04f92517, /* HP LaserJet 2410 Printer */
+ 0x04f92917, /* HP LaserJet 2420 Printer */
+ 0x04f92a17, /* HP LaserJet 2430t Printer */
+ 0x04f91e04, /* HP 2500c Plus Printer */
+ 0x04f90717, /* HP Color LaserJet 2500 Printer */
+ 0x04f92504, /* HP Deskjet D2530 Printer */
+ 0x04f93711, /* HP PSC 2500 Photosmart All-in-One Printer */
+ 0x04f9ac11, /* HP Deskjet Ink Advantage 2510 All-in-One */
+ 0x04f9be11, /* HP Deskjet Ink Advantage 2520HC All-in-One */
+ 0x04f9c211, /* HP Deskjet 2540 All-in-One Printer */
+ 0x04f91c17, /* HP Color LaserJet 2550L Printer */
+ 0x04f94e11, /* HP Photosmart 2570 All-in-One Printer */
+ 0x04f92e17, /* HP Color LaserJet 2600n Printer */
+ 0x04f94511, /* HP Photosmart 2605 All-in-One Printer */
+ 0x04f98011, /* HP Deskjet D2660 Printer */
+ 0x04f90412, /* HP Business Inkjet 2600 Printer */
+ 0x04f93617, /* HP Color LaserJet 2605dtn Printer */
+ 0x04f92f17, /* HP Color LaserJet 2605 Printer */
+ 0x04f93117, /* HP Color LaserJet 2605dn Printer */
+ 0x04f9c911, /* HP Officejet 2620 All-in-One */
+ 0x04f9ca11, /* HP Deskjet Ink Advantage 2645 All-in-One Printer */
+ 0x04f94611, /* HP Photosmart 2710 All-in-One Printer */
+ 0x04f93c17, /* HP Color LaserJet 2700n Printer */
+ 0x04f93717, /* HP Color LaserJet 2700 Printer */
+ 0x04f92617, /* HP Color LaserJet 2800 All-in-One Printer */
+ 0x04f91112, /* HP Business Inkjet 2800 Printer */
+ 0x04f90612, /* HP Business Inkjet 3000 Printer */
+ 0x04f96717, /* HP Color LaserJet 3000 Printer */
+ 0x04f99511, /* HP Deskjet 3000 j310 series */
+ 0x04f97617, /* HP LaserJet P3004 Printer */
+ 0x04f97317, /* HP LaserJet P3005 Printer */
+ 0x04f98d17, /* HP LaserJet P3015 Printer */
+ 0x04f91617, /* HP LaserJet 3015 All-in-One Printer */
+ 0x04f97a17, /* HP LaserJet M3027 Multifunction Printer */
+ 0x04f97517, /* HP LaserJet M3035 Multifunction Printer */
+ 0x04f99311, /* HP Deskjet 3050 J610 series */
+ 0x04f9a011, /* HP Deskjet 3050A J611 series */
+ 0x04f93317, /* HP LaserJet 3052 All-in-One Printer */
+ 0x04f93417, /* HP LaserJet 3055 All-in-One Printer */
+ 0x04f9a211, /* HP Deskjet 3070 B611 series */
+ 0x04f95611, /* HP Photosmart C3110 All-in-One Printer */
+ 0x04f95011, /* HP Photosmart 3108 All-in-One Printer */
+ 0x04f95111, /* HP Photosmart 3207 All-in-One Printer */
+ 0x04f90117, /* HP LaserJet 3200 All-in-One Printer */
+ 0x04f90817, /* HP LaserJet 3300 Multifunction Printer */
+ 0x04f95211, /* HP Photosmart 3308 All-in-One Printer */
+ 0x04f97004, /* HP Deskjet 3320v Color Inkjet Printer */
+ 0x04f90917, /* HP LaserJet 3330 Multifunction Printer */
+ 0x04f91917, /* HP LaserJet 3380 All-in-One Printer */
+ 0x04f93517, /* HP LaserJet 3390 All-in-One Printer */
+ 0x04f97104, /* HP Deskjet 3420 Color Inkjet Printer */
+ 0x04f91517, /* HP Color LaserJet 3500n Printer */
+ 0x04f93112, /* HP Officejet J3508 All-in-One Printer */
+ 0x04f97817, /* HP Color LaserJet CP3505n Printer */
+ 0x04f9ad11, /* HP Deskjet Ink Advantage 3515 e-All-in-One */
+ 0x04f9b011, /* HP Deskjet 3520 e-All-in-One Series */
+ 0x04f98517, /* HP Color LaserJet CP3525 Printer */
+ 0x04f98a17, /* HP Color LaserJet CM3530 Multifunction Printer */
+ 0x04f9c711, /* HP Deskjet Ink Advantage 3540 e-All-in-One Printer Series */
+ 0x04f96117, /* HP Color LaserJet 3550 Printer */
+ 0x04f96917, /* HP Color LaserJet 3600 Printer */
+ 0x04f96812, /* HP Officejet Pro 3610 Black and White e-All-in-One */
+ 0x04f96d12, /* HP Officejet Pro 3620 Black and White e-All-in-One */
+ 0x04f97204, /* HP Deskjet 3650 Color Inkjet Printer */
+ 0x04f90a17, /* HP Color LaserJet 3700 Printer */
+ 0x04f97404, /* HP Deskjet 3740 Color Inkjet Printer */
+ 0x04f96817, /* HP Color LaserJet 3800 Printer */
+ 0x04f91b04, /* HP Deskjet 3810 Color Inkjet Printer */
+ 0x04f91a04, /* HP Deskjet 3816 Color Inkjet Printer */
+ 0x04f91c04, /* HP Deskjet 3819 Color Inkjet Printer */
+ 0x04f91904, /* HP Deskjet 3820 Color Inkjet Printer */
+ 0x04f97504, /* HP Deskjet 3843 Color Inkjet Printer */
+ 0x04f97604, /* HP Deskjet 3900 Color Inkjet Printer */
+ 0x04f90714, /* HP Designjet 4000ps */
+ 0x04f99c11, /* HP Officejet 4000 K210 Printer */
+ 0x04f97b17, /* HP Color LaserJet CP4005n Printer */
+ 0x04f98817, /* HP Color LaserJet CP4020 Series Printer */
+ 0x04f95711, /* HP Photosmart C4110 All-in-One Printer */
+ 0x04f97704, /* HP Deskjet D4145 Printer */
+ 0x04f97e04, /* HP Deskjet F4135 All-in-One Printer */
+ 0x04f93111, /* HP OfficeJet 4100 Series All-in-One Printer */
+ 0x04f96017, /* HP LaserJet 4150 Printer */
+ 0x04f93d11, /* HP Officejet 4200 All-in-One Printer */
+ 0x04f95c11, /* HP Photosmart C4205 All-in-One Printer */
+ 0x04f97b04, /* HP Deskjet D4245 Printer */
+ 0x04f96a17, /* HP LaserJet 4240n Printer */
+ 0x04f92417, /* HP LaserJet 4250 Printer */
+ 0x04f95411, /* HP Officejet 4308 All-in-One Printer */
+ 0x04f91f04, /* HP Deskjet D4360 Printer */
+ 0x04f96711, /* HP Photosmart C4340 All-in-One Printer */
+ 0x04f97417, /* HP LaserJet 4345 Multifunction Printer */
+ 0x04f99717, /* HP LaserJet M4349 MFP */
+ 0x04f92317, /* HP LaserJet 4350 Printer */
+ 0x04f96611, /* HP Photosmart C4380 All-in-One Printer */
+ 0x04f96c11, /* HP Photosmart C4410 All-in-One Printer */
+ 0x04f99d11, /* HP Officejet 4400 K410 All-in-One Printer */
+ 0x04f97711, /* HP Deskjet F4440 All-in-One Printer */
+ 0x04f94712, /* HP Officejet 4500 Desktop All-in-One Printer - G510a */
+ 0x04f98c11, /* HP Deskjet F4500 All-in-One Printer Series */
+ 0x04f95712, /* HP Officejet 4500 All-in-One Printer - K710 */
+ 0x04f92a12, /* HP Officejet J4524 All-in-One Printer */
+ 0x04f96b11, /* HP Photosmart C4540 All-in-One Printer */
+ 0x04f9c511, /* HP Envy 4500 e-All-in-One */
+ 0x04f92e12, /* HP Officejet 4500 G510n-z All-in-One Printer */
+ 0x04f9c411, /* HP DeskJet Ink Advantage 4515 e-All-in-One Printer */
+ 0x04f99917, /* HP Color LaserJet CM4540 Multifunction Printer */
+ 0x04f99c17, /* HP LaserJet M4555 MFP */
+ 0x04f97411, /* HP Photosmart C4640 All-in-One Printer */
+ 0x04f96c17, /* HP Color LaserJet 4610n Printer */
+ 0x04f95812, /* HP OfficeJet 4610 All-in-One Printer Series */
+ 0x04f96512, /* HP Deskjet Ink Advantage 4610 All-in-One Printer Series */
+ 0x04f96612, /* HP Deskjet Ink Advantage 4620 e-All-in-One Printer */
+ 0x04f96412, /* HP OfficeJet 4620 e-All-in-One Printer */
+ 0x04f9c611, /* HP Officejet 4630 e-All-in-One Printer */
+ 0x04f9c811, /* HP Deskjet Ink Advantage 4640 e-All-in-One Printer series */
+ 0x04f91a17, /* HP Color LaserJet 4650 Printer */
+ 0x04f92b12, /* HP Officejet J4660 All-in-One Printer */
+ 0x04f92c12, /* HP Officejet J4680c All-in-One Printer */
+ 0x04f97511, /* HP Photosmart C4740 All-in-One Printer */
+ 0x04f96217, /* HP Color LaserJet 4700 Printer */
+ 0x04f97d17, /* HP Color LaserJet CM4730 Multifunction Printer */
+ 0x04f96317, /* HP Color LaserJet 4730xs Multifunction Printer */
+ 0x04f97917, /* HP LaserJet M5025 Multifunction Printer */
+ 0x04f97217, /* HP LaserJet M5035 Multifunction Printer */
+ 0x04f9a117, /* HP LaserJet M5039 Multifunction Printer */
+ 0x04f9c802, /* HP Photosmart D5060 Printer */
+ 0x04f95811, /* HP Photosmart C5140 All-in-One Printer */
+ 0x04f9c402, /* HP Photosmart D5145 Printer */
+ 0x04f92411, /* HP Officejet 5100 All-in-One Printer */
+ 0x04f95d11, /* HP Photosmart C5240 All-in-One Printer */
+ 0x04f96417, /* HP LaserJet 5200 Printer */
+ 0x04f96617, /* HP LaserJet 5200L Printer */
+ 0x04f98917, /* HP LaserJet 5200LX Printer */
+ 0x04f95117, /* HP Color LaserJet CP5225 */
+ 0x04f97111, /* HP Photosmart C5370 All-in-One Printer */
+ 0x04f91f12, /* HP Officejet Pro K5300 Printer */
+ 0x04f96811, /* HP Photosmart D5345 Printer */
+ 0x04f92012, /* HP Officejet Pro K5400dn Printer */
+ 0x04f98604, /* HP Deskjet 5420v Photo Printer */
+ 0x04f96d11, /* HP Photosmart D5460 Printer */
+ 0x04f93a11, /* HP Officejet 5505 All-in-One Printer */
+ 0x04f93012, /* HP Officejet J5505 All-in-One Printer */
+ 0x04f98211, /* HP Deskjet D5545 Printer */
+ 0x04f97211, /* HP Photosmart C5540 All-in-One Printer */
+ 0x04f9a111, /* HP Photosmart 5510 e-All-in-One */
+ 0x04f9b411, /* HP Photosmart 5510d e-All-in-One */
+ 0x04f99b17, /* HP Color LaserJet CP5520 Series Printer */
+ 0x04f9b111, /* HP Photosmart 5520 e-All-in-One */
+ 0x04f9b611, /* HP Deskjet Ink Advantage 5525 e-All-in-One */
+ 0x04f9c311, /* HP ENVY 5530 e-All-in-One Printer */
+ 0x04f91f17, /* HP Color LaserJet 5550n Printer */
+ 0x04f96004, /* HP Deskjet 5550 Color Inkjet Printer */
+ 0x04f94f11, /* HP Officejet 5600 Series All-in-One Printer */
+ 0x04f9cc11, /* HP Envy 5640 e-All-in-One */
+ 0x04f96104, /* HP Deskjet 5650 Color Inkjet Printer */
+ 0x04f95b11, /* HP Officejet J5725 All-in-One Printer */
+ 0x04f98104, /* HP Deskjet 5700 Color Inkjet Printer */
+ 0x04f9cd11, /* HP Officejet 5740 e-All-in-One */
+ 0x04f9a004, /* HP Deskjet 5800 Color Inkjet Printer */
+ 0x04f98704, /* HP Deskjet 5938 Photo Printer */
+ 0x04f94312, /* HP Officejet 6000 Wireless Printer - E609n */
+ 0x04f94212, /* HP Officejet 6000 Printer - E609a */
+ 0x04f96f17, /* HP Color LaserJet CP6015dn Printer */
+ 0x04f97c17, /* HP Color LaserJet CM6030 Multifunction Printer */
+ 0x04f99517, /* HP Color LaserJet CM6049 MFP */
+ 0x04f95911, /* HP Photosmart C6150 All-in-One Printer */
+ 0x04f95e12, /* HP OfficeJet 6100 ePrinter H611a */
+ 0x04f90b14, /* HP Designjet z6100ps 60in photo */
+ 0x04f9c502, /* HP Photosmart D6160 Printer */
+ 0x04f92d11, /* HP Officejet 6105 All-in-One Printer */
+ 0x04f93404, /* HP Deskjet 6120 Color Inkjet Printer */
+ 0x04f94b11, /* HP Officejet 6200 All-in-One Printer */
+ 0x04f91014, /* HP Designjet z6200PS 42in Photo */
+ 0x04f96a11, /* HP Photosmart C6240 All-in-One Printer */
+ 0x04f97312, /* HP OfficeJet Pro 6230 ePrinter */
+ 0x04f97011, /* HP Photosmart C6324 All-in-One Printer */
+ 0x04f95311, /* HP Officejet 6301 All-in-One Printer */
+ 0x04f93312, /* HP Officejet J6405 All-in-One Printer */
+ 0x04f94412, /* HP Officejet 6500 All-in-One Printer - E709a */
+ 0x04f95412, /* HP Officejet 6500 E710n-z */
+ 0x04f95512, /* HP Officejet 6500 E710 */
+ 0x04f94512, /* HP Officejet 6500 Wireless All-in-One Printer - E709n */
+ 0x04f98204, /* HP Deskjet 6500 Color Inkjet Printer */
+ 0x04f9a511, /* HP Photosmart 6510 e-All-in-one */
+ 0x04f9af11, /* HP Photsmart 6520 e All-in-One */
+ 0x04f9ba11, /* HP Deskjet Ink Advantage 6525 e-All-in-One */
+ 0x04f98504, /* HP Deskjet 6600 Series Color Inkjet Printer */
+ 0x04f95d12, /* HP Officejet 6600 e-All-in-One Printer - H711a */
+ 0x04f95c12, /* HP Officejet 6700 Premium e-All-in-One Printer-H711n */
+ 0x04f98404, /* HP Deskjet 6800 Color Inkjet Printer */
+ 0x04f97412, /* HP OfficeJet 6800 e-All-in-one */
+ 0x04f97212, /* HP OfficeJet Pro 6830 e-All-in-one */
+ 0x04f98804, /* HP Deskjet 6980xi Printer */
+ 0x04f94612, /* HP Officejet 7000 E809 series */
+ 0x04f92611, /* HP Officejet 7100 All-in-One Printer */
+ 0x04f95a11, /* HP Photosmart C7150 All-in-One Printer */
+ 0x04f92612, /* HP Officejet K7100 Printer */
+ 0x04f9c602, /* HP Photosmart D7145 Printer */
+ 0x04f96012, /* HP Officejet 7110 Wide Format ePrinter */
+ 0x04f96911, /* HP Photosmart D7245 Printer */
+ 0x04f9b002, /* HP Photosmart 7260w Photo Printer */
+ 0x04f94111, /* HP Officejet 7205 All-in-One Printer */
+ 0x04f96511, /* HP Photosmart C7250 All-in-One Printer */
+ 0x04f94211, /* HP Officejet 7310 All-in-One Printer */
+ 0x04f92512, /* HP Officejet Pro L7300 Series All-in-One Printer */
+ 0x04f9c702, /* HP Photosmart D7345 Printer */
+ 0x04f92002, /* HP Photosmart 7345 Printer */
+ 0x04f94311, /* HP Officejet 7408 All-in-One Printer */
+ 0x04f9b802, /* HP Photosmart 7450 Photo Printer */
+ 0x04f95e11, /* HP Photosmart D7460 Printer */
+ 0x04f93412, /* HP Officejet Pro L7480 All-in-One Printer */
+ 0x04f92112, /* HP Officejet Pro L7500 Series All-in-One Printer */
+ 0x04f94812, /* HP Officejet 7500 E910 */
+ 0x04f96f11, /* HP Photosmart D7560 Printer */
+ 0x04f9a611, /* HP Photosmart 7510 e-All-in-One */
+ 0x04f9bc11, /* HP Photosmart 7520 e-All-in-One */
+ 0x04f93e02, /* HP Photosmart 7550 Printer */
+ 0x04f92212, /* HP Officejet Pro L7600 Series All-in-One Printer */
+ 0x04f9b202, /* HP Photosmart 7655 Photo Printer */
+ 0x04f96e12, /* HP Officejet 7610 Wide Format e-All-in-One Printer */
+ 0x04f9dc11, /* HP Envy 7640 e-All-in-One */
+ 0x04f92312, /* HP Officejet Pro L7700 Series All-in-One Printer */
+ 0x04f9b402, /* HP Photosmart 7755 Photo Printer */
+ 0x04f9c002, /* HP Photosmart 7830 Printer */
+ 0x04f9b602, /* HP Photosmart 7960 Photo Printer */
+ 0x04f9d011, /* HP Envy 8000 e-All-in-One */
+ 0x04f95612, /* HP Officejet Pro 8000 Enterprise A811a */
+ 0x04f93612, /* HP Officejet Pro 8000 Printer - A809a */
+ 0x04f9c102, /* HP Photosmart 8030 Printer */
+ 0x04f9de11, /* HP OfficeJet 8040 e-All-in-One */
+ 0x04f97717, /* HP CM8050 Color Multifunction Printer with Edgeline Technology */
+ 0x04f97117, /* HP CM8060 Color Multifunction Printer with Edgeline Technology */
+ 0x04f95b12, /* HP OfficeJet Pro 8100 ePrinter-N811a */
+ 0x04f96411, /* HP Photosmart C8150 All-in-One Printer */
+ 0x04f9ba02, /* HP Photosmart 8150 Photo Printer */
+ 0x04f9c202, /* HP Photosmart 8230 Printer */
+ 0x04f9be02, /* HP Photosmart Pro B8330 Printer */
+ 0x04f9bb02, /* HP Photosmart 8450gp Photo Printer */
+ 0x04f93812, /* HP Officejet Pro 8500 All-in-One Printer - A909a */
+ 0x04f94012, /* HP Officejet Pro 8500 Premier All-in-One Printer - A909n */
+ 0x04f93912, /* HP Officejet Pro 8500 Wireless All-in-One Printer - A909g */
+ 0x04f9d102, /* HP Photosmart B8550 Photo Printer */
+ 0x04f95312, /* HP OfficeJet Pro 8500A Plus e-AiO Printer - A910g */
+ 0x04f92712, /* HP Officejet Pro K8600 Color Printer */
+ 0x04f95912, /* HP OfficeJet Pro 8600 e-AiO N911a */
+ 0x04f97112, /* HP OfficeJet Pro 8610 e-All-in-One Printer */
+ 0x04f97012, /* HP OfficeJet Pro 8620 e-All-in-One Printer */
+ 0x04f96f12, /* HP OfficeJet Pro 8630 e-All-in-One Printer */
+ 0x04f97712, /* HP OfficeJet Pro 8640 e-All-in-One Printer */
+ 0x04f97612, /* HP OfficeJet Pro 8660 e-All-in-One Printer */
+ 0x04f9bc02, /* HP Photosmart 8750 Professional Photo Printer */
+ 0x04f9d002, /* HP Photosmart Pro B8850 Printer */
+ 0x04f98417, /* HP LaserJet 9040 Multifunction Printer */
+ 0x04f92017, /* HP LaserJet 9040 Printer */
+ 0x04f92117, /* HP LaserJet 9050 Multifunction Printer */
+ 0x04f98317, /* HP LaserJet M9050 Multifunction Printer */
+ 0x04f99617, /* HP LaserJet M9059 MFP */
+ 0x04f90d12, /* HP Officejet 9110 All-in-One Printer */
+ 0x04f9bd02, /* HP Photosmart Pro B9180gp Photo Printer */
+ 0x04f92217, /* HP Color LaserJet 9500n Printer */
+ 0x04f90b12, /* HP Deskjet 9650 Printer */
+ 0x04f91212, /* HP Deskjet 9800 Printer */
+ 0x04f93c2a, /* HP Color LaserJet Pro M252n */
+ 0x04f9382a, /* HP Color LaserJet Enterprise M553n */
+ 0x04f9582a, /* HP Color LaserJet Enterprise M552dn */
+ 0x04f9552a, /* HP LaserJet Enterprise M604n */
+ 0x04f93e2a, /* HP LaserJet Enterprise M605n */
+ 0x04f93f2a, /* HP LaserJet Enterprise M606dn */
+ 0x04f9e311, /* HP DeskJet 3630 All-in-One Printer */
+ 0x04f9e111, /* HP DeskJet 2130 All-in-One Printer series */
+ 0x04f9df11, /* HP Deskjet 1110 Printer */
+ 0x04f9e511, /* HP OfficeJet 3830 All-in-One Printer */
+ 0x04f9e611, /* HP DeskJet Ink Advantage 3830 All-in-One Printer */
+ 0x04f9d911, /* HP OfficeJet 4650 All-in-One Printer series */
+ 0x04f9d711, /* HP ENVY 4520 All-in-One Printer series */
+ 0x04f9ce11, /* HP Envy 5540 All-in-One Printer series */
+ 0x04f9e811, /* HP Envy 4510 All-in-One */
+ 0x04f9842a, /* HP Color LaserJet Pro MFP M274n */
+ 0x04f9e211, /* HP DeskJet Ink Advantage Ultra 4720 All-in-One Printer series */
+ 0x04f9db11, /* HP DeskJet Ink Advantage 5640 All-in-One Printer series */
+ 0x04f9da11, /* HP DeskJet Ink Advantage 4670 All-in-One */
+ 0x04f9d811, /* HP DeskJet Ink Advantage 4530 All-in-One */
+ 0x04f9422a, /* HP LaserJet Enterprise M506 series */
+ 0x04f9542a, /* HP LaserJet Pro M402dw */
+ 0x04f9602a, /* HP LaserJet Pro M402n */
+ 0x04f9522a, /* HP Color Laserjet Pro M452dn */
+ 0x04f95a2a, /* HP Laserjet Pro MFP M426fdn */
+ 0x04f9402a, /* HP Laserjet Enterprise MFP M527dn */
+ 0x04f9412a, /* HP Laserjet Enterprise Flow MFP M527c */
+ 0x04f95305, /* HP Scanjet Pro 3500 f1 Flatbed Scanner */
+ 0x04f93a2a, /* HP Color LaserJet Enterprise MFP M577 Series */
+ 0x04f94a2a, /* HP Color LaserJet Enterprise Flow MFP M577 Series */
+ 0x04f9512a, /* HP Color Laserjet Pro MFP M477 fnw */
+ 0x04f9d611, /* HP PageWide Pro 577dw Multifunction Printer */
+ 0x04f9d311, /* HP PageWide Pro 552dw Printer */
+ 0x04f9d211, /* HP PageWide Pro 452dw Printer */
+ 0x04f9d111, /* HP PageWide Pro 452dn Printer */
+ 0x04f9d511, /* HP PageWide Pro 477dw Multifunction Printer */
+ 0x04f9d411, /* HP PageWide Pro 477dn Multifunction Printer */
+ 0x04f9ed11, /* HP DeskJet GT 5810 All-in-One Printer */
+ 0x04f9ee11, /* HP DeskJet GT 5820 All-in-One Printer */
+ 0x04f9e711, /* HP OfficeJet 200 Mobile Printer */
+ 0x04f97a12, /* HP OfficeJet Pro 8710 All-in-One Printer */
+ 0x04f96312, /* HP OfficeJet Pro 8740 All-in-One Printer */
+ 0x04f97b12, /* HP OfficeJet Pro 8720 All-in-One Printer */
+ 0x04f9652a, /* HP Laserjet Pro M501n */
+ 0x04f9832a, /* HP PageWide Enterprise Color MFP 586dn */
+ 0x04f9822a, /* HP PageWide Enterprise Color Flow MFP 586z */
+ 0x04f9fa11, /* HP PageWide Managed MFP P57750dw */
+ 0x04f9f911, /* HP PageWide Managed P55250dw */
+ 0x04f97d12, /* HP OfficeJet Pro 8210 */
+ 0x04f9862a, /* HP Color Laserjet MFP M377 fnw */
+ 0x04f9f511, /* HP DeskJet Ink Advantage Ultra 5738 All-in-One Printer */
+ 0x04f91254, /* HP ENVY Photo 6200 All-in-One */
+ 0x04f91154, /* HP ENVY Photo 7100 All-in-One */
+ 0x04f91054, /* HP ENVY Photo 7800 All-in-One */
+ 0x04f9e911, /* HP OfficeJet 250 Mobile All-in-One */
+ 0x04f90d54, /* HP OfficeJet Pro 6960 All-in-One */
+ 0x04f90c54, /* HP OfficeJet Pro 6970 All-in-One */
+ 0x04f903f0, /* HP DeskJet 3700 All-in-One */
+ 0x04f90e54, /* HP OfficeJet 6960 All-in-One */
+ 0x04f9802a, /* HP PageWide Managed Color E55650dn */
+ 0x04f91554, /* HP OfficeJet Pro 8732 All-in-One Printer */
+ 0x04f90954, /* HP OfficeJet 8702 All-in-One */
+ 0x04f9f211, /* HP PageWide MFP 377dw */
+ 0x04f90154, /* HP OfficeJet Pro 7740 Wide Format All-in-One */
+ 0x04f90f54, /* HP OfficeJet 6950 All-in-One */
+ 0x04f95605, /* HP Scanjet Pro 3000 S3 */
+ 0x04f95705, /* HP Scanjet Enterprise Flow 5000 S4 */
+ 0x04f95805, /* HP Scanjet Enterprise Flow 7000 S3 */
+ 0x04f9612a, /* HP LaserJet M102a */
+ 0x04f91654, /* HP OfficeJet Pro 7720 Wide Format All-in-One */
+ 0x04f91754, /* HP OfficeJet Pro 7730 Wide Format All-in-One */
+ 0x04f90853, /* HP ENVY 5000 All-in-One */
+ 0x04f90a54, /* HP DeskJet Ink Advantage 5075 All-in-One */
+ 0x04f9632a, /* HP LaserJet Pro M203d */
+ 0x04f9642a, /* HP LaserJet Pro MFP M227sdn */
+ 0x04f9622a, /* HP LaserJet Pro MFP M132a */
+ 0x04f90b53, /* HP DeskJet Ink Advantage 5275 All-in-One */
+ 0x04f9b22a, /* HP Color LaserJet Managed Flow MFP E77830z */
+ 0x04f9b32a, /* HP Color LaserJet Managed MFP E87640 dn */
+ 0x04f9b12a, /* HP LaserJet Managed MFP E82540dn */
+ 0x04f9b02a, /* HP LaserJet Managed MFP E72525dn */
+ 0x04f96b2a, /* HP LaserJet Enterprise M607n */
+ 0x04f96c2a, /* HP LaserJet Managed E60055dn */
+ 0x04f9672a, /* HP LaserJet Enterprise MFP M631dn */
+ 0x04f9682a, /* HP LaserJet Managed MFP E62555dn */
+ 0x04f9a32a, /* HP Color LaserJet Enterprise M652n */
+ 0x04f9a42a, /* HP Color LaserJet Managed E65050dn */
+ 0x04f9a52a, /* HP Color LaserJet Enterprise MFP M681dh */
+ 0x04f9a62a, /* HP Color LaserJet Managed MFP E67550dh */
+ 0x04f9fe11, /* HP PageWide Pro 750dn */
+ 0x04f9eb11, /* HP PageWide Pro MFP 772dw */
+ 0x04f9fc11, /* HP PageWide Managed MFP P77740zs */
+ 0x04f9f611, /* HP PageWide Managed P75050dn */
+ 0x04f9932a, /* HP LaserJet Pro MFP M26a */
+ 0x04f90753, /* HP DeskJet 2200 All-in-One */
+ 0x04f90053, /* HP DeskJet 2620 All-in-One */
+ 0x04f9b62a, /* HP PageWide Enterprise Color 765dn */
+ 0x04f9b72a, /* HP PageWide Managed Color E75160dn */
+ 0x04f90e53, /* HP AMP All-in-One */
+ 0x04f9b42a, /* HP PageWide Enterprise Color MFP 780dn */
+ 0x04f9b52a, /* HP PageWide Managed Color MFP E77650dn */
+ 0x04f9ba2a, /* HP Scanjet Enterprise Flow N9120 fn2 Document Scanner */
+ 0x04f9b92a, /* HP Digital Sender Flow 8500 fn2 Document Capture Workstation */
+ 0x04f9be2a, /* HP LaserJet Pro M15w */
+ 0x04f9bf2a, /* HP LaserJet Pro MFP M28w */
+ 0x04f9ac2a, /* HP Color LaserJet Pro M253a */
+ 0x04f9af2a, /* HP Color LaserJet Pro MFP M180nw */
+ 0x04f9ad2a, /* HP Color LaserJet Pro MFP M281fdw */
+ 0x04f9ae2a, /* HP Color LaserJet Pro M154a */
+ 0x04f9c92a, /* HP PageWide Managed Color MFP P77440dn */
+ 0x04f9c72a, /* HP PageWide Managed Color MFP P77450dn */
+ 0x04f90f53, /* HP Smart Tank 350 */
+ 0x04f91253, /* HP Smart Tank Wireless 450 */
+ 0x04f91053, /* HP Ink Tank 310 */
+ 0x04f91353, /* HP Ink Tank Wireless 410 */
+ 0x04f91453, /* HP Ink Tank 115 */
+ 0x04f9ef2a, /* HP PageWide 755dn */
+ 0x04f9ee2a, /* HP PageWide MFP 774dn */
+ 0x04f9e92a, /* HP LaserJet Pro M118dw */
+ 0x04f9ec2a, /* HP LaserJet Pro MFP M148dw */
+ 0x04f9ed2a, /* HP LaserJet Pro MFP M148fdw */
+ 0x04f98411, /* HP Scanjet Enterprise 7500 */
+ 0x04f92454, /* HP OfficeJet Pro All-in-One 9010 */
+ 0x04f92354, /* HP OfficeJet Pro All-in-One 9020 */
+ 0x04f92554, /* HP OfficeJet All-in-One 9010 */
+ 0x04f92654, /* HP OfficeJet Pro 8030 All-in-One Printer series */
+ 0x04f92854, /* HP OfficeJet 8020 All-in-One Printer series */
+ 0x04f92754, /* HP OfficeJet Pro 8020 All-in-One Printer series */
+ 0x04f92954, /* HP OfficeJet 8010 All-in-One Printer series */
+ 0x04f91c54, /* HP Smart Tank Plus 650 */
+ 0x04f91b54, /* HP Smart Tank 610 */
+ 0x04f91a54, /* HP Smart Tank Plus 550 */
+ 0x04f91954, /* HP Smart Tank 510 */
+ 0x04f9e32a, /* HP LaserJet Managed MFP E62655dn */
+ 0x04f9e02a, /* HP LaserJet Managed E60155dn */
+ 0x04f9e22a, /* HP Color LaserJet Managed MFP E67650dh */
+ 0x04f9e12a, /* HP Color LaserJet Managed E65150dn */
+ 0x04f9f42a, /* HP Neverstop Laser MFP 1200a */
+ 0x04f9f32a, /* HP Neverstop Laser 1000a */
+ 0x04f9f02a, /* HP Laser NS 1020 */
+ 0x04f9f12a, /* HP Laser NS MFP 1005 */
+ 0x04f92b54, /* HP Smart Tank 500 series */
+ 0x04f92a54, /* HP Smart Tank 530 series */
+ 0x04f92d54, /* HP Smart Tank Plus 570 series */
+ 0x04f9ca2a, /* HP LaserJet Enterprise M507n */
+ 0x04f9d22a, /* HP Laserjet Managed E50145dn */
+ 0x04f9cc2a, /* HP LaserJet Enterprise MFP M528dn */
+ 0x04f9d32a, /* HP LaserJet Managed MFP E52645dn */
+ 0x04f99d2a, /* HP Color LaserJet Enterprise M751n */
+ 0x04f99e2a, /* HP Color LaserJet Managed E75245dn */
+ 0x04f9de2a, /* HP LaserJet Pro M305d */
+ 0x04f9c12a, /* HP LaserJet Pro M404d */
+ 0x04f9c22a, /* HP LaserJet Pro MFP M428dw */
+ 0x04f9df2a, /* HP LaserJet Pro MFP M329dn */
+ 0x04f9c32a, /* HP LaserJet Pro MFP M428fdn */
+ 0x04f9c42a, /* HP Color LaserJet Pro M453cdn */
+ 0x04f9c62a, /* HP Color LaserJet Pro MFP M479dw */
+ 0x04f9c52a, /* HP Color LaserJet Pro MFP M478fcdn */
+ 0x04f99f2a, /* HP Color LaserJet Enterprise MFP M776dn */
+ 0x04f9a12a, /* HP Color laserjet Enterprise M856dn */
+ 0x04f9a22a, /* HP Color laserjet Managed E85055 */
+ 0x04f90c70, /* HP Color LaserJet Pro M155a */
+ 0x04f90a70, /* HP Color LaserJet Pro M256dn */
+ 0x04f90970, /* HP Color LaserJet Pro MFP M282nw */
+ 0x04f90870, /* HP Color LaserJet Pro MFP M182n */
+ 0x04f95a05, /* HP Scanjet Pro 2000 S2 */
+ 0x04f95e05, /* HP ScanJet Enterprise Flow N7000 snw1 */
+ 0x04f95c05, /* HP ScanJet Pro N4000 snw1 */
+ 0x04f95b05, /* HP ScanJet Pro 3000 s4 */
+ 0x04f95d05, /* HP ScanJet Enterprise Flow 5000 s5 */
+ };
+
+ /* Taken from epkowa.desc from iscan-data package for Epson driver */
+ private const uint32 epkowa_devices[] = { 0x04b80101, 0x04b80102, 0x04b80103, 0x04b80104, 0x04b80105, 0x04b80106, 0x04b80107, 0x04b80108, 0x04b80109, 0x04b8010a, 0x04b8010b, 0x04b8010c, 0x04b8010d, 0x04b8010e, 0x04b8010f, 0x04b80110, 0x04b80112, 0x04b80114, 0x04b80116, 0x04b80118, 0x04b80119, 0x04b8011a, 0x04b8011b, 0x04b8011c, 0x04b8011d, 0x04b8011e, 0x04b8011f, 0x04b80120, 0x04b80121, 0x04b80122, 0x04b80126, 0x04b80128, 0x04b80129, 0x04b8012a, 0x04b8012b, 0x04b8012c, 0x04b8012d, 0x04b8012e, 0x04b8012f, 0x04b80130, 0x04b80131, 0x04b80133, 0x04b80135, 0x04b80136, 0x04b80137, 0x04b80138, 0x04b8013a, 0x04b8013b, 0x04b8013c, 0x04b8013d, 0x04b80142, 0x04b80143, 0x04b80144, 0x04b80147, 0x04b8014a, 0x04b8014b, 0x04b80151, 0x04b80153, 0x04b80801, 0x04b80802, 0x04b80805, 0x04b80806, 0x04b80807, 0x04b80808, 0x04b8080a, 0x04b8080c, 0x04b8080d, 0x04b8080e, 0x04b8080f, 0x04b80810, 0x04b80811, 0x04b80813, 0x04b80814, 0x04b80815, 0x04b80817, 0x04b80818, 0x04b80819, 0x04b8081a, 0x04b8081c, 0x04b8081d, 0x04b8081f, 0x04b80820, 0x04b80821, 0x04b80827, 0x04b80828, 0x04b80829, 0x04b8082a, 0x04b8082b, 0x04b8082e, 0x04b8082f, 0x04b80830, 0x04b80831, 0x04b80833, 0x04b80834, 0x04b80835, 0x04b80836, 0x04b80837, 0x04b80838, 0x04b80839, 0x04b8083a, 0x04b8083c, 0x04b8083f, 0x04b80841, 0x04b80843, 0x04b80844, 0x04b80846, 0x04b80847, 0x04b80848, 0x04b80849, 0x04b8084a, 0x04b8084c, 0x04b8084d, 0x04b8084f, 0x04b80850, 0x04b80851, 0x04b80852, 0x04b80853, 0x04b80854, 0x04b80855, 0x04b80856, 0x04b8085c, 0x04b8085d, 0x04b8085e, 0x04b8085f, 0x04b80860, 0x04b80861, 0x04b80862, 0x04b80863, 0x04b80864, 0x04b80865, 0x04b80866, 0x04b80869, 0x04b8086a, 0x04b80870, 0x04b80871, 0x04b80872, 0x04b80873, 0x04b80878, 0x04b80879, 0x04b8087b, 0x04b8087c, 0x04b8087d, 0x04b8087e, 0x04b8087f, 0x04b80880, 0x04b80881, 0x04b80883, 0x04b80884, 0x04b80885, 0x04b8088f, 0x04b80890, 0x04b80891, 0x04b80892, 0x04b80893, 0x04b80894, 0x04b80895, 0x04b80896, 0x04b80897, 0x04b80898, 0x04b80899, 0x04b8089a, 0x04b8089b, 0x04b8089c, 0x04b8089d, 0x04b8089e, 0x04b8089f, 0x04b808a0, 0x04b808a1, 0x04b808a5, 0x04b808a6, 0x04b808a8, 0x04b808a9, 0x04b808aa, 0x04b808ab, 0x04b808ac, 0x04b808ad, 0x04b808ae, 0x04b808af, 0x04b808b0, 0x04b808b3, 0x04b808b4, 0x04b808b5, 0x04b808b6, 0x04b808b7, 0x04b808b8, 0x04b808b9, 0x04b808bd, 0x04b808be, 0x04b808bf, 0x04b808c0, 0x04b808c1, 0x04b808c3, 0x04b808c4, 0x04b808c5, 0x04b808c6, 0x04b808c7, 0x04b808c8, 0x04b808c9, 0x04b808ca, 0x04b808cd, 0x04b808d0 };
+
+
+ /* Taken from /usr/local/lexmark/unix_scan_drivers/etc/lexmark_nscan.conf */
+ /* Lexmark IDs extracted using command:
+ * grep -r "usb .* /usr" --no-filename --only-matching | sed 's/usb //' | sed 's/ 0x//' | sed 's/ \/usr/,/'
+ */
+ private const uint32 lexmark_nscan_devices[] = {
+ 0x043d0279,
+ 0x043d027a,
+ 0x043d01D6,
+ 0x043d01D7,
+ 0x043d01D8,
+ 0x043d01DC,
+ 0x043d01DE,
+ 0x043d01E0,
+ 0x043d01FA,
+ 0x043d01FB,
+ 0x043d01FC,
+ 0x043d01FD,
+ 0x043d01FE,
+ 0x043d01FF,
+ 0x043d01F4,
+ 0x043d0120,
+ 0x043d0121,
+ 0x043d0128,
+ 0x043d014F,
+ 0x043d0149,
+ 0x043d0152,
+ 0x043d0168,
+ 0x043d0169,
+ 0x043d016A,
+ 0x043d012D,
+ 0x043d01C4,
+ 0x043d01C5,
+ 0x043d01C6,
+ 0x043d01CF,
+ 0x043d01D0,
+ 0x043d01D1,
+ 0x043d01DB,
+ 0x043d01ED,
+ 0x043d01F1,
+ 0x043d01F5,
+ 0x043d0222,
+ 0x043d0223,
+ 0x043d0227,
+ 0x043d0228,
+ 0x043d022A,
+ 0x043d022B,
+ 0x043d022F,
+ 0x043d0230,
+ 0x043d0231,
+ 0x043d0234,
+ 0x043d0235,
+ 0x043d0244,
+ 0x043d0245,
+ 0x043d0246,
+ 0x043d0247,
+ 0x043d0248,
+ 0x043d024A,
+ 0x043d024E,
+ 0x043d024F
+ };
+
+ /* Brother IDs extracted using the following Python
+ *
+ * import sys
+ * for f in sys.argv:
+ * for l in file (f).readlines ():
+ * tokens = l.strip().split (',')
+ * if len (tokens) >= 4:
+ * print (' 0x%08x' % (0x04f9 << 16 | int (tokens[0], 16)) + ", /* " + tokens[3].strip("\"") + " * /")
+ */
+
+ /* HPAIO IDs extracted using the following Python:
+ import sys
+ ids = []
+ for f in sys.argv:
+ for l in file (f).readlines ():
+ if l.startswith ('model1='):
+ model=l[7:].strip ()
+ elif l.startswith ('usb-pid='):
+ pid = int (l[8:].strip (), 16)
+ if pid == 0:
+ continue
+ usb_id = '0x%08x' % (0x04f9 << 16 | pid)
+ if not usb_id in ids:
+ ids.append (usb_id)
+ print (usb_id + ", /* " + model + " * /")
+ */
+
+ public string? suggest_driver ()
+ {
+ if (usb_context == null)
+ return null;
+
+ var driver_map = new HashTable (direct_hash, direct_equal);
+ add_devices (driver_map, brscan_devices, "brscan");
+ add_devices (driver_map, brscan2_devices, "brscan2");
+ add_devices (driver_map, brscan3_devices, "brscan3");
+ add_devices (driver_map, brscan4_devices, "brscan4");
+ add_devices (driver_map, pixma_devices, "pixma");
+ add_devices (driver_map, samsung_devices, "samsung");
+ add_devices (driver_map, smfp_devices, "smfp");
+ add_devices (driver_map, hpaio_devices, "hpaio");
+ add_devices (driver_map, epkowa_devices, "epkowa");
+ add_devices (driver_map, lexmark_nscan_devices, "lexmark_nscan");
+ var devices = usb_context.get_devices ();
+ for (var i = 0; i < devices.length; i++)
+ {
+ var device = devices.data[i];
+ var driver = driver_map.lookup (device.get_vid () << 16 | device.get_pid ());
+ if (driver != null)
+ return driver;
+ }
+
+ return null;
+ }
+
+ private void add_devices (HashTable map, uint32[] devices, string driver)
+ {
+ for (var i = 0; i < devices.length; i++)
+ map.insert (devices[i], driver);
+ }
+
+ private void authorize_cb (Scanner scanner, string resource)
+ {
+ app.authorize.begin (resource, (obj, res) =>
+ {
+ var data = app.authorize.end(res);
+ if (data.success)
+ {
+ scanner.authorize (data.username, data.password);
+ }
+ });
+ }
+
+ private Page append_page (int width = 100, int height = 141, int dpi = 100)
+ {
+ /* Use current page if not used */
+ var page = book.get_page (-1);
+ if (page != null && !page.has_data)
+ {
+ app.selected_page = page;
+ page.start ();
+ return page;
+ }
+
+ /* Scan direction from the settings. */
+ ScanDirection scan_direction;
+ switch (app.page_orientation)
+ {
+ case PageOrientation.TOP:
+ scan_direction = ScanDirection.TOP_TO_BOTTOM;
+ break;
+ case PageOrientation.BOTTOM:
+ scan_direction = ScanDirection.BOTTOM_TO_TOP;
+ break;
+ case PageOrientation.LEFT:
+ scan_direction = ScanDirection.LEFT_TO_RIGHT;
+ break;
+ case PageOrientation.RIGHT:
+ scan_direction = ScanDirection.RIGHT_TO_LEFT;
+ break;
+ default:
+ scan_direction = ScanDirection.TOP_TO_BOTTOM;
+ break;
+ }
+
+ /* Copy info from previous page */
+ bool do_crop = false;
+ string named_crop = null;
+ var cx = 0, cy = 0, cw = 0, ch = 0;
+ if (page != null)
+ {
+ width = page.scan_width;
+ height = page.scan_height;
+ dpi = page.dpi;
+
+ do_crop = page.has_crop;
+ if (do_crop)
+ {
+ named_crop = page.crop_name;
+ cx = page.crop_x;
+ cy = page.crop_y;
+ cw = page.crop_width;
+ ch = page.crop_height;
+ }
+ }
+
+ page = new Page (width, height, dpi, scan_direction);
+ book.append_page (page);
+ if (do_crop)
+ {
+ if (named_crop != null)
+ {
+ /* only set the name - otherwise scaling breaks if dpi changes */
+ page.set_only_crop_name (named_crop);
+ }
+ page.set_custom_crop (cw, ch);
+ page.move_crop (cx, cy);
+ }
+ app.selected_page = page;
+ page.start ();
+
+ return page;
+ }
+
+ private void scanner_new_page_cb (Scanner scanner)
+ {
+ append_page ();
+ }
+
+ private string? get_profile_for_device (string device_name)
+ {
+#if HAVE_COLORD
+ var device_id = "sane:%s".printf (device_name);
+ debug ("Getting color profile for device %s", device_name);
+
+ var client = new Cd.Client ();
+ try
+ {
+ client.connect_sync ();
+ }
+ catch (Error e)
+ {
+ debug ("Failed to connect to colord: %s", e.message);
+ return null;
+ }
+
+ Cd.Device device;
+ try
+ {
+ device = client.find_device_by_property_sync (Cd.DEVICE_PROPERTY_SERIAL, device_id);
+ }
+ catch (Error e)
+ {
+ debug ("Unable to find colord device %s: %s", device_name, e.message);
+ return null;
+ }
+
+ try
+ {
+ device.connect_sync ();
+ }
+ catch (Error e)
+ {
+ debug ("Failed to get properties from the device %s: %s", device_name, e.message);
+ return null;
+ }
+
+ var profile = device.get_default_profile ();
+ if (profile == null)
+ {
+ debug ("No default color profile for device: %s", device_name);
+ return null;
+ }
+
+ try
+ {
+ profile.connect_sync ();
+ }
+ catch (Error e)
+ {
+ debug ("Failed to get properties from the profile %s: %s", device_name, e.message);
+ return null;
+ }
+
+ if (profile.filename == null)
+ {
+ debug ("No icc color profile for the device %s", device_name);
+ return null;
+ }
+
+ debug ("Using color profile %s for device %s", profile.filename, device_name);
+ return profile.filename;
+#else
+ return null;
+#endif
+ }
+
+ private void scanner_page_info_cb (Scanner scanner, ScanPageInfo info)
+ {
+ debug ("Page is %d pixels wide, %d pixels high, %d bits per pixel",
+ info.width, info.height, info.depth);
+
+ /* Add a new page */
+ scanned_page = append_page ();
+ scanned_page.set_page_info (info);
+
+ /* Get ICC color profile */
+ /* FIXME: The ICC profile could change */
+ /* FIXME: Don't do a D-bus call for each page, cache color profiles */
+ scanned_page.color_profile = get_profile_for_device (info.device);
+ }
+
+ private void scanner_line_cb (Scanner scanner, ScanLine line)
+ {
+ scanned_page.parse_scan_line (line);
+ }
+
+ private class PostProcessingTask
+ {
+ private int remove_empty;
+ private bool autorotate;
+ private bool color_reduce;
+ private bool alternating_page_orientation;
+ private int page_number;
+ private Page page;
+
+ public PostProcessingTask (Page page, int remove_empty, bool autorotate, bool color_reduce, bool alternating_page_orientation, int page_number)
+ {
+ this.page = page;
+ this.remove_empty = remove_empty;
+ this.autorotate = autorotate;
+ this.color_reduce = color_reduce;
+ this.alternating_page_orientation = alternating_page_orientation;
+ this.page_number = page_number;
+ }
+
+ public void run ()
+ {
+ this.page.has_no_text = this.page.is_empty_page (remove_empty);
+ if (autorotate)
+ {
+ this.page.deskew_text_page ();
+ }
+ if (color_reduce)
+ {
+ this.page.automatic_color_filter ();
+ }
+ if (alternating_page_orientation && (page_number % 2 == 1) && (page_number > 0))
+ {
+ this.page.rotate_right ();
+ this.page.rotate_right ();
+ }
+
+ /* scale down for preview */
+ this.page.pixels_are_being_edited = false;
+ this.page.update_pixels (false, true);
+ }
+ }
+
+ private void scanner_page_done_cb (Scanner scanner)
+ {
+ /* prevent scaling down for post-processing */
+ scanned_page.pixels_are_being_edited = true;
+ scanned_page.finish ();
+ var page_number = scanner.get_page_number ();
+ /* subtract 1 if ADF - this is one page more than expected */
+ if (app.get_scan_type () == ScanType.ADF)
+ page_number -= 1;
+
+ if (scanned_page != null && scanned_page.has_data)
+ {
+ var task_page = scanned_page;
+ /* internal post-processing */
+ try
+ {
+ postprocessing_pool.add (new PostProcessingTask(
+ task_page,
+ app.empty_page_threshold,
+ app.auto_rotation_correction,
+ app.auto_color_reduction,
+ app.alternating_page_orientation,
+ page_number
+ ));
+ }
+ catch (Error e)
+ {
+ warning ("Failed to add post-processing task to the thread pool: %s", e.message);
+ }
+ }
+
+ scanned_page = null;
+ app.update_info_label (scanner.get_page_number ());
+ }
+
+ private void remove_empty_page ()
+ {
+ var page = book.get_page (-1);
+ if (page == null || !page.has_data)
+ book.permanently_delete_page (page);
+ }
+
+ private void scanner_document_done_cb (Scanner scanner)
+ {
+ remove_empty_page ();
+
+ // only check if at least 2 pages were scanned
+ if (scanner.get_page_number() > 1)
+ {
+ check_empty_pages_after_document_done (scanner.get_page_number());
+ }
+ else {
+ var page = book.get_page (-1);
+ if (page != null)
+ page.has_no_text = false;
+ }
+ app.scan_finished ();
+ }
+
+ private void check_empty_pages_after_document_done (int scanned_pages)
+ {
+ /* Automatically mark empty pages as deleted */
+ for (int i = 0; i < book.n_pages; i++)
+ {
+ var page = book.get_page (i);
+ if (page.has_no_text)
+ page.set_page_is_deleted (true);
+ }
+ }
+
+ private void display_page_delay_cb (int delay)
+ {
+ app.display_page_delay_arc (delay);
+ }
+
+ private void scanner_failed_cb (Scanner scanner, int error_code, string error_string)
+ {
+ remove_empty_page ();
+ scanned_page = null;
+ if (book.n_pages == 0)
+ app.clear_document ();
+ if (error_code != Sane.Status.CANCELLED)
+ {
+ app.show_error_dialog (/* Title of error dialog when scan failed */
+ _("Failed to scan"),
+ error_string);
+ }
+ }
+
+ private uint inhibit_cookie;
+ private FreedesktopScreensaver? fdss;
+
+ private void scanner_scanning_changed_cb (Scanner scanner)
+ {
+ var is_scanning = scanner.is_scanning ();
+
+ if (is_scanning)
+ {
+ /* Attempt to inhibit the screensaver when scanning */
+ var reason = _("Scan in progress");
+
+ /* This should work on Gnome, Budgie, Cinnamon, Mate, Unity, ...
+ * but will not work on KDE, LXDE, XFCE, ... */
+ inhibit_cookie = inhibit (app, Gtk.ApplicationInhibitFlags.IDLE, reason);
+
+ if (inhibit_cookie == 0)
+ {
+ /* If the previous method didn't work, try the one
+ * provided by Freedesktop. It should work with KDE,
+ * LXDE, XFCE, and maybe others as well. */
+ try
+ {
+ if ((fdss = FreedesktopScreensaver.get_proxy ()) != null)
+ {
+ inhibit_cookie = fdss.inhibit ("Simple-Scan", reason);
+ }
+ }
+ catch (Error error) {}
+ }
+ }
+ else
+ {
+ /* When finished scanning, uninhibit if inhibit was working */
+ if (inhibit_cookie != 0)
+ {
+ if (fdss == null)
+ uninhibit (inhibit_cookie);
+ else
+ {
+ try
+ {
+ fdss.uninhibit (inhibit_cookie);
+ }
+ catch (Error error) {}
+ fdss = null;
+ }
+
+ inhibit_cookie = 0;
+ }
+ }
+
+ app.scanning = is_scanning;
+ }
+
+ private void scan_cb (AppWindow ui, string? device, ScanOptions options)
+ {
+ debug ("Requesting scan at %d dpi from device '%s'", options.dpi, device);
+ scanner.scan (device, options);
+ }
+
+ private void cancel_cb (AppWindow ui)
+ {
+ scanner.cancel ();
+ }
+
+ private void redetect_cb (AppWindow ui)
+ {
+ scanner.redetect ();
+ }
+
+
+ private static void log_cb (string? log_domain, LogLevelFlags log_level, string message)
+ {
+ string prefix;
+
+ switch (log_level & LogLevelFlags.LEVEL_MASK)
+ {
+ case LogLevelFlags.LEVEL_ERROR:
+ prefix = "ERROR:";
+ break;
+ case LogLevelFlags.LEVEL_CRITICAL:
+ prefix = "CRITICAL:";
+ break;
+ case LogLevelFlags.LEVEL_WARNING:
+ prefix = "WARNING:";
+ break;
+ case LogLevelFlags.LEVEL_MESSAGE:
+ prefix = "MESSAGE:";
+ break;
+ case LogLevelFlags.LEVEL_INFO:
+ prefix = "INFO:";
+ break;
+ case LogLevelFlags.LEVEL_DEBUG:
+ prefix = "DEBUG:";
+ break;
+ default:
+ prefix = "LOG:";
+ break;
+ }
+
+ log_file.printf ("[%+.2fs] %s %s\n", log_timer.elapsed (), prefix, message);
+ if (debug_enabled)
+ stderr.printf ("[%+.2fs] %s %s\n", log_timer.elapsed (), prefix, message);
+ }
+
+ private static void fix_pdf (string filename) throws Error
+ {
+ uint8[] data;
+ FileUtils.get_data (filename, out data);
+
+ var fixed_file = FileStream.open (filename + ".fixed", "w");
+
+ var offset = 0;
+ var line_number = 0;
+ var xref_offset = 0;
+ var xref_line = -1;
+ var startxref_line = -1;
+ var fixed_size = -1;
+ var line = new StringBuilder ();
+ while (offset < data.length)
+ {
+ var end_offset = offset;
+ line.assign ("");
+ while (end_offset < data.length)
+ {
+ var c = data[end_offset];
+ line.append_c ((char) c);
+ end_offset++;
+ if (c == '\n')
+ break;
+ }
+
+ if (line.str == "startxref\n")
+ startxref_line = line_number;
+
+ if (line.str == "xref\n")
+ xref_line = line_number;
+
+ /* Fix PDF header and binary comment */
+ if (line_number < 2 && line.str.has_prefix ("%%"))
+ {
+ xref_offset--;
+ fixed_file.printf ("%s", line.str.substring (1));
+ }
+
+ /* Fix xref subsection count */
+ else if (line_number == xref_line + 1 && line.str.has_prefix ("1 "))
+ {
+ fixed_size = int.parse (line.str.substring (2)) + 1;
+ fixed_file.printf ("0 %d\n", fixed_size);
+ fixed_file.printf ("0000000000 65535 f \n");
+ }
+
+ /* Fix xref format */
+ else if (line_number > xref_line && line.str.has_suffix (" 0000 n\n"))
+ fixed_file.printf ("%010d 00000 n \n", int.parse (line.str) + xref_offset);
+
+ /* Fix xref offset */
+ else if (startxref_line > 0 && line_number == startxref_line + 1)
+ fixed_file.printf ("%d\n".printf (int.parse (line.str) + xref_offset));
+
+ else if (fixed_size > 0 && line.str.has_prefix ("/Size "))
+ fixed_file.printf ("/Size %d\n".printf (fixed_size));
+
+ /* Fix EOF marker */
+ else if (line_number == startxref_line + 2 && line.str.has_prefix ("%%%%"))
+ fixed_file.printf ("%s", line.str.substring (2));
+
+ else
+ for (var i = offset; i < end_offset; i++)
+ fixed_file.putc ((char) data[i]);
+
+ line_number++;
+ offset = end_offset;
+ }
+
+ if (FileUtils.rename (filename, filename + "~") >= 0)
+ FileUtils.rename (filename + ".fixed", filename);
+ }
+
+ public static int main (string[] args)
+ {
+ Intl.setlocale (LocaleCategory.ALL, "");
+ Intl.bindtextdomain (GETTEXT_PACKAGE, LOCALE_DIR);
+ Intl.bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ Intl.textdomain (GETTEXT_PACKAGE);
+
+ var c = new OptionContext (/* Arguments and description for --help text */
+ _("[DEVICE…] — Scanning utility"));
+ c.add_main_entries (options, GETTEXT_PACKAGE);
+ try
+ {
+ c.parse (ref args);
+ }
+ catch (Error e)
+ {
+ stderr.printf ("%s\n", e.message);
+ stderr.printf (/* Text printed out when an unknown command-line argument provided */
+ _("Run “%s --help” to see a full list of available command line options."), args[0]);
+ stderr.printf ("\n");
+ return Posix.EXIT_FAILURE;
+ }
+ if (show_version)
+ {
+ /* Note, not translated so can be easily parsed */
+ stderr.printf ("simple-scan %s\n", VERSION);
+ return Posix.EXIT_SUCCESS;
+ }
+ if (fix_pdf_filename != null)
+ {
+ try
+ {
+ fix_pdf (fix_pdf_filename);
+ for (var i = 1; i < args.length; i++)
+ fix_pdf (args[i]);
+ }
+ catch (Error e)
+ {
+ stderr.printf ("Error fixing PDF file: %s\n", e.message);
+ return Posix.EXIT_FAILURE;
+ }
+ return Posix.EXIT_SUCCESS;
+ }
+
+ ScanDevice? device = null;
+ if (args.length > 1)
+ {
+ device = new ScanDevice ();
+ device.name = args[1];
+ device.label = args[1];
+ }
+
+ /* Log to a file */
+ log_timer = new Timer ();
+ var path = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", null);
+ DirUtils.create_with_parents (path, 0700);
+ path = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "simple-scan.log", null);
+ log_file = FileStream.open (path, "w");
+ if (log_file == null )
+ {
+ stderr.printf ("Error: Unable to open %s file for writing\n", path);
+ return Posix.EXIT_FAILURE;
+ }
+ Log.set_default_handler (log_cb);
+
+ debug ("Starting %s %s, PID=%i", args[0], VERSION, Posix.getpid ());
+
+ Gtk.init ();
+
+ var app = new SimpleScan (device);
+ return app.run ();
+ }
+}
diff --git a/src/snapshot-area.vala b/src/snapshot-area.vala
new file mode 100644
index 0000000000000000000000000000000000000000..3ceb36cd9c641b433f223facebd03dc6582b0b12
--- /dev/null
+++ b/src/snapshot-area.vala
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+/**
+ * Snapshot-based drawing area.
+ *
+ * Subclasses Gtk.DrawingArea so we keep all of its widget plumbing (size
+ * machinery, scrollable container support, GtkBuilder compatibility) but
+ * overrides the snapshot vfunc so the default cairo draw_func path is never
+ * entered. No cairo context is created during rendering.
+ */
+[CCode (has_target = true)]
+public delegate void SnapshotFunc (Gtk.Snapshot snapshot, int width, int height);
+
+public class SnapshotArea : Gtk.DrawingArea
+{
+ private SnapshotFunc? snapshot_func = null;
+
+ public SnapshotArea ()
+ {
+ }
+
+ public void set_snapshot_func (owned SnapshotFunc func)
+ {
+ this.snapshot_func = (owned) func;
+ queue_draw ();
+ }
+
+ public override void snapshot (Gtk.Snapshot snapshot)
+ {
+ if (snapshot_func != null)
+ snapshot_func (snapshot, get_width (), get_height ());
+ }
+}