[simple-scan] initial support for executing postprocessing scripts



commit db90d5b188267b6377305d36b6199f196902e263
Author: Alex Vogt <a vogt linexus de>
Date:   Thu Jan 6 20:06:09 2022 +0100

    initial support for executing postprocessing scripts

 data/org.gnome.SimpleScan.gschema.xml | 20 +++++++
 data/ui/preferences-dialog.ui         | 98 +++++++++++++++++++++++++++++++++++
 src/app-window.vala                   | 10 +++-
 src/book.vala                         | 30 +++++++++--
 src/meson.build                       |  1 +
 src/postprocessor.vala                | 44 ++++++++++++++++
 src/preferences-dialog.vala           | 35 +++++++++++++
 src/simple-scan-postprocessing.sh     | 83 +++++++++++++++++++++++++++++
 8 files changed, 316 insertions(+), 5 deletions(-)
---
diff --git a/data/org.gnome.SimpleScan.gschema.xml b/data/org.gnome.SimpleScan.gschema.xml
index 290add1f..03f19227 100644
--- a/data/org.gnome.SimpleScan.gschema.xml
+++ b/data/org.gnome.SimpleScan.gschema.xml
@@ -77,5 +77,25 @@
       <summary>Delay in millisecond between pages</summary>
       <description>Delay in millisecond between pages.</description>
     </key>
+    <key name="postproc-enabled" type="b">
+      <default>false</default>
+      <summary>Whether or not postprocessing is enabled</summary>
+      <description>Whether or not postprocessing is enabled.</description>
+    </key>
+    <key name="postproc-script" type="s">
+      <default>''</default>
+      <summary>The path to the postprocessing script</summary>
+      <description>The path to the postprocessing script.</description>
+    </key>
+    <key name="postproc-arguments" type="s">
+      <default>''</default>
+      <summary>Additional arguments for the postprocessing script</summary>
+      <description>Additional arguments for the postprocessing script.</description>
+    </key>
+    <key name="postproc-keep-original" type="b">
+      <default>false</default>
+      <summary>Whether or not to keep the original, unprocessed file</summary>
+      <description>Whether or not to keep the original, unprocessed file. The "_orig" filename will be added 
to the filename immediately before the file extension. </description>
+    </key>
   </schema>
 </schemalist>
diff --git a/data/ui/preferences-dialog.ui b/data/ui/preferences-dialog.ui
index 4bc42d62..fe63150a 100644
--- a/data/ui/preferences-dialog.ui
+++ b/data/ui/preferences-dialog.ui
@@ -261,6 +261,104 @@
             </child>
           </object>
         </child>
+        <child>
+          <object class="HdyPreferencesGroup">
+            <property name="visible">True</property>
+            <property name="title" translatable="yes" comments="Preferences Dialog: Section label for 
postprocessing settings">Postprocessing</property>
+            <child>
+              <object class="HdyActionRow">
+                <property name="visible">True</property>
+                <property name="title" translatable="yes" comments="Switch to enable postprocessing">_Enable 
Postprocessing</property>
+                <property name="use_underline">True</property>
+                <property name="activatable_widget">postproc_enable_box</property>
+                <child>
+                  <object class="GtkBox" id="postproc_enable_box">
+                    <property name="visible">True</property>
+                    <property name="valign">center</property>
+                    <property name="homogeneous">True</property>
+                    <child>
+                      <object class="GtkSwitch" id="postproc_enable_switch">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="hexpand">False</property>
+                        <property name="active">True</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="HdyActionRow">
+                <property name="visible">True</property>
+                <property name="title" translatable="yes" comments="Label beside postprocesing script name 
entry">_Script</property>
+                <property name="use_underline">True</property>
+                <property name="activatable_widget">postproc_script_entry</property>
+                <child>
+                  <object class="GtkBox" id="postproc_script_box">
+                    <property name="visible">True</property>
+                    <property name="valign">center</property>
+                    <property name="homogeneous">True</property>
+
+                    <child>
+                      <object class="GtkEntry" id="postproc_script_entry">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="hexpand">True</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="HdyActionRow">
+                <property name="visible">True</property>
+                <property name="title" translatable="yes" comments="Label beside postprocesing arguments 
entry">_Script arguments</property>
+                <property name="use_underline">True</property>
+                <property name="activatable_widget">postproc_args_entry</property>
+                <child>
+                  <object class="GtkBox" id="postproc_args_box">
+                    <property name="visible">True</property>
+                    <property name="valign">center</property>
+                    <property name="homogeneous">True</property>
+
+                    <child>
+                      <object class="GtkEntry" id="postproc_args_entry">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="hexpand">True</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="HdyActionRow">
+                <property name="visible">True</property>
+                <property name="title" translatable="yes" comments="Label beside keep keep original file 
radio">_Keep original file</property>
+                <property name="use_underline">True</property>
+                <property name="activatable_widget">postproc_keep_original_box</property>
+                <child>
+                  <object class="GtkBox" id="postproc_keep_original_box">
+                    <property name="visible">True</property>
+                    <property name="valign">center</property>
+                    <property name="homogeneous">True</property>
+                      <child>
+                        <object class="GtkSwitch" id="postproc_keep_original_switch">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="hexpand">False</property>
+                        <property name="active">True</property>
+                        </object>
+                      </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
       </object>
     </child>
   </template>
