From 63f4f02aa78e6b48f7ee346d31a813ca0978651e Mon Sep 17 00:00:00 2001 From: Lars Ingebrigtsen Date: Sun, 29 Sep 2019 01:26:02 +0200 Subject: [PATCH] If requested, use external image converters for exotic formats * doc/lispref/display.texi (Defining Images): Document it. * lisp/image.el (convert-images-externally): New variable. (image-type): Use it. (create-image): Convert images. * lisp/image/image-converter.el (image-converter--convert): New file. --- doc/lispref/display.texi | 7 ++ etc/NEWS | 7 ++ lisp/image.el | 34 +++++- lisp/image/image-converter.el | 191 ++++++++++++++++++++++++++++++++++ 4 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 lisp/image/image-converter.el diff --git a/doc/lispref/display.texi b/doc/lispref/display.texi index fd6820897f3..ec288b1c47d 100644 --- a/doc/lispref/display.texi +++ b/doc/lispref/display.texi @@ -6044,6 +6044,13 @@ properties---for example, (create-image "foo.xpm" 'xpm nil :heuristic-mask t) @end example +@vindex convert-images-externally +If Emacs doesn't have native support for the image format in question, +and @code{convert-images-externally} is non-@code{nil}, Emacs will try +to determine whether there are external utilities that can be used to +transform the image in question to @acronym{PNG} before displaying. +GraphicsMagick, ImageMagick and ffmpeg are currently supported. + The function returns @code{nil} if images of this type are not supported. Otherwise it returns an image descriptor. @end defun diff --git a/etc/NEWS b/etc/NEWS index 34b7a5aa29d..677b710e223 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2656,6 +2656,13 @@ data about creation times and orientation and the like. 'exif-parse-file' and 'exif-parse-buffer' are the main interface functions. +*** New library image-converter. +To view exotic image formats that Emacs doesn't have native support +for, the new 'convert-images-externally' variable can be set to t. If +the system has GraphicsMagick, ImageMagick or ffmpeg installed, it +will be used to convert images automatically before displaying with +'create-image'. + *** 'image-mode' now uses this library to automatically rotate images according to the orientation in the Exif data, if any. diff --git a/lisp/image.el b/lisp/image.el index b36a5138b1b..00b4c487cd7 100644 --- a/lisp/image.el +++ b/lisp/image.el @@ -141,6 +141,17 @@ based on the font pixel size." (const :tag "Automatically compute" auto)) :version "26.1") +(defcustom convert-images-externally nil + "If non-nil, `create-image' will use external converters for exotic formats. +Emacs handles most of the common image formats (JPEG, PNG, GIF +and so on) internally, but images that doesn't have native +support in Emacs can still be displayed by Emacs if external +conversion programs (like ImageMagick \"convert\", GraphicsMagick +\"gm\" or \"ffmpeg\") are installed." + :group 'image + :type 'bool + :version "27.1") + ;; Map put into text properties on images. (defvar image-map (let ((map (make-sparse-keymap))) @@ -357,6 +368,9 @@ be determined." ;; If nothing seems to be supported, return first type that matched. (or first (setq first type)))))))) +(declare-function image-convert-p "image-converter.el" (file)) +(declare-function image-convert "image-converter.el" (image)) + ;;;###autoload (defun image-type (source &optional type data-p) "Determine and return image type. @@ -372,10 +386,16 @@ Optional DATA-P non-nil means SOURCE is a string containing image data." (setq type (if data-p (image-type-from-data source) (or (image-type-from-file-header source) - (image-type-from-file-name source)))) - (or type (error "Cannot determine image type"))) - (or (memq type (and (boundp 'image-types) image-types)) - (error "Invalid image type `%s'" type)) + (image-type-from-file-name source) + (and convert-images-externally + (progn + (require 'image-converter) + (image-convert-p source)))))) + (unless type + (error "Cannot determine image type"))) + (when (and (not (eq type 'image-convert)) + (not (memq type (and (boundp 'image-types) image-types)))) + (error "Invalid image type `%s'" type)) type) @@ -438,6 +458,12 @@ Image file names that are not absolute are searched for in the `x-bitmap-file-path' (in that order)." ;; It is x_find_image_file in image.c that sets the search path. (setq type (image-type file-or-data type data-p)) + ;; If we have external image conversion switched on (for exotic, + ;; non-native image formats), then we convert the file. + (when (eq type 'image-convert) + (setq file-or-data (image-convert file-or-data) + type 'png + data-p t)) (when (image-type-available-p type) (append (list 'image :type type (if data-p :data :file) file-or-data) (and (not (plist-get props :scale)) diff --git a/lisp/image/image-converter.el b/lisp/image/image-converter.el new file mode 100644 index 00000000000..f485c348426 --- /dev/null +++ b/lisp/image/image-converter.el @@ -0,0 +1,191 @@ +;;; image-converter.el --- Converting images from exotic formats -*- lexical-binding: t -*- + +;; Copyright (C) 2019 Free Software Foundation, Inc. + +;; Author: Lars Magne Ingebrigtsen +;; Keywords: images + +;; This file is 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 GNU Emacs. If not, see . + +;;; Commentary: + +;; The main interface function here is `image-convert'. + +;;; Code: + +(require 'cl-generic) +(eval-when-compile (require 'cl-lib)) + +(defcustom convert-external-images nil + "If non-nil, `create-image' will use external converters for exotic formats. +Emacs handles most of the common image formats (JPEG, PNG, GIF +and so on) internally, but images that doesn't have native +support in Emacs can still be displayed by Emacs if external +conversion programs (like ImageMagick \"convert\", GraphicsMagick +\"gm\" or \"ffmpeg\") are installed." + :group 'image + :type 'bool + :version "27.1") + +(defcustom image-converter nil + "What external converter to use. +`imagemagick', `graphicsmagick' and `ffmpeg' are supported." + :group 'image + :type 'symbol + :version "27.1") + +(defvar image-converter-regexp nil + "A regexp that matches the file name suffixes that can be converted.") + +(defvar image-converter--converters + '((graphicsmagick :command "gm convert" :probe "-list format") + (imagemagick :command "convert" :probe "-list format") + (ffmpeg :command "ffmpeg" :probe "-decoders")) + "List of supported image converters to try.") + +(defun image-convert-p (file) + "Return `image-convert' if FILE can be converted." + ;; Find an installed image converter. + (unless image-converter + (image-converter--find-converter)) + (and image-converter + (string-match image-converter-regexp file) + 'image-convert)) + +(defun image-convert (image) + "Convert IMAGE to a format Emacs can display. +IMAGE can either be a file name, which will make the return value +a string with the image data. It can also be an image object as +returned by `create-image'. If so, it has to be an image object +where created with DATA-P nil (i.e., it has to refer to a file)." + ;; Find an installed image converter. + (unless image-converter + (image-converter--find-converter)) + (unless image-converter + (error "No external image converters installed")) + (when (and (listp image) + (not (plist-get (cdr image) :file))) + (error "Only images that refer to files can be converted")) + (with-temp-buffer + (set-buffer-multibyte nil) + (when-let ((err (image-converter--convert + image-converter + (if (listp image) + (plist-get (cdr image) :file) + image)))) + (error "%s" err)) + (if (listp image) + ;; Return an image object that's the same as we were passed, + ;; but ignore the :type and :file values. + (apply #'create-image (buffer-string) 'png t + (cl-loop for (key val) on (cdr image) by #'cddr + unless (memq key '(:type :file)) + append (list key val))) + (buffer-string)))) + +(defun image-converter--value (type elem) + "Return the value of ELEM of image converter TYPE." + (plist-get (cdr (assq type image-converter--converters)) elem)) + +(cl-defmethod image-converter--probe ((type (eql graphicsmagick))) + "Check whether the system has GraphicsMagick installed." + (with-temp-buffer + (let ((command (split-string (image-converter--value type :command) " ")) + formats) + (when (zerop (apply #'call-process (car command) nil '(t nil) nil + (append (cdr command) + (split-string + (image-converter--value type :probe) " ")))) + (goto-char (point-min)) + (when (re-search-forward "^-" nil t) + (forward-line 1) + ;; Lines look like + ;; " 8BIM P rw- Photoshop resource format". + (while (re-search-forward "^ *\\([A-Z0-9]+\\) +. +r" nil t) + (push (downcase (match-string 1)) formats))) + (nreverse formats))))) + +(cl-defmethod image-converter--probe ((type (eql imagemagick))) + "Check whether the system has ImageMagick installed." + (with-temp-buffer + (let ((command (split-string (image-converter--value type :command) " ")) + formats) + ;; Can't check return value; ImageMagick convert usually returns + ;; a non-zero result on "-list format". + (apply #'call-process (car command) nil '(t nil) nil + (append (cdr command) + (split-string (image-converter--value type :probe) " "))) + (goto-char (point-min)) + (when (re-search-forward "^-" nil t) + (forward-line 1) + ;; Lines look like + ;; " WPG* r-- Word Perfect Graphics". + (while (re-search-forward "^ *\\([A-Z0-9]+\\)\\*? +r" nil t) + (push (downcase (match-string 1)) formats))) + (nreverse formats)))) + +(cl-defmethod image-converter--probe ((type (eql ffmpeg))) + "Check whether the system has ffmpeg installed." + (with-temp-buffer + (let ((command (image-converter--value type :command)) + formats) + (when (zerop (call-process command nil '(t nil) nil + (image-converter--value type :probe))) + (goto-char (point-min)) + (when (re-search-forward "^ *-" nil t) + (forward-line 1) + ;; Lines look like + ;; " V....D alias_pix Alias/Wavefront PIX image" + (while (re-search-forward "^ *V[^ ]+ +\\([a-z0-9_]+\\)" nil t) + (push (match-string 1) formats))) + (nreverse formats))))) + +(defun image-converter--find-converter () + "Find an installed image converter." + (dolist (elem image-converter--converters) + (when-let ((formats (image-converter--probe (car elem)))) + (setq image-converter (car elem) + image-converter-regexp (concat "\\." (regexp-opt formats) "\\'"))))) + +(cl-defmethod image-converter--convert ((type (eql graphicsmagick)) file) + "Convert using GraphicsMagick." + (image-converter--convert-magick type file)) + +(cl-defmethod image-converter--convert ((type (eql imagemagick)) file) + "Convert using ImageMagick." + (image-converter--convert-magick type file)) + +(defun image-converter--convert-magick (type file) + (let ((command (split-string (image-converter--value type :command) " "))) + (unless (zerop (apply #'call-process (car command) + nil t nil + (append (cdr command) + (list (expand-file-name file) "png:-")))) + ;; If the command failed, hopefully the buffer contains the + ;; error message. + (buffer-string)))) + +(cl-defmethod image-converter--convert ((type (eql ffmpeg)) file) + "Convert using ffmpeg." + (unless (zerop (call-process (image-converter--value type :command) + nil '(t nil) nil + "-i" (expand-file-name file) + "-c:v" "png" "-f" "image2pipe" "-")) + "ffmpeg error when converting")) + +(provide 'image-converter) + +;;; image-converter.el ends here -- 2.39.2