[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
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;; 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
"jpeg"
- "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."
nil
(widget-put widget
:error
- (format "Unknown languages: %s; available are: %s"
+ (format "Unknown language(s): %s; available are: %s"
(mapconcat #'identity val ", ")
(mapconcat #'identity langs ", ")))
widget)))
@@ -125,35 +141,136 @@ The config files may reside in
‘/usr/share/tessdata/configs’."
(defcustom scanner-tesseract-options
'()
- "Additional options to pass to tesseract(1)."
+ "Additional options to pass to tesseract(1).
+FORM?"
:type '(repeat string))
(defcustom scanner-scan-mode
"Color"
"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).
+FORM?"
:type '(repeat string))
(defcustom scanner-device-name
nil
- "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
+name.")
+
+(eval-when-compile
+ (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
%m")))
+ ;; 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’."
scanner-tesseract-options
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."
(interactive)
;; 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."
(interactive)
- ;; 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)
- [elpa] branch externals/scanner created (now 72ecf43), Stefan Monnier, 2020/04/10
- [elpa] externals/scanner 704f055 04/56: add compile dependencies to check target, Stefan Monnier, 2020/04/10
- [elpa] externals/scanner 027658b 06/56: ignore image files, Stefan Monnier, 2020/04/10
- [elpa] externals/scanner 4596ed9 09/56: add autoloads, minor refactoring, Stefan Monnier, 2020/04/10
- [elpa] externals/scanner 30b97d3 05/56: add implementation of image scanning and first test case,
Stefan Monnier <=
- [elpa] externals/scanner 9665335 10/56: add a menu and configuration functions, Stefan Monnier, 2020/04/10
- [elpa] externals/scanner d5be7b8 12/56: clean up resolution setters, Stefan Monnier, 2020/04/10
- [elpa] externals/scanner 1e60cd2 03/56: add test and check targets to Makefile, gitignore dep, Stefan Monnier, 2020/04/10
- [elpa] externals/scanner 5bc37ff 02/56: add more customizations and validations, arglist functions, Stefan Monnier, 2020/04/10
- [elpa] externals/scanner c952d0d 15/56: rename -options to -switches, Stefan Monnier, 2020/04/10
- [elpa] externals/scanner e5c74e6 11/56: implement multi-page scan modes, Stefan Monnier, 2020/04/10
- [elpa] externals/scanner dbb0ee4 14/56: add commentary, correct docstrings, Stefan Monnier, 2020/04/10
- [elpa] externals/scanner 3e0398c 22/56: correct to eval-and-compile (scanner--device-specific-switches), Stefan Monnier, 2020/04/10
- [elpa] externals/scanner 5f06a4a 21/56: implement correct cleanup and error handling (scanner-scan-document), Stefan Monnier, 2020/04/10
- [elpa] externals/scanner 536e998 16/56: move commands were they belong, Stefan Monnier, 2020/04/10