diff --git a/src/app-window.vala b/src/app-window.vala
index 8c0995a7..cea6837d 100644
--- a/src/app-window.vala
+++ b/src/app-window.vala
@@ -720,7 +720,10 @@ public class AppWindow : Hdy.ApplicationWindow
         save_button.sensitive = false;
         try
         {
-            yield book.save_async (mime_type, settings.get_int ("jpeg-quality"), file, (fraction) =>
+            yield book.save_async (mime_type, settings.get_int ("jpeg-quality"), file,
+                settings.get_boolean ("postproc-enabled"), settings.get_string ("postproc-script"),
+                settings.get_string ("postproc-arguments"), settings.get_boolean ("postproc-keep-original"),
+                (fraction) =>
             {
                 progress_bar.set_fraction (fraction);
             }, cancellable);
@@ -1473,7 +1476,10 @@ public class AppWindow : Hdy.ApplicationWindow
                 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.save_async (mime_type, settings.get_int ("jpeg-quality"), file,
+                settings.get_boolean ("postproc-enabled"), settings.get_string ("postproc-script"),
+                settings.get_string ("postproc-arguments"), settings.get_boolean ("postproc-keep-original"),
+                null, null);
             var command_line = "xdg-email";
             if (mime_type == "application/pdf")
                 command_line += " --attach %s".printf (file.get_path ());
diff --git a/src/book.vala b/src/book.vala
index 798fe980..d2aa54da 100644
--- a/src/book.vala
+++ b/src/book.vala
@@ -136,10 +136,14 @@ public class Book : Object
         return pages.index (page);
     }
 
-    public async void save_async (string mime_type, int quality, File file, ProgressionCallback? 
progress_cb, Cancellable? cancellable = null) throws Error
+    public async void save_async (string mime_type, int quality, File file,
+        bool postproc_enabled, string postproc_script, string postproc_arguments, bool 
postproc_keep_original,
+        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);
+        yield book_saver.save_async (this, mime_type, quality, file,
+            postproc_enabled, postproc_script, postproc_arguments, postproc_keep_original,
+            progress_cb, cancellable);
     }
 }
 
@@ -155,12 +159,15 @@ private class BookSaver
     private AsyncQueue<WriteTask> write_queue;
     private ThreadPool<EncodeTask> 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
