[Top][All Lists]

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

[elpa] externals/scanner 30b97d3 05/56: add implementation of image scan

From: Stefan Monnier
Subject: [elpa] externals/scanner 30b97d3 05/56: add implementation of image scanning and first test case
Date: Fri, 10 Apr 2020 13:55:58 -0400 (EDT)

branch: externals/scanner
commit 30b97d312e8a6ee07732dd6a29b1bd784e570356
Author: Raffael Stocker <address@hidden>
Commit: Raffael Stocker <address@hidden>

    add implementation of image scanning and first test case
 scanner-test.el |  79 +++++++++++++++++++++++++
 scanner.el      | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 239 insertions(+), 19 deletions(-)

diff --git a/scanner-test.el b/scanner-test.el
new file mode 100644
index 0000000..8936ba3
--- /dev/null
+++ b/scanner-test.el
@@ -0,0 +1,79 @@
+;;; scanner-test.el --- Scan documents and images -*- lexical-binding: t -*-
+;; Copyright (C) 2020 Raffael Stocker
+;; Author: Raffael Stocker <address@hidden>
+;; Maintainer: Raffael Stocker <address@hidden>
+;; Created: 05. Feb 2020
+;; Version: 0.0
+;; Package-Requires: ((emacs "25.1") (dash "2.12.0"))
+;; Keywords: hardware multimedia
+;; URL: https://gitlab.com/rstocker/scanner.git
+;; This file is NOT part of GNU Emacs
+;; GNU Emacs 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.
+;; GNU Emacs is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; General Public License for more details.
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+;;; Commentary:
+;; Test the scanner package.
+;; Rules:
+;; - clean up after tests (using ‘save-*exursion’, ‘unwind-protect’ etc.)
+;; - don't alter Emacs's state
+;; - tests should not depend on the current state of the environment
+;; - ‘let’-bind required variables, don't setq them
+;; - ‘skip-unless’ tests if they have dependencies that might not be met
+;;   on the test machine
+;; - use temp buffers if necessary, maybe bind hooks to nil etc.
+;; - tests should restore the environment, if they have side-effects on it
+;; - if writing a test is difficult, maybe refactor the code under test for
+;;   testability
+;; - use :tag to mark tests that need a connection to a scanner, e.g.
+;;   :tag :needs-hardware
+;; - group tests by choosing sensible names
+;; - see the ert doc for a way to implement test fixtures
+;;; Code:
+(load-file "scanner.el")
+(require 'scanner)
+(require 'dash)
+(require 'ert)
+(ert-deftest scanner-test-determine-format ()
+  "Test format determination from extension."
+  (let ((scanner-image-format "default"))
+    (should (string= "jpeg" (scanner--determine-format "jpg")))
+    (should (string= "jpeg" (scanner--determine-format "jpeg")))
+    (should (string= "tiff" (scanner--determine-format "tiff")))
+    (should (string= "tiff" (scanner--determine-format "tif")))
+    (should (string= "pnm" (scanner--determine-format "pnm")))
+    (should (string= "png" (scanner--determine-format "png")))
+    (should (string= "jpeg" (scanner--determine-format "JPG")))
+    (should (string= "default" (scanner--determine-format nil)))
+    (should (string= "default" (scanner--determine-format "")))
+    (should (string= "default" (scanner--determine-format 42)))
+    (should-error (scanner--determine-format '(42))
+                 :type 'wrong-type-argument)))
+(provide 'scanner-test)
+;; Local variables:
+;; eval: (flycheck-mode)
+;; End:
+;;; scanner-test.el ends here
diff --git a/scanner.el b/scanner.el
index 66cdc09..9b8a68a 100644
--- a/scanner.el
+++ b/scanner.el
@@ -73,9 +73,25 @@ The value must be one of the keys in the paper sizes list."
   :type '(restricted-sexp :match-alternatives
                          ((lambda (k) (plist-member scanner-paper-sizes k)))))
+(defcustom scanner-doc-intermediate-format
+  "png"
+  "Intermediate image format for document scanning."
+  :type '(radio (const "jpeg")
+               (const "png")
+               (const "pnm")
+               (const "tiff")))
 (defcustom scanner-image-format
-  "Default image file format."
+  "Image file format."
+  :type '(radio (const "jpeg")
+               (const "png")
+               (const "pnm")
+               (const "tiff")))
+(defcustom scanner-doc-intermediate-format
+  "png"
+  "Intermediate image format for document scanning."
   :type '(radio (const "jpeg")
                (const "png")
                (const "pnm")
@@ -105,7 +121,7 @@ The value must be one of the keys in the paper sizes list."
       (widget-put widget
-                 (format "Unknown languages: %s; available are: %s"
+                 (format "Unknown language(s): %s; available are: %s"
                          (mapconcat #'identity val ", ")
                          (mapconcat #'identity langs ", ")))
@@ -125,35 +141,136 @@ The config files may reside in 
 (defcustom scanner-tesseract-options
-  "Additional options to pass to tesseract(1)."
+  "Additional options to pass to tesseract(1).
   :type '(repeat string))
 (defcustom scanner-scan-mode
   "Scan mode."
-  :type '(radio (const "Color")
-               (const "Gray")
-               (const "Lineart")))
+  :type '(string))
 (defcustom scanner-scanimage-options
-  "Additional options to be passed to scanimage(1)."
+  "Additional options to be passed to scanimage(1).
   :type '(repeat string))
 (defcustom scanner-device-name
-  "SANE scanner device name or nil."
+  "SANE scanner device name or nil.
+If nil, auto-detection will be attempted."
   :type '(restricted-sexp :match-alternatives
                          (stringp 'nil)))
-;; TODO: check for availability of -x and -y arguments and
-;; use them according to the configured paper size
-(defun scanner--scanimage-args (outfile format)
+(defvar scanner--detected-devices
+  nil
+  "List of devices detected by SANE.
+Each element of the list has the form (DEVICE TYPE MODEL) where
+DEVICE is the SANE device name, TYPE the type of the device
+\(e.g. \"flatbed scanner\",) and MODEL is the device's model
+  (defconst scanner--device-specific-options
+    '("--mode" "--depth" "--resolution" "-x" "-y")
+    "List of required device specific options.
+These options are necessary for the full set of features offered
+by the scanner package.  If one of these is missing, something may
+not work as expected."))
+(defconst scanner--device-option-re
+  (eval-when-compile (regexp-opt scanner--device-specific-options t)))
+(defvar scanner--available-options
+  nil
+  "List of required options implemented by the device backend.")
+(defvar scanner--missing-options
+  nil
+  "List of required options missing from the device backend.")
+(defun scanner--check-device-options ()
+  "Return available and missing options provided by the device.
+This function checks the SANE backend of the device selected by
+‘scanner-device-name’ against the required options.  If
+‘scanner-device-name’ is nil, it attempts auto-detection.  The
+return value is a list comprising a list of the available options
+and a list of the missing options.  As a side effect, these
+results are cached."
+  (let ((-compare-fn #'string=)
+       opts)
+    (with-temp-buffer
+      (apply #'call-process scanner-scanimage-program nil t nil "-A"
+            (and scanner-device-name (list "-d" scanner-device-name)))
+      (goto-char (point-min))
+      (while (re-search-forward scanner--device-option-re nil t)
+       (push (match-string 1) opts)))
+    (setq scanner--available-options opts)
+    (setq scanner--missing-options
+         (-difference scanner--device-specific-options opts))
+    (list scanner--available-options scanner--missing-options)))
+(defun scanner-detect-devices ()
+  "Return a list of auto-detected scanning devices.
+Each element of the list contains three elements: the SANE device
+name, the device type, and the vendor and model names."
+  (let ((scanners (process-lines scanner-scanimage-program "-f" "%d|%t|%v 
+    ;; attempt to filter out any spurious error output or other non-relevant
+    ;; stuff
+    (setq scanner--detected-devices
+         (--filter (eql 3 (length it))
+                   (mapcar (lambda (x) (split-string x "|")) scanners)))))
+(defun scanner-select-device (&optional detect)
+  "Select a scanning device, maybe running auto-detection.
+If DETECT is non-nil or a prefix argument is supplied, force
+auto-detection.  Without an argument, auto-detect only if
+no devices have been detected yet.
+The selected device will be used for any future scan until a new
+selection is made."
+  (interactive "P")
+  (let* ((devices (if detect
+                     (scanner-detect-devices)
+                   (or scanner--detected-devices
+                       (scanner-detect-devices))))
+        (choices (mapcar (lambda (dev)
+                           (concat (caddr dev) " (" (car dev) ")"))
+                         devices)))
+    (setq scanner-device-name
+         (cadr (split-string
+                (completing-read "Select scanning device: " choices nil t)
+                "(" t ")")))))
+(defun scanner--scanimage-args (outfile format type)
   "Construct the argument list for scanimage(1).
-OUTFILE is the output filename and FORMAT is the output image format."
-  (-flatten (list "--format" format
-                 "--output-file" outfile
-                 scanner-scanimage-options)))
+OUTFILE is the output filename and FORMAT is the output image
+format.  TYPE is either ‘:image’ or ‘:doc’."
+  (let ((opts scanner--available-options))
+    (-flatten (list (and scanner-device-name
+                        (list "-d" scanner-device-name))
+                   (if (eq :image type)
+                       (concat "--format=" format)
+                     (concat "--format=" scanner-doc-intermediate-format))
+                   "-o" outfile
+                   (and (eq :doc type)
+                        (-when-let* ((x (car (member "-x" opts)))
+                                     (y (car (member "-y" opts)))
+                                     ((&plist scanner-doc-papersize size)
+                                      scanner-paper-sizes))
+                          (list x (number-to-string (car size))
+                                y (number-to-string (cadr size)))))
+                   (and (member "--mode" opts)
+                        (concat "--mode=" scanner-scan-mode))
+                   (and (member "--resolution" opts)
+                        (concat "--resolution=" (number-to-string
+                                                 scanner-doc-resolution)))
+                   scanner-scanimage-options))))
 (defun scanner--tesseract-args (input output-base)
   "Construct the argument list for ‘tesseract(1)’.
@@ -166,10 +283,26 @@ selected output options, see ‘scanner-tesseract-outputs’."
+(defconst scanner--image-extensions
+  '(("jpeg" . "jpeg")
+    ("jpg" . "jpeg")
+    ("png" . "png")
+    ("pnm" . "pnm")
+    ("tiff" . "tiff")
+    ("tif" . "tiff"))
+  "List of known image filename extensions with aliases.")
+(defun scanner--determine-format (extension)
+  "Determine image file format from EXTENSION."
+  (let ((ext (if extension (downcase extension) "")))
+    (or (cdr (assoc ext scanner--image-extensions))
+       scanner-image-format)))
 (defun scanner-scan-document (&optional _filename)
   "Scan a document named FILENAME."
   ;; loop in y-or-n-p over pages of the document
+  ;; scan multiple pages with configurable time delay
   ;; write scanned images to temp files
   ;; convert to temp pdf
   ;; ask for filename and write file?
@@ -185,10 +318,18 @@ selected output options, see ‘scanner-tesseract-outputs’."
 (defun scanner-scan-image ()
   "Scan an image."
-  ;; write scanned image to temp file
-  ;; ask for filename and write file
-  ;; open image if configured to do so
-  )
+  (let* ((filename (read-file-name "Image file: "))
+        (fmt (scanner--determine-format filename))
+        (fname (if (file-name-extension filename)
+                   filename
+                 (concat (file-name-sans-extension filename) "." fmt)))
+        (args (scanner--scanimage-args fname fmt :image)))
+    (cl-flet ((sentinel (process event)
+                       (message
+                        (format "Scanner: %s" (string-trim event)))))
+      (make-process :name "scanimage"
+                   :command `(,scanner-scanimage-program ,@args)
+                   :sentinel #'sentinel))))
 (provide 'scanner)

reply via email to

[Prev in Thread] Current Thread [Next in Thread]