+    public async void save_async (Book book, string mime_type, int quality, File file,
+        bool postproc_enabled, string postproc_script, string postproc_arguments, bool 
postproc_keep_original,
+        ProgressionCallback? progression_callback, Cancellable? cancellable) throws Error
     {
         var timer = new Timer ();
 
@@ -239,6 +246,23 @@ private class BookSaver
 
         timer.stop ();
         debug ("Save time: %f seconds", timer.elapsed (null));
+
+        if ( postproc_enabled ) {
+        /* Perform post-processing */
+            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
diff --git a/src/meson.build b/src/meson.build
index 3f699ebc..240b56d0 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -22,6 +22,7 @@ simple_scan = executable ('simple-scan',
                             'page.vala',
                             'page-icon.vala',
                             'page-view.vala',
+                            'postprocessor.vala',
                             'preferences-dialog.vala',
                             'simple-scan.vala',
                             'scanner.vala',
diff --git a/src/postprocessor.vala b/src/postprocessor.vala
new file mode 100644
index 00000000..2d036c91
--- /dev/null
+++ b/src/postprocessor.vala
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+/*
+ * Copyright (C) 2022 Alexander Vogt
+ * Author: Alexander Vogt <a vogt fulguritus com>
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later
+ * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+public class Postprocessor {
+
+    public Postprocessor(){
+
+    }
+
+    public int process(string script, string mime_type, bool keep_original, string source_file, string 
arguments) throws Error {
+        // Code copied and adapted from https://valadoc.org/glib-2.0/GLib.Process.spawn_sync.html
+        string[] spawn_args = {script, mime_type, keep_original ? "true" : "false", source_file, arguments };
+        string[] spawn_env = Environ.get ();
+        string  process_stdout;
+        string  process_stderr;
+        int     process_status;
+
+        print ("Executing script%s\n", script);
+        Process.spawn_sync (null,               // inherit parent's working dir
+                                               spawn_args,
+                                               spawn_env,
+                                               SpawnFlags.SEARCH_PATH,
+                                               null,
+                                               out process_stdout,
+                                               out process_stderr,
+                                               out process_status);
+           debug ("status: %d\n", process_status);
+           debug ("STDOUT: \n");
+           debug ("process_stdout");
+           debug ("STDERR: \n");
+           debug ("process_stderr");
+
+           return process_status;
+    }
+}
diff --git a/src/preferences-dialog.vala b/src/preferences-dialog.vala
index 8d992abb..02bbaf8d 100644
--- a/src/preferences-dialog.vala
+++ b/src/preferences-dialog.vala
@@ -51,6 +51,14 @@ private class PreferencesDialog : Hdy.PreferencesWindow
     private unowned Gtk.Adjustment brightness_adjustment;
     [GtkChild]
     private unowned Gtk.Adjustment contrast_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;
 
     public PreferencesDialog (Settings settings)
     {
@@ -133,6 +141,33 @@ private class PreferencesDialog : Hdy.PreferencesWindow
         page_delay_6s_button.toggled.connect ((button) => { if (button.active) settings.set_int 
("page-delay", 6000); });
         page_delay_10s_button.toggled.connect ((button) => { if (button.active) settings.set_int 
("page-delay", 10000); });
         page_delay_15s_button.toggled.connect ((button) => { if (button.active) settings.set_int 
("page-delay", 15000); });
+
+        // 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)
diff --git a/src/simple-scan-postprocessing.sh b/src/simple-scan-postprocessing.sh
new file mode 100755
index 00000000..39fb4612
--- /dev/null
+++ b/src/simple-scan-postprocessing.sh
@@ -0,0 +1,83 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-3.0-or-later
+# Copyright (C) 2022 Alexander Vogt
+# Author: Alexander Vogt <a vogt fulguritus com>
+#
+# Sample postprocessing script for gnome-simple-scan for OCR in PDFs
+#
+# This script first identifies a suitable instance of ocrmypdf
+# (https://github.com/ocrmypdf/OCRmyPDF) and then applies this as a
+# postprocessing step to PDFs generated by simple-scan.
+#
+# Usage:
+# =====
+# simple-scan-postprocessing mime-type keep-origin input-file args
+#
+# Currently, only mime-type "application/pdf" is supported, the script will
+# exit without an error if "image/jpeg", "image/png", or "image/webp" is
+# provided. Any other mime-type results in an error.
+# All args are provided to ocrmypdf.
+# If keep-origin is set to "true", a copy of the source file is kept.
+#
+# Example:
+# =======
+# simple-scan-postprocessing application/pdf true scan.pdf -l eng+deu
+# simple-scan-postprocessing application/pdf true scan.pdf -rcd --jbig2-lossy -l deu
+#
+set -e +m
+
+# Arguments
+mime_type="$1"
+keep_original="$2"
+target="$3"
+remainder="${@:4}"
+# Globals
+_ocrmypdfcontainer="jbarlow83/ocrmypdf"
+
+source="${target%.*}_orig.${target##*.}"
+
+# Helper functions
+function findOcrMyPdf() {
+       # Determines the path of ocrmypdf in the following order:
+       #   1. ocrmypdf from the $PATH (local installation)
+       #   2. ocrmypdf through podman (if podman in $PATH)
+       #   3. ocrmypdf through docker (if podman in $PATH)
+       _ocrmypdf=$(which ocrmypdf) && return
+       _ocrmypdf="$(which podman) run --rm -i ${_ocrmypdfcontainer} " && return
+       _ocrmypdf="$(which docker) run --rm -i ${_ocrmypdfcontainer} "
+       if [ $? -ne 0 ]; then
+               echo "No suitable instance of ocrmypdf found. Please check your setup. "
+               exit 1
+       fi
+}
+
+case ${mime_type} in
+       "application/pdf")
+               mv "$target" "$source" # create a backup
+
+               # Determine the version of ocrmypdf to use
+               findOcrMyPdf
+               # Execute OCR
+               ${_ocrmypdf} ${remainder} - - <"$source" >"$target"
+               ;;
+       "image/jpeg")
+               exit 0 # Nothing implemented
+               ;;
+       "image/png")
+               exit 0 # Nothing implemented
+               ;;
+       "image/webp")
+               exit 0 # Nothing implemented
+               ;;
+       *)
+               echo "Unsupported mime-type \"${mime_type}\""
+               exit 1
+               ;;
+esac
+
+# Clean up
+if [ "$keep_original" == "true" ]; then
+       exit 0
+else
+       rm "$source"
+fi


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]