commit d2abe9adf7a1bfcdfd712de1446fd3e710e0f699 (HEAD, refs/remotes/origin/master) Author: Eli Zaretskii Date: Sun Sep 4 10:03:22 2022 +0300 ; * lisp/disp-table.el (standard-display-by-replacement-char): Doc fix. diff --git a/lisp/disp-table.el b/lisp/disp-table.el index 1b14808d78..53dff1e709 100644 --- a/lisp/disp-table.el +++ b/lisp/disp-table.el @@ -307,10 +307,14 @@ be represented by a replacement character. You can evaluate the produced code to use the setup for the current Emacs session, or copy the code into your init file, to make Emacs use it for subsequent sessions. -FROM and TO define the range of characters for which to produce the -setup code for `standard-display-table'. If they are omitted, they -default to #x100 and #x10FFFF respectively, covering the entire -non-ASCII range of Unicode characters. +Interactively, the produced code arranges for any character in +the range [#x100..#x10FFFF] that the terminal cannot display to +be represented by the #xFFFD Unicode replacement character. + +When called from Lisp, FROM and TO define the range of characters for +which to produce the setup code for `standard-display-table'. If they +are omitted, they default to #x100 and #x10FFFF respectively, covering +the entire non-ASCII range of Unicode characters. REPL is the replacement character to use. If it's omitted, it defaults to #xFFFD, the Unicode replacement character, usually displayed as a black diamond with a question mark inside. commit b35a93a0619400e93fd76d7de3d837f990802274 Author: Eli Zaretskii Date: Sun Sep 4 09:03:30 2022 +0300 New command to facilitate text-mode display of unsupported chars * lisp/disp-table.el (standard-display-by-replacement-char): New command. * etc/NEWS: Announce it. diff --git a/etc/NEWS b/etc/NEWS index cc4714e71c..edd4b01eab 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -749,6 +749,15 @@ saved to the X primary selection, following the 'select-active-regions' variable. This support is enabled when 'tty-select-active-regions' is non-nil. +--- +*** New command to set up display of unsupported characters. +The new command 'standard-display-by-replacement-char' produces Lisp +code that sets up the 'standard-display-table' to use a replacement +character for display of characters that the text-mode terminal +doesn't support. It is most useful with the Linux console and similar +terminals, where Emacs has a reliable way of determining which +characters have glyphs in the font loaded into the terminal's memory. + ** ERT +++ diff --git a/lisp/disp-table.el b/lisp/disp-table.el index 422728c61c..1b14808d78 100644 --- a/lisp/disp-table.el +++ b/lisp/disp-table.el @@ -296,6 +296,65 @@ in `.emacs'." (if (coding-system-p c) c 'latin-1)))) (standard-display-european-internal))) + +;;;###autoload +(defun standard-display-by-replacement-char (&optional repl from to) + "Produce code to display characters between FROM and TO using REPL. +This function produces a buffer with code to set up `standard-display-table' +such that characters that cannot be displayed by the terminal, and +don't already have their display set up in `standard-display-table', will +be represented by a replacement character. You can evaluate the produced +code to use the setup for the current Emacs session, or copy the code +into your init file, to make Emacs use it for subsequent sessions. + +FROM and TO define the range of characters for which to produce the +setup code for `standard-display-table'. If they are omitted, they +default to #x100 and #x10FFFF respectively, covering the entire +non-ASCII range of Unicode characters. +REPL is the replacement character to use. If it's omitted, it defaults +to #xFFFD, the Unicode replacement character, usually displayed as a +black diamond with a question mark inside. +The produced code sets up `standard-display-table' to show REPL with +the `homoglyph' face, making the replacements stand out on display. + +This command is most useful with text-mode terminals, such as the +Linux console, for which Emacs has a reliable way of determining +which characters can be displayed and which cannot." + (interactive) + (or repl + (setq repl #xfffd)) + (or (and from to (<= from to)) + (setq from #x100 + to (max-char 'unicode))) + (let ((buf (get-buffer-create "*Display replacements*")) + (ch from) + (tbl standard-display-table) + first) + (with-current-buffer buf + (erase-buffer) + (insert "(let ((tbl standard-display-table))\n") + (while (<= ch to) + (cond + ((or (char-displayable-p ch) + (aref tbl ch)) + (setq ch (1+ ch))) + (t + (setq first ch) + (while (and (<= ch to) + (not (or (char-displayable-p ch) + (aref tbl ch)))) + (setq ch (1+ ch))) + (insert + " (set-char-table-range tbl '(" + (format "#x%x" first) + " . " + (format "#x%x" (1- ch)) + ")\n\ (vconcat (list (make-glyph-code " + (format "#x%x" repl) " 'homoglyph))))\n")))) + (insert ")\n")) + (pop-to-buffer buf))) + + (provide 'disp-table) ;;; disp-table.el ends here commit 1d9e4900a336b6fa2047404ff25ec31cf8ec613f Author: Eli Zaretskii Date: Sun Sep 4 08:40:52 2022 +0300 Fix update of Dired display when it was called on a cons cell * lisp/dired.el (dired-internal-do-deletions): Make sure that FN's directory entry is updated on display. (Bug#57565) diff --git a/lisp/dired.el b/lisp/dired.el index fa06c8fd44..facfb35ab4 100644 --- a/lisp/dired.el +++ b/lisp/dired.el @@ -3746,7 +3746,10 @@ non-empty directories is allowed." (progress-reporter-update progress-reporter succ) (dired-fun-in-all-buffers (file-name-directory fn) (file-name-nondirectory fn) - #'dired-delete-entry fn)) + #'dired-delete-entry fn) + ;; For when FN's directory name is different + ;; from the current buffer's dired-directory. + (dired-delete-entry fn)) (quit (throw '--delete-cancel (message "OK, canceled"))) (error ;; catch errors from failed deletions (dired-log "%s: %s\n" (car err) (error-message-string err)) commit 0ffde8a81fd11b5cf42b5a7ac2f9417d6688744b Author: Stefan Monnier Date: Sat Sep 3 22:58:44 2022 -0400 * lisp/term/linux.el (gpm-mouse-enable): Remove left-over declaration diff --git a/lisp/term/linux.el b/lisp/term/linux.el index 60bf91fcf5..f24af3f134 100644 --- a/lisp/term/linux.el +++ b/lisp/term/linux.el @@ -2,8 +2,6 @@ ;; The Linux console handles Latin-1 by default. -(declare-function gpm-mouse-enable "t-mouse" ()) - (defun terminal-init-linux () "Terminal initialization function for linux." (unless (terminal-coding-system) @@ -15,13 +13,11 @@ ;; Compositions confuse cursor movement. (setq-default auto-composition-mode "linux") - ;; Don't translate ESC TAB to backtab as directed - ;; by ncurses-6.3. + ;; Don't translate ESC TAB to backtab as directed by ncurses-6.3. (define-key input-decode-map "\e\t" nil) ;; Make Latin-1 input characters work, too. - ;; Meta will continue to work, because the kernel - ;; turns that into Escape. + ;; Meta will continue to work, because the kernel turns that into Escape. ;; The arg only matters in that it is not t or nil. (set-input-meta-mode 'iso-latin-1)) commit 2dd1c2ab19f7fb99ecee60e27e63b2fb045f6970 Author: Stefan Monnier Date: Sat Sep 3 22:38:28 2022 -0400 gv.el and cl-macs.el: Fix bug#57397 * lisp/emacs-lisp/gv.el (gv-get): Obey symbol macros. * lisp/emacs-lisp/cl-macs.el (cl--letf): Remove workaround placed to try and handle symbol macros. * test/lisp/emacs-lisp/cl-macs-tests.el (cl-macs-test--symbol-macrolet): Add new testcase. diff --git a/lisp/emacs-lisp/cl-macs.el b/lisp/emacs-lisp/cl-macs.el index edd633675d..9755c2636d 100644 --- a/lisp/emacs-lisp/cl-macs.el +++ b/lisp/emacs-lisp/cl-macs.el @@ -2762,7 +2762,7 @@ Each PLACE may be a symbol, or any generalized variable allowed by `setf'. (funcall setter vold))) binds)))) (let* ((binding (car bindings)) - (place (macroexpand (car binding) macroexpand-all-environment))) + (place (car binding))) (gv-letplace (getter setter) place (macroexp-let2 nil vnew (cadr binding) (if (symbolp place) diff --git a/lisp/emacs-lisp/gv.el b/lisp/emacs-lisp/gv.el index eaab6439ad..1db9d96d99 100644 --- a/lisp/emacs-lisp/gv.el +++ b/lisp/emacs-lisp/gv.el @@ -87,7 +87,11 @@ with a (not necessarily copyable) Elisp expression that returns the value to set it to. DO must return an Elisp expression." (cond - ((symbolp place) (funcall do place (lambda (v) `(setq ,place ,v)))) + ((symbolp place) + (let ((me (macroexpand-1 place macroexpand-all-environment))) + (if (eq me place) + (funcall do place (lambda (v) `(setq ,place ,v))) + (gv-get me do)))) ((not (consp place)) (signal 'gv-invalid-place (list place))) (t (let* ((head (car place)) diff --git a/test/lisp/emacs-lisp/cl-macs-tests.el b/test/lisp/emacs-lisp/cl-macs-tests.el index 19ede627a1..2a647e0830 100644 --- a/test/lisp/emacs-lisp/cl-macs-tests.el +++ b/test/lisp/emacs-lisp/cl-macs-tests.el @@ -539,7 +539,20 @@ collection clause." ((p (gv-synthetic-place cl (lambda (v) `(setcar l ,v))))) (cl-incf p))) l) - '(1)))) + '(1))) + ;; Make sure `gv-synthetic-place' isn't macro-expanded before + ;; `cl-letf' gets to see its `gv-expander'. + (should (equal + (condition-case err + (let ((x 1)) + (list x + (cl-letf (((gv-synthetic-place (+ 1 2) + (lambda (v) `(setq x ,v))) + 7)) + x) + x)) + (error err)) + '(1 7 3)))) (ert-deftest cl-macs-loop-conditional-step-clauses () "These tests failed under the initial fixes in #bug#29799." commit 1d1158397bce41466078e384eed2d1e214e206de Author: Gregory Heytings Date: Sat Sep 3 22:43:26 2022 +0000 Look up keybindings in correct buffer in describe-function. * lisp/help-fns.el (help-fns--key-bindings): New parameter. Use it when looking up keybindings. (describe-function-1): Add the buffer in which the command was invoked as argument to 'help-fns--key-bindings'. Fixes bug#57568. diff --git a/lisp/help-fns.el b/lisp/help-fns.el index bb5b3bb71f..3f3a5747dc 100644 --- a/lisp/help-fns.el +++ b/lisp/help-fns.el @@ -510,13 +510,15 @@ the C sources, too." (src-file (locate-library file-name t nil 'readable))) (and src-file (file-readable-p src-file) src-file)))))) -(defun help-fns--key-bindings (function) +(defun help-fns--key-bindings (function orig-buffer) (when (commandp function) (let ((pt2 (with-current-buffer standard-output (point))) (remapped (command-remapping function))) (unless (memq remapped '(ignore undefined)) - (let* ((all-keys (where-is-internal - (or remapped function) overriding-local-map nil nil)) + (let* ((all-keys + (with-current-buffer orig-buffer + (where-is-internal + (or remapped function) overriding-local-map nil nil))) (seps (seq-group-by (lambda (key) (and (vectorp key) @@ -1129,7 +1131,7 @@ Returns a list of the form (REAL-FUNCTION DEF ALIASED REAL-DEF)." (string-match "\\([^\\]=\\|[^=]\\|\\`\\)\\\\[[{<]" doc-raw) (autoload-do-load real-def)) - (help-fns--key-bindings function) + (help-fns--key-bindings function describe-function-orig-buffer) (with-current-buffer standard-output (let ((doc (condition-case nil ;; FIXME: Maybe `help-fns--signature' should return `doc' commit 22bee93d92567a1b01ebf7354089e6695e134611 Author: Jeff Walsh Date: Thu Jun 9 10:02:01 2022 +1000 Update error message to reflect variable rename * src/comp.c (Fcomp_el_to_eln_filename): Update error message. (Bug#55861) [ According to the Git metadata, this commit 8436e0bee9cf7a was already merged from `emacs-28`, yet the code says it was not. :-( ] diff --git a/src/comp.c b/src/comp.c index 70e7d5a8bb..4813ca04a9 100644 --- a/src/comp.c +++ b/src/comp.c @@ -4467,7 +4467,7 @@ the latter is supposed to be used by the Emacs build procedure. */) } if (NILP (base_dir)) error ("Cannot find suitable directory for output in " - "`comp-native-load-path'."); + "`native-comp-eln-load-path'."); } if (!file_name_absolute_p (SSDATA (base_dir))) commit 9788f00cab2dd1ef53866a8a786c0f02a54d5a6f Author: Eli Zaretskii Date: Sat Sep 3 19:12:49 2022 +0300 ; Fix last change. diff --git a/doc/emacs/frames.texi b/doc/emacs/frames.texi index 93a6cd70d3..8a255fa40f 100644 --- a/doc/emacs/frames.texi +++ b/doc/emacs/frames.texi @@ -232,7 +232,7 @@ scrolling with the @key{Ctrl} modifier. When this mode is enabled, mouse wheel produces special events like @code{wheel-up} and @code{wheel-down}. (Some older systems report them as @code{mouse-4} and @code{mouse-5}.) If the mouse has a horizontal scroll wheel, it -produces @code{wheel-left} and @code{wheel-right} events as well.. +produces @code{wheel-left} and @code{wheel-right} events as well. @vindex mouse-wheel-scroll-amount-horizontal Emacs also supports horizontal scrolling with the @key{Shift} commit 65e3568293b5c3188fad626617027eea54882b3d Author: Eli Zaretskii Date: Sat Sep 3 19:11:51 2022 +0300 Fix indexing of mouse-wheel events * doc/emacs/frames.texi (Mouse Commands): Add index entries for wheel events. diff --git a/doc/emacs/frames.texi b/doc/emacs/frames.texi index d78cbffaa7..93a6cd70d3 100644 --- a/doc/emacs/frames.texi +++ b/doc/emacs/frames.texi @@ -215,6 +215,10 @@ deactivating the mark. @xref{Shift Selection}. @vindex mouse-wheel-follow-mouse @vindex mouse-wheel-scroll-amount @vindex mouse-wheel-progressive-speed +@cindex wheel-up, a mouse event +@cindex wheel-down, a mouse event +@cindex wheel-left, a mouse event +@cindex wheel-right, a mouse event Some mice have a ``wheel'' which can be used for scrolling. Emacs supports scrolling windows with the mouse wheel, by default, on most graphical displays. To toggle this feature, use @kbd{M-x @@ -224,7 +228,11 @@ buffers are scrolled. The variable @code{mouse-wheel-progressive-speed} determines whether the scroll speed is linked to how fast you move the wheel. This mode also supports increasing or decreasing the font size, by default bound to -scrolling with the @key{Ctrl} modifier. +scrolling with the @key{Ctrl} modifier. When this mode is enabled, +mouse wheel produces special events like @code{wheel-up} and +@code{wheel-down}. (Some older systems report them as @code{mouse-4} +and @code{mouse-5}.) If the mouse has a horizontal scroll wheel, it +produces @code{wheel-left} and @code{wheel-right} events as well.. @vindex mouse-wheel-scroll-amount-horizontal Emacs also supports horizontal scrolling with the @key{Shift} commit aace5455b015c3d1a9ae7fd9c7eb14fa1250342f Merge: ec72b55657 99a5a72537 Author: Eli Zaretskii Date: Sat Sep 3 19:02:18 2022 +0300 Merge branch 'master' of git.savannah.gnu.org:/srv/git/emacs commit ec72b55657bde93d81ecabceaeed5aaadbb41f34 Author: Eli Zaretskii Date: Sat Sep 3 19:01:21 2022 +0300 Revert "* doc/emacs/commands.texi (Mice): Improve indexing." This reverts commit 1cea0ae4133bb22fd70d483df105e5a4653bc56c. The index entries it added don't belong to the place where they were added. diff --git a/doc/emacs/commands.texi b/doc/emacs/commands.texi index 9bf046704c..64e75c9609 100644 --- a/doc/emacs/commands.texi +++ b/doc/emacs/commands.texi @@ -151,8 +151,6 @@ commands in the same way you bind them to keyboard events mouse in Emacs; @pxref{Mouse Commands}, and the sections that follow it, for more details about mouse commands in Emacs. -@cindex wheel-down -@cindex wheel-up When you click the left mouse button, Emacs receives a @code{mouse-1} event. To see what command is bound to that event, you can type @kbd{C-h c} and then press the left mouse button. Similarly, @@ -166,8 +164,6 @@ system configuration. will report @code{mouse-4} and @code{mouse-5}, while all other systems will report @code{wheel-down} and @code{wheel-up}. -@cindex wheel-left -@cindex wheel-right Some mice also have a horizontal scroll wheel, and touchpads usually support scrolling horizontally as well. These events are reported as @code{wheel-left} and @code{wheel-right} on all systems other than commit 99a5a72537be811ae4220d9b58329991d6aa3d4d Author: Mattias Engdegård Date: Sat Sep 3 16:35:16 2022 +0200 lisp/emacs-lisp/seq.el: remove unnecessary compatibility code * lisp/emacs-lisp/seq.el (seq-take, seq--activate-font-lock-keywords): Simplify unnecessarily guarded code, as this file will only ever be used with the same version of Emacs. diff --git a/lisp/emacs-lisp/seq.el b/lisp/emacs-lisp/seq.el index b6f0f66e5b..1b4a49e4e3 100644 --- a/lisp/emacs-lisp/seq.el +++ b/lisp/emacs-lisp/seq.el @@ -618,13 +618,7 @@ Signal an error if SEQUENCE is empty." (cl-defmethod seq-take ((list list) n) "Optimized implementation of `seq-take' for lists." - (if (eval-when-compile (fboundp 'take)) - (take n list) - (let ((result '())) - (while (and list (> n 0)) - (setq n (1- n)) - (push (pop list) result)) - (nreverse result)))) + (take n list)) (cl-defmethod seq-drop-while (pred (list list)) "Optimized implementation of `seq-drop-while' for lists." @@ -655,16 +649,6 @@ Signal an error if SEQUENCE is empty." sequence (concat sequence))) -(defun seq--activate-font-lock-keywords () - "Activate font-lock keywords for some symbols defined in seq." - (font-lock-add-keywords 'emacs-lisp-mode - '("\\" "\\"))) - -(unless (fboundp 'elisp--font-lock-flush-elisp-buffers) - ;; In Emacs≥25, (via elisp--font-lock-flush-elisp-buffers and a few others) - ;; we automatically highlight macros. - (add-hook 'emacs-lisp-mode-hook #'seq--activate-font-lock-keywords)) - (defun seq-split (sequence length) "Split SEQUENCE into a list of sub-sequences of at most LENGTH. All the sub-sequences will be of LENGTH, except the last one, commit 1cea0ae4133bb22fd70d483df105e5a4653bc56c Author: Stefan Kangas Date: Sat Sep 3 17:47:14 2022 +0200 * doc/emacs/commands.texi (Mice): Improve indexing. diff --git a/doc/emacs/commands.texi b/doc/emacs/commands.texi index 64e75c9609..9bf046704c 100644 --- a/doc/emacs/commands.texi +++ b/doc/emacs/commands.texi @@ -151,6 +151,8 @@ commands in the same way you bind them to keyboard events mouse in Emacs; @pxref{Mouse Commands}, and the sections that follow it, for more details about mouse commands in Emacs. +@cindex wheel-down +@cindex wheel-up When you click the left mouse button, Emacs receives a @code{mouse-1} event. To see what command is bound to that event, you can type @kbd{C-h c} and then press the left mouse button. Similarly, @@ -164,6 +166,8 @@ system configuration. will report @code{mouse-4} and @code{mouse-5}, while all other systems will report @code{wheel-down} and @code{wheel-up}. +@cindex wheel-left +@cindex wheel-right Some mice also have a horizontal scroll wheel, and touchpads usually support scrolling horizontally as well. These events are reported as @code{wheel-left} and @code{wheel-right} on all systems other than commit b39daf417b16e2fe6c948f71af05373bcdca0e10 Merge: b01d529e8d 9a0c469085 Author: Stefan Monnier Date: Sat Sep 3 11:28:44 2022 -0400 Merge branch 'master' of git+ssh://git.sv.gnu.org/srv/git/emacs commit 9a0c469085352cd1813fb1eb44045404366b7963 Author: Eli Zaretskii Date: Sat Sep 3 18:07:53 2022 +0300 ; * doc/emacs/commands.texi: Fix a typo. diff --git a/doc/emacs/commands.texi b/doc/emacs/commands.texi index ada3bf6a43..64e75c9609 100644 --- a/doc/emacs/commands.texi +++ b/doc/emacs/commands.texi @@ -141,7 +141,7 @@ use @key{F1} to display a list of commands starting with @key{ESC}. @node Mice @section Mice -@cindexd mouse input +@cindex mouse input By default, Emacs supports all the normal mouse actions like setting the cursor by clicking on the left mouse button, and selecting an area commit b01d529e8de00b38a2a9e401254a34f018ee4004 Author: Stefan Monnier Date: Sat Sep 3 10:52:57 2022 -0400 * lisp/emacs-lisp/macroexp.el (macroexp--compiler-macro): Soften message Clarify that the error is "harmless". diff --git a/lisp/emacs-lisp/macroexp.el b/lisp/emacs-lisp/macroexp.el index c3ba1b36d4..f4df40249d 100644 --- a/lisp/emacs-lisp/macroexp.el +++ b/lisp/emacs-lisp/macroexp.el @@ -110,7 +110,8 @@ each clause." (let ((symbols-with-pos-enabled t)) (apply handler form (cdr form))) (error - (message "Compiler-macro error for %S: Handler: %S\n%S" (car form) handler err) + (message "Warning: Optimization failure for %S: Handler: %S\n%S" + (car form) handler err) form))) (defun macroexp--funcall-if-compiled (_form) commit d60e930d34fe0f4a88a790f98dcd43999327240c Author: Stefan Monnier Date: Sat Sep 3 10:46:46 2022 -0400 * lisp/emacs-lisp/cl-macs.el: Use `define-symbol-prop` (bug#50869) (cl-define-compiler-macro, cl-defstruct, cl-deftype): Prefer `define-symbol-prop` over `put` so `unload-feature` can undo those definitions. diff --git a/lisp/emacs-lisp/cl-macs.el b/lisp/emacs-lisp/cl-macs.el index 80ca43c902..edd633675d 100644 --- a/lisp/emacs-lisp/cl-macs.el +++ b/lisp/emacs-lisp/cl-macs.el @@ -3105,7 +3105,7 @@ To see the documentation for a defined struct type, use `(and ,pred-form t))) forms) (push `(eval-and-compile - (put ',name 'cl-deftype-satisfies ',predicate)) + (define-symbol-prop ',name 'cl-deftype-satisfies ',predicate)) forms)) (let ((pos 0) (descp descs)) (while descp @@ -3570,7 +3570,7 @@ and then returning foo." (cl-defun ,fname ,(if (memq '&whole args) (delq '&whole args) (cons '_cl-whole-arg args)) ,@body) - (put ',func 'compiler-macro #',fname)))) + (define-symbol-prop ',func 'compiler-macro #',fname)))) ;;;###autoload (defun cl-compiler-macroexpand (form) @@ -3679,8 +3679,8 @@ macro that returns its `&whole' argument." The type name can then be used in `cl-typecase', `cl-check-type', etc." (declare (debug cl-defmacro) (doc-string 3) (indent 2)) `(cl-eval-when (compile load eval) - (put ',name 'cl-deftype-handler - (cl-function (lambda (&cl-defs ('*) ,@arglist) ,@body))))) + (define-symbol-prop ',name 'cl-deftype-handler + (cl-function (lambda (&cl-defs ('*) ,@arglist) ,@body))))) (cl-deftype extended-char () '(and character (not base-char))) ;; Define fixnum so `cl-typep' recognize it and the type check emitted commit bf37ea1873cd2cec1072afb10e885b3a654e8829 Author: Stefan Monnier Date: Sat Sep 3 10:40:47 2022 -0400 * lisp/loadhist.el (loadhist-unload-element): Remove auxiliary function info See bug#50869. diff --git a/lisp/loadhist.el b/lisp/loadhist.el index b4ed043246..0cb02f072e 100644 --- a/lisp/loadhist.el +++ b/lisp/loadhist.el @@ -171,6 +171,13 @@ unloading." (cond ((null hist) (defalias fun nil) + ;; FIXME: Arguably these properties should be applied via + ;; `define-symbol-prop', but most code still uses just `put'. + ;; FIXME: Maybe these properties should be attached to the + ;; function itself (as for `advertised-calling-convention') + ;; rather than to its symbol. + (if (get fun 'compiler-macro) (put fun 'compiler-macro nil)) + (if (get fun 'gv-expander) (put fun 'gv-expander nil)) ;; Override the change that `defalias' just recorded. (put fun 'function-history nil)) ((equal (car hist) loadhist-unload-filename) commit 06f440eb814636fc6c5ed9d785a2f5ef980ea5e8 Author: Eli Zaretskii Date: Sat Sep 3 17:38:53 2022 +0300 ; Fix recent additions to Emacs manual * doc/emacs/commands.texi (User Input, Mice): Fix punctuation, indexing, and wording. diff --git a/doc/emacs/commands.texi b/doc/emacs/commands.texi index 9d08dd057c..ada3bf6a43 100644 --- a/doc/emacs/commands.texi +++ b/doc/emacs/commands.texi @@ -69,7 +69,7 @@ where the @key{Meta} key does not function reliably. Emacs has extensive support for using mouse buttons, mouse wheels and other pointing devices like touchpads and touch screens. -@xref{Mice} for details. +@xref{Mice}, for details. @cindex keys stolen by window manager @cindex window manager, keys stolen by @@ -141,17 +141,20 @@ use @key{F1} to display a list of commands starting with @key{ESC}. @node Mice @section Mice +@cindexd mouse input By default, Emacs supports all the normal mouse actions like setting the cursor by clicking on the left mouse button, and selecting an area -by dragging the mouse cursor. All mouse actions can be bound to -commands in the same way you bind keyboard events (@pxref{Keys}). +by dragging the mouse pointer. All mouse actions can be used to bind +commands in the same way you bind them to keyboard events +(@pxref{Keys}). This section provides a general overview of using the +mouse in Emacs; @pxref{Mouse Commands}, and the sections that follow +it, for more details about mouse commands in Emacs. -@cindex mouse-1 When you click the left mouse button, Emacs receives a -@code{mouse-1} event. To see what command that event is bound to, you -can say @kbd{C-h c} and then use the left mouse button. Similarly, -the middle mouse button is @code{mouse-2} and the left mouse button is +@code{mouse-1} event. To see what command is bound to that event, you +can type @kbd{C-h c} and then press the left mouse button. Similarly, +the middle mouse button is @code{mouse-2} and the right mouse button is @code{mouse-3}. If you have a mouse with a wheel, the wheel events are commonly bound to either @code{wheel-down} or @code{wheel-up}, or @code{mouse-4} and @code{mouse-5}, but that depends on the operating @@ -172,6 +175,7 @@ can bind a special command that triggers when you, for instance, holds down the Meta key and then uses the middle mouse button. In that case, the event name will be @code{M-mouse-2}. +@cindex touchscreen events On some systems, you can also bind commands for handling touch screen events. In that case, the events are called @code{touchscreen-update} and @code{touchscreen-end}. commit 996f8d85d7dd5a43b1e97c0d763baa67fd6122b0 Author: Stefan Monnier Date: Sat Sep 3 10:33:02 2022 -0400 * lisp/help-fns.el (find-lisp-object-file-name): Revert last change diff --git a/lisp/help-fns.el b/lisp/help-fns.el index 88e553c1a0..bb5b3bb71f 100644 --- a/lisp/help-fns.el +++ b/lisp/help-fns.el @@ -424,7 +424,7 @@ If ALSO-C-SOURCE is non-nil, instead of returning `C-source', this function will attempt to locate the definition of OBJECT in the C sources, too." (let* ((autoloaded (autoloadp type)) - (file-name (or (and autoloaded (autoload-file type)) + (file-name (or (and autoloaded (nth 1 type)) (symbol-file ;; FIXME: Why do we have this weird "If TYPE is the ;; value returned by `symbol-function' for a function commit 88b895ee56693b460e2b04f681f138da36635c4d Author: Po Lu Date: Sat Sep 3 21:50:04 2022 +0800 Improve documentation of scroll wheel event types in new Mice node * doc/emacs/commands.texi (Mice): Improve documentation of scroll wheel event types; fix doc for Emacs 29 and describe horizontal wheel movement. diff --git a/doc/emacs/commands.texi b/doc/emacs/commands.texi index c16ed4797e..9d08dd057c 100644 --- a/doc/emacs/commands.texi +++ b/doc/emacs/commands.texi @@ -148,17 +148,24 @@ by dragging the mouse cursor. All mouse actions can be bound to commands in the same way you bind keyboard events (@pxref{Keys}). @cindex mouse-1 - When you click the left mouse button, Emacs receives a @code{mouse-1} -event. To see what command that event is bound to, you can say -@kbd{C-h c} and then use the left mouse button. Similarly, the middle -mouse button is @code{mouse-2} and the left mouse button is + When you click the left mouse button, Emacs receives a +@code{mouse-1} event. To see what command that event is bound to, you +can say @kbd{C-h c} and then use the left mouse button. Similarly, +the middle mouse button is @code{mouse-2} and the left mouse button is @code{mouse-3}. If you have a mouse with a wheel, the wheel events -are commonly bound to @code{mouse-4} and @code{mouse-5}, but that -depends on the device. - - For mouse-wheel events can also be @code{wheel-up} or -@code{wheel-down}, and the easiest way to tell is to just use @kbd{C-h -c} and then use the mouse. +are commonly bound to either @code{wheel-down} or @code{wheel-up}, or +@code{mouse-4} and @code{mouse-5}, but that depends on the operating +system configuration. + + In general, legacy X systems and terminals (@pxref{Text-Only Mouse}) +will report @code{mouse-4} and @code{mouse-5}, while all other systems +will report @code{wheel-down} and @code{wheel-up}. + + Some mice also have a horizontal scroll wheel, and touchpads usually +support scrolling horizontally as well. These events are reported as +@code{wheel-left} and @code{wheel-right} on all systems other than +terminals and legacy X systems, where they are @code{mouse-6} and +@code{mouse-7}. You can also combine keyboard modifiers with mouse events, so you can bind a special command that triggers when you, for instance, holds commit b861adce060a8c11bfa302b820975a5f32a07cd1 Author: Po Lu Date: Sat Sep 3 21:45:46 2022 +0800 ; * character.c (Fmax_char): Fix build with type checking. diff --git a/src/character.c b/src/character.c index dc21649b22..5df49adade 100644 --- a/src/character.c +++ b/src/character.c @@ -185,7 +185,9 @@ by the Unicode Standard. */ attributes: const) (Lisp_Object unicode) { - return unicode ? make_fixnum (MAX_UNICODE_CHAR) : make_fixnum (MAX_CHAR); + return (!NILP (unicode) + ? make_fixnum (MAX_UNICODE_CHAR) + : make_fixnum (MAX_CHAR)); } DEFUN ("unibyte-char-to-multibyte", Funibyte_char_to_multibyte, commit ab5ca80e745e86c33e6bec86c9331978d071d1a4 Author: Po Lu Date: Sat Sep 3 21:39:30 2022 +0800 Work around another X server bug in crossing event dispatch * src/xterm.c (xi_focus_handle_for_device): Clear implicit focus along with FocusOut. (bug#57468) (x_mouse_leave): Avoid invalid reads of dpyinfo->x_focus_event_frame on input extension builds. diff --git a/src/xterm.c b/src/xterm.c index 19d2198cdf..accd1b90fb 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -12740,6 +12740,25 @@ xi_focus_handle_for_device (struct x_display_info *dpyinfo, case XI_FocusOut: device->focus_frame = NULL; + + /* So, unfortunately, the X Input Extension is implemented such + that means XI_Leave events will not have their focus field + set if the core focus is transferred to another window after + an entry event that pretends to (or really does) set the + implicit focus. In addition, if the core focus is set, but + the extension focus on the client pointer is not, all + XI_Enter events will have their focus fields set, despite not + actually changing the effective focus window. Combined with + almost all window managers not setting the focus on input + extension devices, this means that Emacs will continue to + think the implicit focus is set on one of its frames if the + actual (core) focus is transferred to another window while + the pointer remains inside a frame. The only workaround in + this case is to clear the implicit focus along with + XI_FocusOut events, which is not correct at all, but better + than leaving frames in an incorrectly-focused state. + (bug#57468) */ + device->focus_implicit_frame = NULL; break; case XI_Enter: @@ -13163,7 +13182,13 @@ x_mouse_leave (struct x_display_info *dpyinfo) hlinfo->mouse_face_mouse_frame = NULL; } - x_new_focus_frame (dpyinfo, dpyinfo->x_focus_event_frame); +#ifdef HAVE_XINPUT2 + if (!dpyinfo->supports_xi2) + /* I don't understand what the call below is supposed to do. But + reading dpyinfo->x_focus_event_frame is invalid on input + extension builds, so disable it there. */ +#endif + x_new_focus_frame (dpyinfo, dpyinfo->x_focus_event_frame); } #endif commit 419d7579056850057d9897f012acd30d2670b7b9 Author: Lars Ingebrigtsen Date: Sat Sep 3 15:12:16 2022 +0200 Add a Mice node in the Emacs manual * doc/emacs/commands.texi (User Input): Don't claim to not document mouse buttons. (Mice): New node (bug#50948). diff --git a/doc/emacs/commands.texi b/doc/emacs/commands.texi index 431cc2e5ce..c16ed4797e 100644 --- a/doc/emacs/commands.texi +++ b/doc/emacs/commands.texi @@ -24,8 +24,8 @@ input. GNU Emacs is primarily designed for use with the keyboard. While it is possible to use the mouse to issue editing commands through the -menu bar and tool bar, that is not as efficient as using the keyboard. -Therefore, this manual mainly documents how to edit with the keyboard. +menu bar and tool bar, that is usually not as efficient as using the +keyboard. @cindex control character Keyboard input into Emacs is based on a heavily-extended version of @@ -67,6 +67,10 @@ where the @key{Meta} key does not function reliably. Emacs supports 3 additional modifier keys, see @ref{Modifier Keys}. + Emacs has extensive support for using mouse buttons, mouse wheels +and other pointing devices like touchpads and touch screens. +@xref{Mice} for details. + @cindex keys stolen by window manager @cindex window manager, keys stolen by On graphical displays, the window manager might block some keyboard @@ -135,6 +139,36 @@ exception to this rule is @key{ESC}: @kbd{@key{ESC} C-h} is equivalent to @kbd{C-M-h}, which does something else entirely. You can, however, use @key{F1} to display a list of commands starting with @key{ESC}. +@node Mice +@section Mice + + By default, Emacs supports all the normal mouse actions like setting +the cursor by clicking on the left mouse button, and selecting an area +by dragging the mouse cursor. All mouse actions can be bound to +commands in the same way you bind keyboard events (@pxref{Keys}). + +@cindex mouse-1 + When you click the left mouse button, Emacs receives a @code{mouse-1} +event. To see what command that event is bound to, you can say +@kbd{C-h c} and then use the left mouse button. Similarly, the middle +mouse button is @code{mouse-2} and the left mouse button is +@code{mouse-3}. If you have a mouse with a wheel, the wheel events +are commonly bound to @code{mouse-4} and @code{mouse-5}, but that +depends on the device. + + For mouse-wheel events can also be @code{wheel-up} or +@code{wheel-down}, and the easiest way to tell is to just use @kbd{C-h +c} and then use the mouse. + + You can also combine keyboard modifiers with mouse events, so you +can bind a special command that triggers when you, for instance, holds +down the Meta key and then uses the middle mouse button. In that +case, the event name will be @code{M-mouse-2}. + + On some systems, you can also bind commands for handling touch +screen events. In that case, the events are called +@code{touchscreen-update} and @code{touchscreen-end}. + @node Commands @section Keys and Commands diff --git a/doc/emacs/emacs.texi b/doc/emacs/emacs.texi index 5c022684cd..d0e048ae06 100644 --- a/doc/emacs/emacs.texi +++ b/doc/emacs/emacs.texi @@ -148,6 +148,7 @@ Important General Concepts function keys). * Keys:: Key sequences: what you type to request one editing action. +* Mice:: Using the mouse and keypads. * Commands:: Named functions run by key sequences to do editing. * Entering Emacs:: Starting Emacs from the shell. * Exiting:: Stopping or killing Emacs. commit 2d337ca577566198b00e6e144208f3773b212589 Author: Philip Kaludercic Date: Sat Sep 3 14:15:29 2022 +0200 * subr.el (buffer-match-p): Use 'pcase' (bug#57502) diff --git a/lisp/subr.el b/lisp/subr.el index 2ffc594997..e4d3245537 100644 --- a/lisp/subr.el +++ b/lisp/subr.el @@ -6992,32 +6992,32 @@ CONDITION is either: (lambda (conditions) (catch 'match (dolist (condition conditions) - (when (cond - ((eq condition t)) - ((stringp condition) - (string-match-p condition (buffer-name buffer))) - ((functionp condition) - (if (eq 1 (cdr (func-arity condition))) - (funcall condition buffer) - (funcall condition buffer arg))) - ((eq (car-safe condition) 'major-mode) - (eq - (buffer-local-value 'major-mode buffer) - (cdr condition))) - ((eq (car-safe condition) 'derived-mode) - (provided-mode-derived-p - (buffer-local-value 'major-mode buffer) - (cdr condition))) - ((eq (car-safe condition) 'not) - (not (funcall match (cdr condition)))) - ((eq (car-safe condition) 'or) - (funcall match (cdr condition))) - ((eq (car-safe condition) 'and) - (catch 'fail - (dolist (c (cdr conditions)) - (unless (funcall match c) - (throw 'fail nil))) - t))) + (when (pcase condition + ('t t) + ((pred stringp) + (string-match-p condition (buffer-name buffer))) + ((pred functionp) + (if (eq 1 (cdr (func-arity condition))) + (funcall condition buffer) + (funcall condition buffer arg))) + (`(major-mode . ,mode) + (eq + (buffer-local-value 'major-mode buffer) + mode)) + (`(derived-mode . ,mode) + (provided-mode-derived-p + (buffer-local-value 'major-mode buffer) + mode)) + (`(not . ,cond) + (not (funcall match cond))) + (`(or . ,args) + (funcall match args)) + (`(and . ,args) + (catch 'fail + (dolist (c args) + (unless (funcall match (list c)) + (throw 'fail nil))) + t))) (throw 'match t))))))) (funcall match (list condition)))) commit 252f135f441e4119251b273c8074ff005f425f26 Author: Lars Ingebrigtsen Date: Sat Sep 3 14:46:21 2022 +0200 Mention M-x list-packages in the Help node in the Emacs manual * doc/emacs/help.texi (Help): Mention listing packages (bug#50936). diff --git a/doc/emacs/help.texi b/doc/emacs/help.texi index d206dee385..84b082825c 100644 --- a/doc/emacs/help.texi +++ b/doc/emacs/help.texi @@ -47,7 +47,7 @@ window displaying the @samp{*Help*} buffer will be reused instead. If you are looking for a certain feature, but don't know what it is called or where to look, we recommend three methods. First, try an apropos command, then try searching the manual index, then look in the -FAQ and the package keywords. +FAQ and the package keywords, and finally try listing external packages. @table @kbd @item C-h a @var{topics} @key{RET} @@ -70,6 +70,9 @@ This displays the Emacs FAQ, using Info. @item C-h p This displays the available Emacs packages based on keywords. @xref{Package Keywords}. + +@item M-x list-packages +This displays a list of external packages. @xref{Packages}. @end table @kbd{C-h} or @key{F1} mean ``help'' in various other contexts as commit 6c11214dc1124bcb459088e89334e16e46127e16 Author: Lars Ingebrigtsen Date: Sat Sep 3 14:23:26 2022 +0200 Inhibit nativecomp of loaddefs files * lisp/emacs-lisp/generate-lisp-file.el (generate-lisp-file-trailer): Allow inhibiting nativecomp. * lisp/emacs-lisp/loaddefs-gen.el (loaddefs-generate--rubric): Inhibit native-comp, because it's not very useful for loaddefs files. diff --git a/lisp/emacs-lisp/generate-lisp-file.el b/lisp/emacs-lisp/generate-lisp-file.el index 8896a3f701..7b087a4ecb 100644 --- a/lisp/emacs-lisp/generate-lisp-file.el +++ b/lisp/emacs-lisp/generate-lisp-file.el @@ -63,12 +63,12 @@ inserted." (cl-defun generate-lisp-file-trailer (file &key version inhibit-provide (coding 'utf-8-emacs-unix) autoloads - compile provide) + compile provide inhibit-native-compile) "Insert a standard trailer for FILE. By default, this trailer inhibits version control, byte compilation, updating autoloads, and uses a `utf-8-emacs-unix' coding system. These can be inhibited by providing non-nil -values to the VERSION, NO-PROVIDE, AUTOLOADS and COMPILE +values to the VERSION, AUTOLOADS, COMPILE and NATIVE-COMPILE keyword arguments. CODING defaults to `utf-8-emacs-unix'. Use a nil value to @@ -79,7 +79,11 @@ If PROVIDE is non-nil, use that in the `provide' statement instead of using FILE as the basis. If `standard-output' is bound to a buffer, insert in that buffer. -If no, insert at point in the current buffer." +If no, insert at point in the current buffer. + +If INHITBIT-NATIVE-COMPILE is non-nil, add a cookie to inhibit +native compilation. (By default, a file will be native-compiled +if it's also byte-compiled)." (with-current-buffer (if (bufferp standard-output) standard-output (current-buffer)) @@ -96,9 +100,11 @@ If no, insert at point in the current buffer." (unless version (insert ";; version-control: never\n")) (unless compile - (insert ";; no-byte-" "compile: t\n")) ;; #$ is byte-compiled into nil. + (insert ";; no-byte-" "compile: t\n")) (unless autoloads (insert ";; no-update-autoloads: t\n")) + (when inhibit-native-compile + (insert ";; no-native-" "compile: t\n")) (when coding (insert (format ";; coding: %s\n" (if (eq coding t) diff --git a/lisp/emacs-lisp/loaddefs-gen.el b/lisp/emacs-lisp/loaddefs-gen.el index e13b92bab8..005a46c2d7 100644 --- a/lisp/emacs-lisp/loaddefs-gen.el +++ b/lisp/emacs-lisp/loaddefs-gen.el @@ -504,6 +504,7 @@ If COMPILE, don't include a \"don't compile\" cookie." (generate-lisp-file-trailer file :provide (and (stringp feature) feature) :compile compile + :inhibit-native-compile t :inhibit-provide (not feature)) (buffer-string)))) commit 91ba20fff159cc88c87e09f5a8256d6412c98990 Author: Po Lu Date: Sat Sep 3 19:55:31 2022 +0800 Work around potential X server bug * src/xterm.c (handle_one_xevent): Ignore core crossing events on input extension builds. The X server is not actually supposed to deliver them to us, and it messes up MPX focus tracking. (bug#57468) diff --git a/src/xterm.c b/src/xterm.c index 138fa7ea6c..19d2198cdf 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -19276,6 +19276,18 @@ handle_one_xevent (struct x_display_info *dpyinfo, x_display_set_last_user_time (dpyinfo, event->xcrossing.time, event->xcrossing.send_event); +#ifdef HAVE_XINPUT2 + /* For whatever reason, the X server continues to deliver + EnterNotify and LeaveNotify events despite us selecting for + related XI_Enter and XI_Leave events. It's not just our + problem, since windows created by "xinput test-xi2" suffer + from the same defect. Simply ignore all such events while + the input extension is enabled. (bug#57468) */ + + if (dpyinfo->supports_xi2) + goto OTHER; +#endif + if (x_top_window_to_frame (dpyinfo, event->xcrossing.window)) x_detect_focus_change (dpyinfo, any, event, &inev.ie); @@ -19377,6 +19389,18 @@ handle_one_xevent (struct x_display_info *dpyinfo, x_display_set_last_user_time (dpyinfo, event->xcrossing.time, event->xcrossing.send_event); +#ifdef HAVE_XINPUT2 + /* For whatever reason, the X server continues to deliver + EnterNotify and LeaveNotify events despite us selecting for + related XI_Enter and XI_Leave events. It's not just our + problem, since windows created by "xinput test-xi2" suffer + from the same defect. Simply ignore all such events while + the input extension is enabled. (bug#57468) */ + + if (dpyinfo->supports_xi2) + goto OTHER; +#endif + #ifdef HAVE_XWIDGETS { struct xwidget_view *xvw = xwidget_view_from_window (event->xcrossing.window); @@ -19402,14 +19426,7 @@ handle_one_xevent (struct x_display_info *dpyinfo, #else f = x_top_window_to_frame (dpyinfo, event->xcrossing.window); #endif -#if defined USE_X_TOOLKIT && defined HAVE_XINPUT2 && !defined USE_MOTIF - /* The XI2 event mask is set on the frame widget, so this event - likely originates from the shell widget, which we aren't - interested in. (But don't ignore this on Motif, since we - want to clear the mouse face when a popup is active.) */ - if (dpyinfo->supports_xi2) - f = NULL; -#endif + if (f) { /* Now clear dpyinfo->last_mouse_motion_frame, or commit dcfe3314cd78e95d992fe00f757ce906d49586cd Author: Eli Zaretskii Date: Sat Sep 3 13:45:53 2022 +0300 Teach 'max-char' about the Unicode code range * src/character.c (Fmax_char): Accept an optional argument UNICODE, and, if non-nil, return the maximum codepoint defined by Unicode. * lisp/emacs-lisp/comp.el (comp-known-type-specifiers): Update the signature of 'max-char'. * etc/NEWS: * doc/lispref/nonascii.texi (Character Codes): Update the documentation of 'max-char'. diff --git a/doc/lispref/nonascii.texi b/doc/lispref/nonascii.texi index 6dc23637a7..71fee45c4a 100644 --- a/doc/lispref/nonascii.texi +++ b/doc/lispref/nonascii.texi @@ -404,9 +404,12 @@ This returns @code{t} if @var{charcode} is a valid character, and @cindex maximum value of character codepoint @cindex codepoint, largest value -@defun max-char +@defun max-char &optional unicode This function returns the largest value that a valid character -codepoint can have. +codepoint can have in Emacs. If the optional argument @var{unicode} +is non-@code{nil}, it returns the largest character codepoint defined +by the Unicode Standard (which is smaller than the maximum codepoint +supported by Emacs). @example @group diff --git a/etc/NEWS b/etc/NEWS index 8269d3e7bf..cc4714e71c 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2789,6 +2789,12 @@ request the name of the ".eln" file which defined a given symbol. +++ ** New macro 'with-memoization' provides a very primitive form of memoization. ++++ +** 'max-char' can now report the maximum codepoint according to Unicode. +When called with a new optional argument UNICODE non-nil, 'max-char' +will now report the maximum valid codepoint defined by the Unicode +Standard. + ** Themes --- diff --git a/lisp/emacs-lisp/comp.el b/lisp/emacs-lisp/comp.el index e10443588e..306ec918b1 100644 --- a/lisp/emacs-lisp/comp.el +++ b/lisp/emacs-lisp/comp.el @@ -462,7 +462,7 @@ Useful to hook into pass checkers.") (marker-buffer (function (marker) (or buffer null))) (markerp (function (t) boolean)) (max (function ((or number marker) &rest (or number marker)) number)) - (max-char (function () fixnum)) + (max-char (function (&optional t) fixnum)) (member (function (t list) list)) (memory-limit (function () integer)) (memq (function (t list) list)) diff --git a/src/character.c b/src/character.c index 968daccafa..dc21649b22 100644 --- a/src/character.c +++ b/src/character.c @@ -178,12 +178,14 @@ usage: (characterp OBJECT) */ return (CHARACTERP (object) ? Qt : Qnil); } -DEFUN ("max-char", Fmax_char, Smax_char, 0, 0, 0, - doc: /* Return the character of the maximum code. */ +DEFUN ("max-char", Fmax_char, Smax_char, 0, 1, 0, + doc: /* Return the maximum character code. +If UNICODE is non-nil, return the maximum character code defined +by the Unicode Standard. */ attributes: const) - (void) + (Lisp_Object unicode) { - return make_fixnum (MAX_CHAR); + return unicode ? make_fixnum (MAX_UNICODE_CHAR) : make_fixnum (MAX_CHAR); } DEFUN ("unibyte-char-to-multibyte", Funibyte_char_to_multibyte, commit db2f8b8415b538ccb43f11a2142567ec6c5451d9 Author: Stefan Kangas Date: Sat Sep 3 11:04:07 2022 +0200 Increase image-dired-show-all-from-dir-max-files to 1000 * lisp/image/image-dired.el (image-dired-show-all-from-dir-max-files): Increase to 1000. diff --git a/etc/NEWS b/etc/NEWS index ef9bfd08e3..8269d3e7bf 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2023,13 +2023,12 @@ This replaces the message most navigation commands in the thumbnail buffer used to show at the bottom of the screen. +++ -*** 'image-dired-show-all-from-dir-max-files' has been increased to 500. -This option controls asking for confirmation when starting Image-Dired -in a directory with many files. However, Image-Dired creates -thumbnails in the background these days, so this is not as important -as it used to be, back when entering a large directory could lock up -Emacs for tens of seconds. In addition, you can now customize this -option to nil to disable this confirmation completely. +*** 'image-dired-show-all-from-dir-max-files' increased to 1000. +This user option controls asking for confirmation when starting +Image-Dired in a directory with many files. Since Image-Dired creates +thumbnails in the background in recent versions, this is not as +important as it used to be. You can now also customize this option to +nil to disable this confirmation completely. --- *** 'image-dired-rotate-thumbnail-(left|right)' is now obsolete. diff --git a/lisp/image/image-dired.el b/lisp/image/image-dired.el index b92166c1dc..88f4ceaffb 100644 --- a/lisp/image/image-dired.el +++ b/lisp/image/image-dired.el @@ -323,7 +323,7 @@ Used by `image-dired-copy-with-exif-file-name'." :type 'string :version "29.1") -(defcustom image-dired-show-all-from-dir-max-files 500 +(defcustom image-dired-show-all-from-dir-max-files 1000 "Maximum number of files in directory before prompting. If there are more image files than this in a selected directory, commit 01534a2d31981cb831a1a650e9023b55dc2b1b2c Author: Lars Ingebrigtsen Date: Sat Sep 3 11:07:12 2022 +0200 Fix image-dired-util.el compile warning * lisp/image/image-dired-util.el (require): Require for cl-case. diff --git a/lisp/image/image-dired-util.el b/lisp/image/image-dired-util.el index cb1632e1be..6eb5fa18ce 100644 --- a/lisp/image/image-dired-util.el +++ b/lisp/image/image-dired-util.el @@ -24,6 +24,7 @@ ;;; Code: (require 'xdg) +(eval-when-compile (require 'cl-lib)) (defvar image-dired-dir) (defvar image-dired-thumbnail-storage) commit ec331e172de92ae62d4548262425256288fed61d Author: Stefan Kangas Date: Wed Aug 31 05:59:35 2022 +0200 Add new defgroup image-dired-dired * lisp/image/image-dired-dired.el (image-dired-dired-append-when-browsing): Rename from 'image-dired-append-when-browsing'. Update all uses. (image-dired-dired): New defgroup. (image-dired-dired-append-when-browsing) (image-dired-dired-disp-props): Use above new defgroup. diff --git a/lisp/image/image-dired-dired.el b/lisp/image/image-dired-dired.el index b15d46d111..ef0323d166 100644 --- a/lisp/image/image-dired-dired.el +++ b/lisp/image/image-dired-dired.el @@ -26,7 +26,15 @@ (require 'image-dired) -(defcustom image-dired-append-when-browsing nil +(defgroup image-dired-dired nil + "Dired specific commands for Image-Dired." + :prefix "image-dired-dired-" + :link '(info-link "(emacs) Image-Dired") + :group 'image-dired) + +(define-obsolete-variable-alias 'image-dired-append-when-browsing + 'image-dired-dired-append-when-browsing "29.1") +(defcustom image-dired-dired-append-when-browsing nil "Append thumbnails in thumbnail buffer when browsing. If non-nil, using `image-dired-next-line-and-display' and `image-dired-previous-line-and-display' will leave a trail of thumbnail @@ -34,7 +42,6 @@ images in the thumbnail buffer. If you enable this and want to clean the thumbnail buffer because it is filled with too many thumbnails, just call `image-dired-display-thumb' to display only the image at point. This value can be toggled using `image-dired-toggle-append-browsing'." - :group 'image-dired :type 'boolean) (defcustom image-dired-dired-disp-props t @@ -43,7 +50,6 @@ Used by `image-dired-next-line-and-display', `image-dired-previous-line-and-display' and `image-dired-mark-and-display-next'. If the database file is large, this can slow down image browsing in Dired and you might want to turn it off." - :group 'image-dired :type 'boolean) ;;;###autoload @@ -96,8 +102,7 @@ Used by `image-dired-dired-toggle-marked-thumbs'." "Move to next Dired line and display thumbnail image." (interactive nil dired-mode) (dired-next-line 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) + (image-dired-display-thumbs t image-dired-dired-append-when-browsing t) (if image-dired-dired-disp-props (image-dired-dired-display-properties))) @@ -105,18 +110,17 @@ Used by `image-dired-dired-toggle-marked-thumbs'." "Move to previous Dired line and display thumbnail image." (interactive nil dired-mode) (dired-previous-line 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) + (image-dired-display-thumbs t image-dired-dired-append-when-browsing t) (if image-dired-dired-disp-props (image-dired-dired-display-properties))) (defun image-dired-toggle-append-browsing () - "Toggle `image-dired-append-when-browsing'." + "Toggle `image-dired-dired-append-when-browsing'." (interactive nil dired-mode) - (setq image-dired-append-when-browsing - (not image-dired-append-when-browsing)) + (setq image-dired-dired-append-when-browsing + (not image-dired-dired-append-when-browsing)) (message "Append browsing %s" - (if image-dired-append-when-browsing + (if image-dired-dired-append-when-browsing "on" "off"))) @@ -124,8 +128,7 @@ Used by `image-dired-dired-toggle-marked-thumbs'." "Mark current file in Dired and display next thumbnail image." (interactive nil dired-mode) (dired-mark 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) + (image-dired-display-thumbs t image-dired-dired-append-when-browsing t) (if image-dired-dired-disp-props (image-dired-dired-display-properties))) @@ -233,7 +236,7 @@ With prefix argument, move ARG lines." :selected image-dired-dired-disp-props] ["Toggle append browsing" image-dired-toggle-append-browsing :style toggle - :selected image-dired-append-when-browsing] + :selected image-dired-dired-append-when-browsing] ["Toggle movement tracking" image-dired-toggle-movement-tracking :style toggle :selected image-dired-track-movement] commit 504d5c2da867a932f7bb43af8be114607d70a572 Author: Stefan Kangas Date: Wed Aug 31 05:52:11 2022 +0200 image-dired: Minor cleanups * lisp/image/image-dired-dired.el (image-dired-dired-after-readin-hook, image-dired-minor-mode): Minor doc fixes. (image-dired-dired-display-image): Don't accept ineffectual prefix argument. * lisp/image/image-dired-util.el (image-dired-file-name-at-point): Use when-let. (image-dired-display-thumb-properties): Redefine as obsolete function alias for 'image-dired-update-header-line'. * lisp/image/image-dired-util.el (image-dired-window-width-pixels): Make obsolete in favor of window-body-width. * lisp/image/image-dired.el (image-dired-line-up-dynamic) (image-dired-display-window-width): Don't use above obsolete function. diff --git a/lisp/image/image-dired-dired.el b/lisp/image/image-dired-dired.el index aa9fa38751..b15d46d111 100644 --- a/lisp/image/image-dired-dired.el +++ b/lisp/image/image-dired-dired.el @@ -81,7 +81,8 @@ previous -ARG, if ARG<0) files." (defun image-dired-dired-after-readin-hook () "Relocate existing thumbnail overlays in Dired buffer after reverting. Move them to their corresponding files if they still exist. -Otherwise, delete overlays." +Otherwise, delete overlays. +Used by `image-dired-dired-toggle-marked-thumbs'." (mapc (lambda (overlay) (when (overlay-get overlay 'put-image) (let* ((image-file (overlay-get overlay 'image-file)) @@ -289,12 +290,12 @@ With prefix argument ARG, create thumbnails even if they already exist image-dired-external-viewer file))) ;;;###autoload -(defun image-dired-dired-display-image (&optional arg) +(defun image-dired-dired-display-image (&optional _) "Display current image file. -See documentation for `image-dired-display-image' for more information. -With prefix argument ARG, display image in its original size." - (interactive "P" dired-mode) - (image-dired-display-image (dired-get-filename) arg)) +See documentation for `image-dired-display-image' for more information." + (declare (advertised-calling-convention () "29.1")) + (interactive nil dired-mode) + (image-dired-display-image (dired-get-filename))) (defun image-dired-copy-with-exif-file-name () "Copy file with unique name to main image directory. diff --git a/lisp/image/image-dired-external.el b/lisp/image/image-dired-external.el index 893d3da843..c26cedc9f2 100644 --- a/lisp/image/image-dired-external.el +++ b/lisp/image/image-dired-external.el @@ -142,8 +142,7 @@ Available format specifiers are the same as in :version "26.1" :type '(repeat (string :tag "Argument"))) -(defcustom image-dired-cmd-rotate-original-program - "jpegtran" +(defcustom image-dired-cmd-rotate-original-program "jpegtran" "Executable used to rotate original image. Used together with `image-dired-cmd-rotate-original-options'." :type 'file) @@ -171,8 +170,7 @@ original image file name and %t which is replaced by Used together with `image-dired-cmd-write-exif-data-options'." :type 'file) -(defcustom image-dired-cmd-write-exif-data-options - '("-%t=%v" "%f") +(defcustom image-dired-cmd-write-exif-data-options '("-%t=%v" "%f") "Arguments of command used to write EXIF data. Used with `image-dired-cmd-write-exif-data-program'. Available format specifiers are: %f which is replaced by diff --git a/lisp/image/image-dired-util.el b/lisp/image/image-dired-util.el index 1318b25a12..cb1632e1be 100644 --- a/lisp/image/image-dired-util.el +++ b/lisp/image/image-dired-util.el @@ -104,9 +104,8 @@ See also `image-dired-thumbnail-storage'." (defun image-dired-file-name-at-point () "Get abbreviated file name for thumbnail or display image at point." - (let ((f (image-dired-original-file-name))) - (when f - (abbreviate-file-name f)))) + (when-let ((f (image-dired-original-file-name))) + (abbreviate-file-name f))) (defun image-dired-associated-dired-buffer () "Get associated Dired buffer at point." @@ -119,10 +118,6 @@ See also `image-dired-thumbnail-storage'." (equal (window-buffer window) buf)) nil t)) -(defun image-dired-window-width-pixels (window) - "Calculate WINDOW width in pixels." - (* (window-width window) (frame-char-width))) - (defun image-dired-display-window () "Return window where `image-dired-display-image-buffer' is visible." (get-window-with-predicate @@ -152,6 +147,11 @@ See also `image-dired-thumbnail-storage'." "Return non-nil if there is an `image-dired' thumbnail at point." (get-text-property (point) 'image-dired-thumbnail)) +(defun image-dired-window-width-pixels (window) + "Calculate WINDOW width in pixels." + (declare (obsolete window-body-width "29.1")) + (* (window-width window) (frame-char-width))) + (provide 'image-dired-util) ;; Local Variables: diff --git a/lisp/image/image-dired.el b/lisp/image/image-dired.el index 87c2cd3f09..b92166c1dc 100644 --- a/lisp/image/image-dired.el +++ b/lisp/image/image-dired.el @@ -1056,13 +1056,13 @@ See also `image-dired-line-up-dynamic'." Calculate how many thumbnails fit." (interactive nil image-dired-thumbnail-mode) (let* ((char-width (frame-char-width)) - (width (image-dired-window-width-pixels (image-dired-thumbnail-window))) - (image-dired-thumbs-per-row - (/ width - (+ (* 2 image-dired-thumb-relief) - (* 2 image-dired-thumb-margin) - (image-dired-thumb-size 'width) - char-width)))) + (width (window-body-width (image-dired-thumbnail-window) t)) + (image-dired-thumbs-per-row + (/ width + (+ (* 2 image-dired-thumb-relief) + (* 2 image-dired-thumb-margin) + (image-dired-thumb-size 'width) + char-width)))) (image-dired-line-up))) (defun image-dired-line-up-interactive () @@ -1368,7 +1368,7 @@ completely fit)." (defun image-dired-display-window-width (window) "Return width, in pixels, of WINDOW." (declare (obsolete nil "29.1")) - (- (image-dired-window-width-pixels window) + (- (window-body-width window t) image-dired-display-window-width-correction)) (defun image-dired-display-window-height (window) @@ -1535,10 +1535,8 @@ Dired." (cons (list tag file) (cdr image-dired-tag-file-list)))) (setq image-dired-tag-file-list (list (list tag file)))))) -(defun image-dired-display-thumb-properties () - "Display thumbnail properties in the echo area." - (declare (obsolete image-dired-update-header-line "29.1")) - (image-dired-update-header-line)) +(define-obsolete-function-alias 'image-dired-display-thumb-properties + #'image-dired-update-header-line "29.1") (defvar image-dired-slideshow-count 0 "Keeping track on number of images in slideshow.") commit e50674833d9ba6eed2ec47ec2f0e867596713523 Author: Stefan Kangas Date: Tue Aug 30 19:05:29 2022 +0200 image-dired: Prefer defvar-keymap * lisp/image/image-dired-dired.el (image-dired-minor-mode-map): * lisp/image/image-dired.el (image-dired-thumbnail-mode-line-up-map) (image-dired-thumbnail-mode-tag-map) (image-dired-thumbnail-mode-map) (image-dired-display-image-mode-map): Prefer defvar-keymap. diff --git a/lisp/image/image-dired-dired.el b/lisp/image/image-dired-dired.el index d5d7e0c38f..aa9fa38751 100644 --- a/lisp/image/image-dired-dired.el +++ b/lisp/image/image-dired-dired.el @@ -191,30 +191,27 @@ With prefix argument, move ARG lines." (select-window window)) (message "Thumbnail buffer not visible")))) -(defvar image-dired-minor-mode-map - (let ((map (make-sparse-keymap))) - ;; (set-keymap-parent map dired-mode-map) - ;; Hijack previous and next line movement. Let C-p and C-b be - ;; though... - (define-key map "p" #'image-dired-dired-previous-line) - (define-key map "n" #'image-dired-dired-next-line) - (define-key map [up] #'image-dired-dired-previous-line) - (define-key map [down] #'image-dired-dired-next-line) - - (define-key map (kbd "C-S-n") #'image-dired-next-line-and-display) - (define-key map (kbd "C-S-p") #'image-dired-previous-line-and-display) - (define-key map (kbd "C-S-m") #'image-dired-mark-and-display-next) - - (define-key map "\C-td" #'image-dired-display-thumbs) - (define-key map [tab] #'image-dired-jump-thumbnail-buffer) - (define-key map "\C-ti" #'image-dired-dired-display-image) - (define-key map "\C-tx" #'image-dired-dired-display-external) - (define-key map "\C-ta" #'image-dired-display-thumbs-append) - (define-key map "\C-t." #'image-dired-display-thumb) - (define-key map "\C-tc" #'image-dired-dired-comment-files) - (define-key map "\C-tf" #'image-dired-mark-tagged-files) - map) - "Keymap for `image-dired-minor-mode'.") +(defvar-keymap image-dired-minor-mode-map + :doc "Keymap for `image-dired-minor-mode'." + ;; Hijack previous and next line movement. Let C-p and C-b be + ;; though... + "p" #'image-dired-dired-previous-line + "n" #'image-dired-dired-next-line + "" #'image-dired-dired-previous-line + "" #'image-dired-dired-next-line + + "C-S-n" #'image-dired-next-line-and-display + "C-S-p" #'image-dired-previous-line-and-display + "C-S-m" #'image-dired-mark-and-display-next + + "C-t d" #'image-dired-display-thumbs + "" #'image-dired-jump-thumbnail-buffer + "C-t i" #'image-dired-dired-display-image + "C-t x" #'image-dired-dired-display-external + "C-t a" #'image-dired-display-thumbs-append + "C-t ." #'image-dired-display-thumb + "C-t c" #'image-dired-dired-comment-files + "C-t f" #'image-dired-mark-tagged-files) (easy-menu-define image-dired-minor-mode-menu image-dired-minor-mode-map "Menu for `image-dired-minor-mode'." diff --git a/lisp/image/image-dired.el b/lisp/image/image-dired.el index 7c4de16172..87c2cd3f09 100644 --- a/lisp/image/image-dired.el +++ b/lisp/image/image-dired.el @@ -819,84 +819,64 @@ You probably want to use this together with (select-window window)) (message "Associated dired buffer not visible")))) -(defvar image-dired-thumbnail-mode-line-up-map - (let ((map (make-sparse-keymap))) - ;; map it to "g" so that the user can press it more quickly - (define-key map "g" #'image-dired-line-up-dynamic) - ;; "f" for "fixed" number of thumbs per row - (define-key map "f" #'image-dired-line-up) - ;; "i" for "interactive" - (define-key map "i" #'image-dired-line-up-interactive) - map) - "Keymap for line-up commands in `image-dired-thumbnail-mode'.") - -(defvar image-dired-thumbnail-mode-tag-map - (let ((map (make-sparse-keymap))) - ;; map it to "t" so that the user can press it more quickly - (define-key map "t" #'image-dired-tag-thumbnail) - ;; "r" for "remove" - (define-key map "r" #'image-dired-tag-thumbnail-remove) - map) - "Keymap for tag commands in `image-dired-thumbnail-mode'.") - -(defvar image-dired-thumbnail-mode-map - (let ((map (make-sparse-keymap))) - (define-key map [right] #'image-dired-forward-image) - (define-key map [left] #'image-dired-backward-image) - (define-key map [up] #'image-dired-previous-line) - (define-key map [down] #'image-dired-next-line) - (define-key map "\C-f" #'image-dired-forward-image) - (define-key map "\C-b" #'image-dired-backward-image) - (define-key map "\C-p" #'image-dired-previous-line) - (define-key map "\C-n" #'image-dired-next-line) - - (define-key map "<" #'image-dired-beginning-of-buffer) - (define-key map ">" #'image-dired-end-of-buffer) - (define-key map (kbd "M-<") #'image-dired-beginning-of-buffer) - (define-key map (kbd "M->") #'image-dired-end-of-buffer) - - (define-key map "d" #'image-dired-flag-thumb-original-file) - (define-key map [delete] #'image-dired-flag-thumb-original-file) - (define-key map "m" #'image-dired-mark-thumb-original-file) - (define-key map "u" #'image-dired-unmark-thumb-original-file) - (define-key map "U" #'image-dired-unmark-all-marks) - (define-key map "." #'image-dired-track-original-file) - (define-key map [tab] #'image-dired-jump-original-dired-buffer) - - ;; add line-up map - (define-key map "g" image-dired-thumbnail-mode-line-up-map) - ;; add tag map - (define-key map "t" image-dired-thumbnail-mode-tag-map) - - (define-key map "\C-m" #'image-dired-display-thumbnail-original-image) - (define-key map [C-return] #'image-dired-thumbnail-display-external) - - (define-key map "L" #'image-dired-rotate-original-left) - (define-key map "R" #'image-dired-rotate-original-right) - - (define-key map "D" #'image-dired-thumbnail-set-image-description) - (define-key map "S" #'image-dired-slideshow-start) - (define-key map "\C-d" #'image-dired-delete-char) - (define-key map " " #'image-dired-display-next-thumbnail-original) - (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) - (define-key map "c" #'image-dired-comment-thumbnail) - - ;; Mouse - (define-key map [mouse-2] #'image-dired-mouse-display-image) - (define-key map [mouse-1] #'image-dired-mouse-select-thumbnail) - (define-key map [mouse-3] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-1] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-2] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-3] #'image-dired-mouse-select-thumbnail) - ;; Seems I must first set C-down-mouse-1 to undefined, or else it - ;; will trigger the buffer menu. If I try to instead bind - ;; C-down-mouse-1 to `image-dired-mouse-toggle-mark', I get a message - ;; about C-mouse-1 not being defined afterwards. Annoying, but I - ;; probably do not completely understand mouse events. - (define-key map [C-down-mouse-1] #'undefined) - (define-key map [C-mouse-1] #'image-dired-mouse-toggle-mark) - map) - "Keymap for `image-dired-thumbnail-mode'.") +(defvar-keymap image-dired-thumbnail-mode-map + :doc "Keymap for `image-dired-thumbnail-mode'." + "" #'image-dired-forward-image + "" #'image-dired-backward-image + "" #'image-dired-previous-line + "" #'image-dired-next-line + "C-f" #'image-dired-forward-image + "C-b" #'image-dired-backward-image + "C-p" #'image-dired-previous-line + "C-n" #'image-dired-next-line + + "<" #'image-dired-beginning-of-buffer + ">" #'image-dired-end-of-buffer + "M-<" #'image-dired-beginning-of-buffer + "M->" #'image-dired-end-of-buffer + + "d" #'image-dired-flag-thumb-original-file + "" #'image-dired-flag-thumb-original-file + "m" #'image-dired-mark-thumb-original-file + "u" #'image-dired-unmark-thumb-original-file + "U" #'image-dired-unmark-all-marks + "." #'image-dired-track-original-file + "" #'image-dired-jump-original-dired-buffer + + "g g" #'image-dired-line-up-dynamic + "g f" #'image-dired-line-up + "g i" #'image-dired-line-up-interactive + + "t t" #'image-dired-tag-thumbnail + "t r" #'image-dired-tag-thumbnail-remove + + "RET" #'image-dired-display-thumbnail-original-image + "C-" #'image-dired-thumbnail-display-external + + "L" #'image-dired-rotate-original-left + "R" #'image-dired-rotate-original-right + + "D" #'image-dired-thumbnail-set-image-description + "S" #'image-dired-slideshow-start + "C-d" #'image-dired-delete-char + "SPC" #'image-dired-display-next-thumbnail-original + "DEL" #'image-dired-display-previous-thumbnail-original + "c" #'image-dired-comment-thumbnail + + ;; Mouse + "" #'image-dired-mouse-display-image + "" #'image-dired-mouse-select-thumbnail + "" #'image-dired-mouse-select-thumbnail + "" #'image-dired-mouse-select-thumbnail + "" #'image-dired-mouse-select-thumbnail + "" #'image-dired-mouse-select-thumbnail + ;; Seems I must first set C-down-mouse-1 to undefined, or else it + ;; will trigger the buffer menu. If I try to instead bind + ;; C-down-mouse-1 to `image-dired-mouse-toggle-mark', I get a message + ;; about C-mouse-1 not being defined afterwards. Annoying, but I + ;; probably do not completely understand mouse events. + "C-" #'undefined + "C-" #'image-dired-mouse-toggle-mark) (easy-menu-define image-dired-thumbnail-mode-menu image-dired-thumbnail-mode-map "Menu for `image-dired-thumbnail-mode'." @@ -930,21 +910,19 @@ You probably want to use this together with ["Refresh thumb" image-dired-refresh-thumb]) ["Quit" quit-window])) -(defvar image-dired-display-image-mode-map - (let ((map (make-sparse-keymap))) - (define-key map "S" #'image-dired-slideshow-start) - (define-key map (kbd "SPC") #'image-dired-display-next-thumbnail-original) - (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) - (define-key map "n" #'image-dired-display-next-thumbnail-original) - (define-key map "p" #'image-dired-display-previous-thumbnail-original) - (define-key map "m" #'image-dired-mark-thumb-original-file) - (define-key map "d" #'image-dired-flag-thumb-original-file) - (define-key map "u" #'image-dired-unmark-thumb-original-file) - (define-key map "U" #'image-dired-unmark-all-marks) - ;; Disable keybindings from `image-mode-map' that doesn't make sense here. - (define-key map "o" nil) ; image-save - map) - "Keymap for `image-dired-display-image-mode'.") +(defvar-keymap image-dired-display-image-mode-map + :doc "Keymap for `image-dired-display-image-mode'." + "S" #'image-dired-slideshow-start + "SPC" #'image-dired-display-next-thumbnail-original + "DEL" #'image-dired-display-previous-thumbnail-original + "n" #'image-dired-display-next-thumbnail-original + "p" #'image-dired-display-previous-thumbnail-original + "m" #'image-dired-mark-thumb-original-file + "d" #'image-dired-flag-thumb-original-file + "u" #'image-dired-unmark-thumb-original-file + "U" #'image-dired-unmark-all-marks + ;; Disable keybindings from `image-mode-map' that doesn't make sense here. + "o" nil) ; image-save (define-derived-mode image-dired-thumbnail-mode special-mode "image-dired-thumbnail" commit a7d716d1c5e36dbb3bd12b879c489035c87f4685 Author: Stefan Kangas Date: Mon Aug 29 19:52:48 2022 +0200 Add new defgroup image-dired-external * lisp/image/image-dired-external.el (image-dired-external): New defgroup. (image-dired-cmd-create-thumbnail-program) (image-dired-cmd-create-thumbnail-options) (image-dired-cmd-pngnq-program, image-dired-cmd-pngnq-options) (image-dired-cmd-pngcrush-program) (image-dired-cmd-pngcrush-options) (image-dired-cmd-optipng-program) (image-dired-cmd-optipng-options) (image-dired-cmd-create-standard-thumbnail-options) (image-dired-cmd-rotate-original-program) (image-dired-cmd-rotate-original-options) (image-dired-temp-rotate-image-file) (image-dired-rotate-original-ask-before-overwrite) (image-dired-cmd-write-exif-data-program) (image-dired-cmd-write-exif-data-options): Use above new defgroup. diff --git a/lisp/image/image-dired-external.el b/lisp/image/image-dired-external.el index ef39e2f938..893d3da843 100644 --- a/lisp/image/image-dired-external.el +++ b/lisp/image/image-dired-external.el @@ -38,13 +38,18 @@ (defvar image-dired-thumb-width) (defvar image-dired-thumbnail-storage) +(defgroup image-dired-external nil + "External process support for Image-Dired." + :prefix "image-dired-" + :link '(info-link "(emacs) Image-Dired") + :group 'image-dired) + (defcustom image-dired-cmd-create-thumbnail-program (if (executable-find "gm") "gm" "convert") "Executable used to create thumbnail. Used together with `image-dired-cmd-create-thumbnail-options'." :type 'file - :version "29.1" - :group 'image-dired) + :version "29.1") (defcustom image-dired-cmd-create-thumbnail-options (let ((opts '("-size" "%wx%h" "%f[0]" @@ -57,7 +62,6 @@ Available format specifiers are: %w which is replaced by `image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', %f which is replaced by the file name of the original image and %t which is replaced by the file name of the thumbnail file." - :group 'image-dired :version "29.1" :type '(repeat (string :tag "Argument"))) @@ -72,7 +76,6 @@ which is replaced by the file name of the thumbnail file." "The file name of the `pngquant' or `pngnq' program. It quantizes colors of PNG images down to 256 colors or fewer using the NeuQuant algorithm." - :group 'image-dired :version "29.1" :type '(choice (const :tag "Not Set" nil) file)) @@ -83,7 +86,6 @@ using the NeuQuant algorithm." "Arguments to pass `image-dired-cmd-pngnq-program'. Available format specifiers are the same as in `image-dired-cmd-create-thumbnail-options'." - :group 'image-dired :type '(repeat (string :tag "Argument")) :version "29.1") @@ -91,7 +93,6 @@ Available format specifiers are the same as in "The file name of the `pngcrush' program. It optimizes the compression of PNG images. Also it adds PNG textual chunks with the information required by the Thumbnail Managing Standard." - :group 'image-dired :type '(choice (const :tag "Not Set" nil) file)) (defcustom image-dired-cmd-pngcrush-options @@ -109,13 +110,11 @@ with the information required by the Thumbnail Managing Standard." Available format specifiers are the same as in `image-dired-cmd-create-thumbnail-options', with %q for a temporary file name (typically generated by pnqnq)." - :group 'image-dired :version "26.1" :type '(repeat (string :tag "Argument"))) (defcustom image-dired-cmd-optipng-program (executable-find "optipng") "The file name of the `optipng' program." - :group 'image-dired :version "26.1" :type '(choice (const :tag "Not Set" nil) file)) @@ -123,7 +122,6 @@ temporary file name (typically generated by pnqnq)." "Arguments passed to `image-dired-cmd-optipng-program'. Available format specifiers are described in `image-dired-cmd-create-thumbnail-options'." - :group 'image-dired :version "26.1" :type '(repeat (string :tag "Argument")) :link '(url-link "man:optipng(1)")) @@ -141,7 +139,6 @@ Available format specifiers are described in "Options for creating thumbnails according to the Thumbnail Managing Standard. Available format specifiers are the same as in `image-dired-cmd-create-thumbnail-options', with %m for file modification time." - :group 'image-dired :version "26.1" :type '(repeat (string :tag "Argument"))) @@ -149,7 +146,6 @@ Available format specifiers are the same as in "jpegtran" "Executable used to rotate original image. Used together with `image-dired-cmd-rotate-original-options'." - :group 'image-dired :type 'file) (defcustom image-dired-cmd-rotate-original-options @@ -161,7 +157,6 @@ number of (positive) degrees to rotate the image, normally 90 or 270 \(for 90 degrees right and left), %o which is replaced by the original image file name and %t which is replaced by `image-dired-temp-image-file'." - :group 'image-dired :version "26.1" :type '(repeat (string :tag "Argument"))) @@ -169,13 +164,11 @@ original image file name and %t which is replaced by (expand-file-name ".image-dired_rotate_temp" (locate-user-emacs-file "image-dired/")) "Temporary file for rotate operations." - :group 'image-dired :type 'file) (defcustom image-dired-cmd-write-exif-data-program "exiftool" "Program used to write EXIF data to image. Used together with `image-dired-cmd-write-exif-data-options'." - :group 'image-dired :type 'file) (defcustom image-dired-cmd-write-exif-data-options @@ -185,7 +178,6 @@ Used with `image-dired-cmd-write-exif-data-program'. Available format specifiers are: %f which is replaced by the image file name, %t which is replaced by the tag name and %v which is replaced by the tag value." - :group 'image-dired :version "26.1" :type '(repeat (string :tag "Argument"))) commit 9f82b49398178e480a4d2cc4f846340572f92886 Author: Stefan Kangas Date: Tue Aug 23 14:59:47 2022 +0200 ; image-dired: Indentation fixes * lisp/image/image-dired-dired.el: * lisp/image/image-dired-external.el: * lisp/image/image-dired-tags.el: * lisp/image/image-dired-util.el: * lisp/image/image-dired.el: Fix indentation. diff --git a/lisp/image/image-dired-dired.el b/lisp/image/image-dired-dired.el index 2f3a66f9cd..d5d7e0c38f 100644 --- a/lisp/image/image-dired-dired.el +++ b/lisp/image/image-dired-dired.el @@ -67,12 +67,12 @@ previous -ARG, if ARG<0) files." if (overlay-get ov 'thumb-file) return ov))) (if thumb-ov (delete-overlay thumb-ov) - (put-image thumb-file image-pos) - (setq overlay + (put-image thumb-file image-pos) + (setq overlay (cl-loop for ov in (overlays-in (point) (1+ (point))) if (overlay-get ov 'put-image) return ov)) - (overlay-put overlay 'image-file image-file) - (overlay-put overlay 'thumb-file thumb-file))))) + (overlay-put overlay 'image-file image-file) + (overlay-put overlay 'thumb-file thumb-file))))) arg ; Show or hide image on ARG next files. 'show-progress) ; Update dired display after each image is updated. (add-hook 'dired-after-readin-hook @@ -353,14 +353,14 @@ matching tag will be marked in the Dired buffer." ;; slow. Don't bother about hits found in other directories ;; than the current one. (when (string= (file-name-as-directory - (expand-file-name default-directory)) - (file-name-as-directory - (file-name-directory curr-file))) - (setq curr-file (file-name-nondirectory curr-file)) - (goto-char (point-min)) - (when (search-forward-regexp (format "\\s %s$" curr-file) nil t) - (setq hits (+ hits 1)) - (dired-mark 1)))) + (expand-file-name default-directory)) + (file-name-as-directory + (file-name-directory curr-file))) + (setq curr-file (file-name-nondirectory curr-file)) + (goto-char (point-min)) + (when (search-forward-regexp (format "\\s %s$" curr-file) nil t) + (setq hits (+ hits 1)) + (dired-mark 1)))) (message "%d files with matching tag marked" hits))) (defun image-dired-dired-display-properties () @@ -374,11 +374,11 @@ matching tag will be marked in the Dired buffer." (message-log-max nil)) (if file-name (message "%s" - (image-dired-format-properties-string - dired-buf - file-name - props - comment))))) + (image-dired-format-properties-string + dired-buf + file-name + props + comment))))) (provide 'image-dired-dired) diff --git a/lisp/image/image-dired-external.el b/lisp/image/image-dired-external.el index c45287bd18..ef39e2f938 100644 --- a/lisp/image/image-dired-external.el +++ b/lisp/image/image-dired-external.el @@ -297,9 +297,9 @@ and remove the cached thumbnail files between each trial run.") 'image-dired-cmd-create-thumbnail-program) (let* ((width (int-to-string (image-dired-thumb-size 'width))) (height (int-to-string (image-dired-thumb-size 'height))) - (modif-time (format-time-string - "%s" (file-attribute-modification-time - (file-attributes original-file)))) + (modif-time (format-time-string + "%s" (file-attribute-modification-time + (file-attributes original-file)))) (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png" thumbnail-file)) (spec @@ -338,7 +338,7 @@ and remove the cached thumbnail files between each trial run.") (image-dired-debug-message (format-time-string "Generated thumbnails in %s.%3N seconds" - (time-subtract nil + (time-subtract nil image-dired--generate-thumbs-start)))) (if (not (and (eq (process-status process) 'exit) (zerop (process-exit-status process)))) @@ -410,7 +410,7 @@ The new file will be named THUMBNAIL-FILE." (image-dired-display-image image-dired-temp-rotate-image-file) (if (or (and image-dired-rotate-original-ask-before-overwrite (y-or-n-p - "Rotate to temp file OK. Overwrite original image? ")) + "Rotate to temp file OK. Overwrite original image? ")) (not image-dired-rotate-original-ask-before-overwrite)) (progn (copy-file image-dired-temp-rotate-image-file file t) @@ -455,8 +455,8 @@ default value at the prompt." (old-value (or (exif-field 'description (exif-parse-file file)) ""))) (if (eq 0 (image-dired-set-exif-data file "ImageDescription" - (read-string "Value of ImageDescription: " - old-value))) + (read-string "Value of ImageDescription: " + old-value))) (message "Successfully wrote ImageDescription tag") (error "Could not write ImageDescription tag"))))) diff --git a/lisp/image/image-dired-tags.el b/lisp/image/image-dired-tags.el index 2ea9d9f5eb..ee3c63b009 100644 --- a/lisp/image/image-dired-tags.el +++ b/lisp/image/image-dired-tags.el @@ -40,7 +40,7 @@ Return the last form in BODY." (declare (indent 0) (debug t)) `(with-temp-buffer (if (file-exists-p image-dired-db-file) - (insert-file-contents image-dired-db-file)) + (insert-file-contents image-dired-db-file)) ,@body)) (defun image-dired-sane-db-file () @@ -71,65 +71,65 @@ FILE-TAGS is an alist in the following form: (image-dired-sane-db-file) (let (end file tag) (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (dolist (elt file-tags) - (setq file (car elt) - tag (cdr elt)) - (goto-char (point-min)) - (if (search-forward-regexp (format "^%s.*$" file) nil t) - (progn - (setq end (point)) - (beginning-of-line) - (when (not (search-forward (format ";%s" tag) end t)) - (end-of-line) - (insert (format ";%s" tag)))) - (goto-char (point-max)) - (insert (format "%s;%s\n" file tag)))) - (save-buffer)))) + (setq buffer-file-name image-dired-db-file) + (dolist (elt file-tags) + (setq file (car elt) + tag (cdr elt)) + (goto-char (point-min)) + (if (search-forward-regexp (format "^%s.*$" file) nil t) + (progn + (setq end (point)) + (beginning-of-line) + (when (not (search-forward (format ";%s" tag) end t)) + (end-of-line) + (insert (format ";%s" tag)))) + (goto-char (point-max)) + (insert (format "%s;%s\n" file tag)))) + (save-buffer)))) (defun image-dired-remove-tag (files tag) "For all FILES, remove TAG from the image database." (image-dired-sane-db-file) (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (let (end) - (unless (listp files) - (if (stringp files) - (setq files (list files)) - (error "Files must be a string or a list of strings!"))) - (dolist (file files) - (goto-char (point-min)) - (when (search-forward-regexp (format "^%s;" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (search-forward-regexp - (format "\\(;%s\\)\\($\\|;\\)" tag) end t) - (delete-region (match-beginning 1) (match-end 1)) - ;; Check if file should still be in the database. If - ;; it has no tags or comments, it will be removed. - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (not (search-forward ";" end t)) - (kill-line 1)))))) - (save-buffer))) + (setq buffer-file-name image-dired-db-file) + (let (end) + (unless (listp files) + (if (stringp files) + (setq files (list files)) + (error "Files must be a string or a list of strings!"))) + (dolist (file files) + (goto-char (point-min)) + (when (search-forward-regexp (format "^%s;" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (search-forward-regexp + (format "\\(;%s\\)\\($\\|;\\)" tag) end t) + (delete-region (match-beginning 1) (match-end 1)) + ;; Check if file should still be in the database. If + ;; it has no tags or comments, it will be removed. + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (not (search-forward ";" end t)) + (kill-line 1)))))) + (save-buffer))) (defun image-dired-list-tags (file) "Read all tags for image FILE from the image database." (image-dired-sane-db-file) (image-dired--with-db-file - (let (end (tags "")) - (when (search-forward-regexp (format "^%s" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (if (search-forward ";" end t) - (if (search-forward "comment:" end t) - (if (search-forward ";" end t) - (setq tags (buffer-substring (point) end))) - (setq tags (buffer-substring (point) end))))) - (split-string tags ";")))) + (let (end (tags "")) + (when (search-forward-regexp (format "^%s" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (if (search-forward ";" end t) + (if (search-forward "comment:" end t) + (if (search-forward ";" end t) + (setq tags (buffer-substring (point) end))) + (setq tags (buffer-substring (point) end))))) + (split-string tags ";")))) ;;;###autoload (defun image-dired-tag-files (arg) @@ -191,34 +191,34 @@ FILE-COMMENTS is an alist on the following form: (image-dired-sane-db-file) (let (end comment-beg-pos comment-end-pos file comment) (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (dolist (elt file-comments) - (setq file (car elt) - comment (cdr elt)) - (goto-char (point-min)) - (if (search-forward-regexp (format "^%s.*$" file) nil t) - (progn - (setq end (point)) - (beginning-of-line) - ;; Delete old comment, if any - (when (search-forward ";comment:" end t) - (setq comment-beg-pos (match-beginning 0)) - ;; Any tags after the comment? - (if (search-forward ";" end t) - (setq comment-end-pos (- (point) 1)) - (setq comment-end-pos end)) - ;; Delete comment tag and comment - (delete-region comment-beg-pos comment-end-pos)) - ;; Insert new comment - (beginning-of-line) - (unless (search-forward ";" end t) - (end-of-line) - (insert ";")) - (insert (format "comment:%s;" comment))) - ;; File does not exist in database - add it. - (goto-char (point-max)) - (insert (format "%s;comment:%s\n" file comment)))) - (save-buffer)))) + (setq buffer-file-name image-dired-db-file) + (dolist (elt file-comments) + (setq file (car elt) + comment (cdr elt)) + (goto-char (point-min)) + (if (search-forward-regexp (format "^%s.*$" file) nil t) + (progn + (setq end (point)) + (beginning-of-line) + ;; Delete old comment, if any + (when (search-forward ";comment:" end t) + (setq comment-beg-pos (match-beginning 0)) + ;; Any tags after the comment? + (if (search-forward ";" end t) + (setq comment-end-pos (- (point) 1)) + (setq comment-end-pos end)) + ;; Delete comment tag and comment + (delete-region comment-beg-pos comment-end-pos)) + ;; Insert new comment + (beginning-of-line) + (unless (search-forward ";" end t) + (end-of-line) + (insert ";")) + (insert (format "comment:%s;" comment))) + ;; File does not exist in database - add it. + (goto-char (point-max)) + (insert (format "%s;comment:%s\n" file comment)))) + (save-buffer)))) (defun image-dired-update-property (prop value) "Update text property PROP with value VALUE at point." @@ -252,19 +252,19 @@ Optionally use old comment from FILE as initial value." "Get comment for file FILE." (image-dired-sane-db-file) (image-dired--with-db-file - (let (end comment-beg-pos comment-end-pos comment) - (when (search-forward-regexp (format "^%s" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (search-forward ";comment:" end t) - (setq comment-beg-pos (point)) - (if (search-forward ";" end t) - (setq comment-end-pos (- (point) 1)) - (setq comment-end-pos end)) - (setq comment (buffer-substring - comment-beg-pos comment-end-pos)))) - comment))) + (let (end comment-beg-pos comment-end-pos comment) + (when (search-forward-regexp (format "^%s" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (search-forward ";comment:" end t) + (setq comment-beg-pos (point)) + (if (search-forward ";" end t) + (setq comment-end-pos (- (point) 1)) + (setq comment-end-pos end)) + (setq comment (buffer-substring + comment-beg-pos comment-end-pos)))) + comment))) ;;; Tag support @@ -290,7 +290,7 @@ easy-to-use form." (remove-overlays) ;; Some help for the user. (widget-insert -"\nEdit comments and tags for each image. Separate multiple tags + "\nEdit comments and tags for each image. Separate multiple tags with a comma. Move forward between fields using TAB or RET. Move to the previous field using backtab (S-TAB). Save by activating the Save button at the bottom of the form or cancel @@ -301,41 +301,41 @@ the operation by activating the Cancel button.\n\n") (dolist (file files) - (setq thumb-file (image-dired-thumb-name file) - img (create-image thumb-file)) - - (insert-image img) - (widget-insert "\n\nComment: ") - (setq comment-widget - (widget-create 'editable-field - :size 60 - :format "%v " - :value (or (image-dired-get-comment file) ""))) - (widget-insert "\nTags: ") - (setq tag-widget - (widget-create 'editable-field - :size 60 - :format "%v " - :value (or (mapconcat - #'identity - (image-dired-list-tags file) - ",") ""))) - ;; Save information in all widgets so that we can use it when - ;; the user saves the form. - (setq image-dired-widget-list - (append image-dired-widget-list - (list (list file comment-widget tag-widget)))) - (widget-insert "\n\n"))) + (setq thumb-file (image-dired-thumb-name file) + img (create-image thumb-file)) + + (insert-image img) + (widget-insert "\n\nComment: ") + (setq comment-widget + (widget-create 'editable-field + :size 60 + :format "%v " + :value (or (image-dired-get-comment file) ""))) + (widget-insert "\nTags: ") + (setq tag-widget + (widget-create 'editable-field + :size 60 + :format "%v " + :value (or (mapconcat + #'identity + (image-dired-list-tags file) + ",") ""))) + ;; Save information in all widgets so that we can use it when + ;; the user saves the form. + (setq image-dired-widget-list + (append image-dired-widget-list + (list (list file comment-widget tag-widget)))) + (widget-insert "\n\n"))) ;; Footer with Save and Cancel button. (widget-insert "\n") (widget-create 'push-button - :notify - (lambda (&rest _ignore) - (image-dired-save-information-from-widgets) - (bury-buffer) - (message "Done")) - "Save") + :notify + (lambda (&rest _ignore) + (image-dired-save-information-from-widgets) + (bury-buffer) + (message "Done")) + "Save") (widget-insert " ") (widget-create 'push-button :notify @@ -356,12 +356,12 @@ tags to their respective image file. Internal function used by `image-dired-dired-edit-comment-and-tags'." (let (file comment tag-string tag-list lst) (image-dired-write-comments - (mapcar - (lambda (widget) - (setq file (car widget) - comment (widget-value (cadr widget))) - (cons file comment)) - image-dired-widget-list)) + (mapcar + (lambda (widget) + (setq file (car widget) + comment (widget-value (cadr widget))) + (cons file comment)) + image-dired-widget-list)) (image-dired-write-tags (dolist (widget image-dired-widget-list lst) (setq file (car widget) diff --git a/lisp/image/image-dired-util.el b/lisp/image/image-dired-util.el index 6380dbb40a..1318b25a12 100644 --- a/lisp/image/image-dired-util.el +++ b/lisp/image/image-dired-util.el @@ -45,7 +45,7 @@ "Return the current thumbnail directory (from variable `image-dired-dir'). Create the thumbnail directory if it does not exist." (let ((image-dired-dir (file-name-as-directory - (expand-file-name image-dired-dir)))) + (expand-file-name image-dired-dir)))) (unless (file-directory-p image-dired-dir) (with-file-modes #o700 (make-directory image-dired-dir t)) diff --git a/lisp/image/image-dired.el b/lisp/image/image-dired.el index c4b2756ebf..7c4de16172 100644 --- a/lisp/image/image-dired.el +++ b/lisp/image/image-dired.el @@ -281,8 +281,8 @@ the window containing the thumbnail buffer, Fixed means to use and No line-up means that no automatic line-up will be done." :type '(choice :tag "Default line-up method" (const :tag "Dynamic" dynamic) - (const :tag "Fixed" fixed) - (const :tag "Interactive" interactive) + (const :tag "Fixed" fixed) + (const :tag "Interactive" interactive) (const :tag "No line-up" none))) (defcustom image-dired-thumbs-per-row 3 @@ -361,16 +361,16 @@ This affects the following commands: (unless (string-match-p (image-file-name-regexp) file) (error "%s is not a valid image file" file)) (let* ((thumb-file (image-dired-thumb-name file)) - (thumb-attr (file-attributes thumb-file))) + (thumb-attr (file-attributes thumb-file))) (when (or (not thumb-attr) - (time-less-p (file-attribute-modification-time thumb-attr) - (file-attribute-modification-time - (file-attributes file)))) + (time-less-p (file-attribute-modification-time thumb-attr) + (file-attribute-modification-time + (file-attributes file)))) (image-dired-create-thumb file thumb-file)) (create-image thumb-file))) -(defun image-dired-insert-thumbnail (file original-file-name - associated-dired-buffer) +(defun image-dired-insert-thumbnail ( file original-file-name + associated-dired-buffer) "Insert thumbnail image FILE. Add text properties ORIGINAL-FILE-NAME and ASSOCIATED-DIRED-BUFFER." (let (beg end) @@ -1066,7 +1066,7 @@ See also `image-dired-line-up-dynamic'." (insert (propertize " " 'display `(space :align-to ,thumb-prev-pos))) (cl-incf seen) (when (and (= seen (- image-dired-thumbs-per-row 1)) - (not (eobp))) + (not (eobp))) (forward-char) (insert "\n") (setq seen 0) @@ -1200,7 +1200,7 @@ non-nil." (let ((file (image-dired-original-file-name))) (when file (if image-dired-track-movement - (image-dired-track-original-file)) + (image-dired-track-original-file)) (image-dired-display-image file)))) (defun image-dired-mouse-select-thumbnail (event) @@ -1403,7 +1403,7 @@ completely fit)." "Calculate WINDOW height in pixels." (declare (obsolete nil "29.1")) ;; Note: The mode-line consumes one line - (* (- (window-height window) 1) (frame-char-height))) + (* (- (window-height window) 1) (frame-char-height))) (defcustom image-dired-cmd-read-exif-data-program "exiftool" "Program used to read EXIF data to image. @@ -1507,7 +1507,7 @@ Dired." (dired-buf (image-dired-associated-dired-buffer))) (if (not (and dired-buf file-name)) (message "No image, or image with correct properties, at point") - (with-current-buffer dired-buf + (with-current-buffer dired-buf (message "%s" file-name) (when (dired-goto-file file-name) (cond ((eq command 'mark) (dired-mark 1)) commit 64b208aa6c47fd2de35404a82b8131c9bc6b9865 Author: Stefan Kangas Date: Tue Aug 23 14:46:03 2022 +0200 image-dired: Make HTML gallery generation obsolete * lisp/image/image-dired.el (image-dired-gallery): Delete defgroup again. (image-dired-gallery-dir, image-dired-gallery-image-root-url) (image-dired-gallery-thumb-image-root-url) (image-dired-gallery-hidden-tags, image-dired-tag-file-list) (image-dired-file-tag-list, image-dired--add-to-tag-file-lists) (image-dired--add-to-file-comment-list) (image-dired--create-gallery-lists, image-dired--hidden-p) (image-dired-gallery-generate): Make obsolete. diff --git a/etc/NEWS b/etc/NEWS index 255d92414f..ef9bfd08e3 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2037,6 +2037,13 @@ Instead, use commands 'image-dired-refresh-thumb' to generate a new thumbnail, or 'image-rotate' to rotate the thumbnail without updating the thumbnail file. +--- +*** HTML image gallery generation is now obsolete. +The 'image-dired-gallery-generate' command and these user options are +now obsolete: 'image-dired-gallery-thumb-image-root-url', +'image-dired-gallery-hidden-tags', 'image-dired-gallery-dir', +'image-dired-gallery-image-root-url'. + ** Dired --- diff --git a/lisp/image/image-dired.el b/lisp/image/image-dired.el index 04bfa6a7ba..c4b2756ebf 100644 --- a/lisp/image/image-dired.el +++ b/lisp/image/image-dired.el @@ -1297,240 +1297,6 @@ Track this in associated Dired buffer if (image-dired-mouse-toggle-mark-1)) (image-dired-thumb-update-marks)) - -;;; Gallery support - -;; TODO: -;; * Support gallery creation when using per-directory thumbnail -;; storage. -;; * Enhanced gallery creation with basic CSS-support and pagination -;; of tag pages with many pictures. - -(defgroup image-dired-gallery nil - "Image-Dired support for generating a HTML gallery." - :prefix "image-dired-" - :group 'image-dired - :version "29.1") - -(defcustom image-dired-gallery-dir - (expand-file-name ".image-dired_gallery" image-dired-dir) - "Directory to store generated gallery html pages. -The name of this directory needs to be \"shared\" to the public -so that it can access the index.html page that image-dired creates." - :type 'directory) - -(defcustom image-dired-gallery-image-root-url - "https://example.org/image-diredpics" - "URL where the full size images are to be found on your web server. -Note that this URL has to be configured on your web server. -Image-Dired expects to find pictures in this directory. -This is used by `image-dired-gallery-generate'." - :type 'string - :version "29.1") - -(defcustom image-dired-gallery-thumb-image-root-url - "https://example.org/image-diredthumbs" - "URL where the thumbnail images are to be found on your web server. -Note that URL path has to be configured on your web server. -Image-Dired expects to find pictures in this directory. -This is used by `image-dired-gallery-generate'." - :type 'string - :version "29.1") - -(defcustom image-dired-gallery-hidden-tags - (list "private" "hidden" "pending") - "List of \"hidden\" tags. -Used by `image-dired-gallery-generate' to leave out \"hidden\" images." - :type '(repeat string)) - -(defvar image-dired-tag-file-list nil - "List to store tag-file structure.") - -(defvar image-dired-file-tag-list nil - "List to store file-tag structure.") - -(defvar image-dired-file-comment-list nil - "List to store file comments.") - -(defun image-dired--add-to-tag-file-lists (tag file) - "Helper function used from `image-dired--create-gallery-lists'. - -Add TAG to FILE in one list and FILE to TAG in the other. - -Lisp structures look like the following: - -image-dired-file-tag-list: - - ((\"filename1\" \"tag1\" \"tag2\" \"tag3\" ...) - (\"filename2\" \"tag1\" \"tag2\" \"tag3\" ...) - ...) - -image-dired-tag-file-list: - - ((\"tag1\" \"filename1\" \"filename2\" \"filename3\" ...) - (\"tag2\" \"filename1\" \"filename2\" \"filename3\" ...) - ...)" - ;; Add tag to file list - (let (curr) - (if image-dired-file-tag-list - (if (setq curr (assoc file image-dired-file-tag-list)) - (setcdr curr (cons tag (cdr curr))) - (setcdr image-dired-file-tag-list - (cons (list file tag) (cdr image-dired-file-tag-list)))) - (setq image-dired-file-tag-list (list (list file tag)))) - ;; Add file to tag list - (if image-dired-tag-file-list - (if (setq curr (assoc tag image-dired-tag-file-list)) - (if (not (member file curr)) - (setcdr curr (cons file (cdr curr)))) - (setcdr image-dired-tag-file-list - (cons (list tag file) (cdr image-dired-tag-file-list)))) - (setq image-dired-tag-file-list (list (list tag file)))))) - -(defun image-dired--add-to-file-comment-list (file comment) - "Helper function used from `image-dired--create-gallery-lists'. - -For FILE, add COMMENT to list. - -Lisp structure looks like the following: - -image-dired-file-comment-list: - - ((\"filename1\" . \"comment1\") - (\"filename2\" . \"comment2\") - ...)" - (if image-dired-file-comment-list - (if (not (assoc file image-dired-file-comment-list)) - (setcdr image-dired-file-comment-list - (cons (cons file comment) - (cdr image-dired-file-comment-list)))) - (setq image-dired-file-comment-list (list (cons file comment))))) - -(defun image-dired--create-gallery-lists () - "Create temporary lists used by `image-dired-gallery-generate'." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end beg file row-tags) - (setq image-dired-tag-file-list nil) - (setq image-dired-file-tag-list nil) - (setq image-dired-file-comment-list nil) - (goto-char (point-min)) - (while (search-forward-regexp "^." nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (setq beg (point)) - (unless (search-forward ";" end nil) - (error "Something is really wrong, check format of database")) - (setq row-tags (split-string - (buffer-substring beg end) ";")) - (setq file (car row-tags)) - (dolist (x (cdr row-tags)) - (if (not (string-match "^comment:\\(.*\\)" x)) - (image-dired--add-to-tag-file-lists x file) - (image-dired--add-to-file-comment-list file (match-string 1 x))))))) - ;; Sort tag-file list - (setq image-dired-tag-file-list - (sort image-dired-tag-file-list - (lambda (x y) - (string< (car x) (car y)))))) - -(defun image-dired--hidden-p (file) - "Return t if image FILE has a \"hidden\" tag." - (cl-loop for tag in (cdr (assoc file image-dired-file-tag-list)) - if (member tag image-dired-gallery-hidden-tags) return t)) - -(defun image-dired-gallery-generate () - "Generate gallery pages. -First we create a couple of Lisp structures from the database to make -it easier to generate, then HTML-files are created in -`image-dired-gallery-dir'." - (interactive) - (if (eq 'per-directory image-dired-thumbnail-storage) - (error "Currently, gallery generation is not supported \ -when using per-directory thumbnail file storage")) - (image-dired--create-gallery-lists) - (let ((tags image-dired-tag-file-list) - (index-file (format "%s/index.html" image-dired-gallery-dir)) - count tag tag-file - comment file-tags tag-link tag-link-list) - ;; Make sure gallery root exist - (if (file-exists-p image-dired-gallery-dir) - (if (not (file-directory-p image-dired-gallery-dir)) - (error "Variable image-dired-gallery-dir is not a directory")) - ;; FIXME: Should we set umask to 077 here, as we do for thumbnails? - (make-directory image-dired-gallery-dir)) - ;; Open index file - (with-temp-file index-file - (if (file-exists-p index-file) - (insert-file-contents index-file)) - (insert "\n") - (insert " \n") - (insert "

Image-Dired Gallery

\n") - (insert (format "

\n Gallery generated %s\n

\n" - (current-time-string))) - (insert "

Tag index

\n") - (setq count 1) - ;; Pre-generate list of all tag links - (dolist (curr tags) - (setq tag (car curr)) - (when (not (member tag image-dired-gallery-hidden-tags)) - (setq tag-link (format "%s" count tag)) - (if tag-link-list - (setq tag-link-list - (append tag-link-list (list (cons tag tag-link)))) - (setq tag-link-list (list (cons tag tag-link)))) - (setq count (1+ count)))) - (setq count 1) - ;; Main loop where we generated thumbnail pages per tag - (dolist (curr tags) - (setq tag (car curr)) - ;; Don't display hidden tags - (when (not (member tag image-dired-gallery-hidden-tags)) - ;; Insert link to tag page in index - (insert (format " %s
\n" (cdr (assoc tag tag-link-list)))) - ;; Open per-tag file - (setq tag-file (format "%s/%s.html" image-dired-gallery-dir count)) - (with-temp-file tag-file - (if (file-exists-p tag-file) - (insert-file-contents tag-file)) - (erase-buffer) - (insert "\n") - (insert " \n") - (insert "

Index

\n") - (insert (format "

Images with tag "%s"

" tag)) - ;; Main loop for files per tag page - (dolist (file (cdr curr)) - (unless (image-dired-hidden-p file) - ;; Insert thumbnail with link to full image - (insert - (format "\n" - image-dired-gallery-image-root-url - (file-name-nondirectory file) - image-dired-gallery-thumb-image-root-url - (file-name-nondirectory (image-dired-thumb-name file)) file)) - ;; Insert comment, if any - (if (setq comment (cdr (assoc file image-dired-file-comment-list))) - (insert (format "
\n%s
\n" comment)) - (insert "
\n")) - ;; Insert links to other tags, if any - (when (> (length - (setq file-tags (assoc file image-dired-file-tag-list))) 2) - (insert "[ ") - (dolist (extra-tag file-tags) - ;; Only insert if not file name or the main tag - (if (and (not (equal extra-tag tag)) - (not (equal extra-tag file))) - (insert - (format "%s " (cdr (assoc extra-tag tag-link-list)))))) - (insert "]
\n")))) - (insert "

Index

\n") - (insert " \n") - (insert "\n")) - (setq count (1+ count)))) - (insert " \n") - (insert "")))) - ;;; bookmark.el support @@ -1775,6 +1541,10 @@ Dired." (image-dired-display-image file)) (error "No original file name at point")))) +(make-obsolete-variable 'image-dired-tag-file-list nil "29.1") +(defvar image-dired-tag-file-list nil + "List to store tag-file structure.") + (defun image-dired-add-to-tag-file-list (tag file) "Add relation between TAG and FILE." (declare (obsolete nil "29.1")) @@ -1800,16 +1570,247 @@ Dired." "Number of pictures to display in slideshow.") (make-obsolete-variable 'image-dired-slideshow-times "no longer used." "29.1") +(make-obsolete-variable 'image-dired-gallery-dir nil "29.1") +(defcustom image-dired-gallery-dir + (expand-file-name ".image-dired_gallery" image-dired-dir) + "Directory to store generated gallery html pages. +The name of this directory needs to be \"shared\" to the public +so that it can access the index.html page that image-dired creates." + :type 'directory) + +(make-obsolete-variable 'image-dired-gallery-image-root-url nil "29.1") +(defcustom image-dired-gallery-image-root-url + "https://example.org/image-diredpics" + "URL where the full size images are to be found on your web server. +Note that this URL has to be configured on your web server. +Image-Dired expects to find pictures in this directory. +This is used by `image-dired-gallery-generate'." + :type 'string + :version "29.1") + +(make-obsolete-variable 'image-dired-gallery-thumb-image-root-url nil "29.1") +(defcustom image-dired-gallery-thumb-image-root-url + "https://example.org/image-diredthumbs" + "URL where the thumbnail images are to be found on your web server. +Note that URL path has to be configured on your web server. +Image-Dired expects to find pictures in this directory. +This is used by `image-dired-gallery-generate'." + :type 'string + :version "29.1") + +(make-obsolete-variable 'image-dired-gallery-hidden-tags nil "29.1") +(defcustom image-dired-gallery-hidden-tags + (list "private" "hidden" "pending") + "List of \"hidden\" tags. +Used by `image-dired-gallery-generate' to leave out \"hidden\" images." + :type '(repeat string)) + +(make-obsolete-variable 'image-dired-file-tag-list nil "29.1") +(defvar image-dired-file-tag-list nil + "List to store file-tag structure.") + +(make-obsolete-variable 'image-dired-file-comment-list nil "29.1") +(defvar image-dired-file-comment-list nil + "List to store file comments.") + +(defun image-dired--add-to-tag-file-lists (tag file) + "Helper function used from `image-dired--create-gallery-lists'. + +Add TAG to FILE in one list and FILE to TAG in the other. + +Lisp structures look like the following: + +image-dired-file-tag-list: + + ((\"filename1\" \"tag1\" \"tag2\" \"tag3\" ...) + (\"filename2\" \"tag1\" \"tag2\" \"tag3\" ...) + ...) + +image-dired-tag-file-list: + + ((\"tag1\" \"filename1\" \"filename2\" \"filename3\" ...) + (\"tag2\" \"filename1\" \"filename2\" \"filename3\" ...) + ...)" + (declare (obsolete nil "29.1")) + ;; Add tag to file list + (let (curr) + (if image-dired-file-tag-list + (if (setq curr (assoc file image-dired-file-tag-list)) + (setcdr curr (cons tag (cdr curr))) + (setcdr image-dired-file-tag-list + (cons (list file tag) (cdr image-dired-file-tag-list)))) + (setq image-dired-file-tag-list (list (list file tag)))) + ;; Add file to tag list + (if image-dired-tag-file-list + (if (setq curr (assoc tag image-dired-tag-file-list)) + (if (not (member file curr)) + (setcdr curr (cons file (cdr curr)))) + (setcdr image-dired-tag-file-list + (cons (list tag file) (cdr image-dired-tag-file-list)))) + (setq image-dired-tag-file-list (list (list tag file)))))) + +(defun image-dired--add-to-file-comment-list (file comment) + "Helper function used from `image-dired--create-gallery-lists'. + +For FILE, add COMMENT to list. + +Lisp structure looks like the following: + +image-dired-file-comment-list: + + ((\"filename1\" . \"comment1\") + (\"filename2\" . \"comment2\") + ...)" + (declare (obsolete nil "29.1")) + (if image-dired-file-comment-list + (if (not (assoc file image-dired-file-comment-list)) + (setcdr image-dired-file-comment-list + (cons (cons file comment) + (cdr image-dired-file-comment-list)))) + (setq image-dired-file-comment-list (list (cons file comment))))) + +(defun image-dired--create-gallery-lists () + "Create temporary lists used by `image-dired-gallery-generate'." + (declare (obsolete nil "29.1")) + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end beg file row-tags) + (setq image-dired-tag-file-list nil) + (setq image-dired-file-tag-list nil) + (setq image-dired-file-comment-list nil) + (goto-char (point-min)) + (while (search-forward-regexp "^." nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (setq beg (point)) + (unless (search-forward ";" end nil) + (error "Something is really wrong, check format of database")) + (setq row-tags (split-string + (buffer-substring beg end) ";")) + (setq file (car row-tags)) + (dolist (x (cdr row-tags)) + (with-suppressed-warnings + ((obsolete image-dired--add-to-tag-file-lists + image-dired--add-to-file-comment-list)) + (if (not (string-match "^comment:\\(.*\\)" x)) + (image-dired--add-to-tag-file-lists x file) + (image-dired--add-to-file-comment-list file (match-string 1 x)))))))) + ;; Sort tag-file list + (setq image-dired-tag-file-list + (sort image-dired-tag-file-list + (lambda (x y) + (string< (car x) (car y)))))) + +(defun image-dired--hidden-p (file) + "Return t if image FILE has a \"hidden\" tag." + (declare (obsolete nil "29.1")) + (cl-loop for tag in (cdr (assoc file image-dired-file-tag-list)) + if (member tag image-dired-gallery-hidden-tags) return t)) + +(defun image-dired-gallery-generate () + "Generate gallery pages. +First we create a couple of Lisp structures from the database to make +it easier to generate, then HTML-files are created in +`image-dired-gallery-dir'." + (declare (obsolete nil "29.1")) + (interactive) + (if (eq 'per-directory image-dired-thumbnail-storage) + (error "Currently, gallery generation is not supported \ +when using per-directory thumbnail file storage")) + (with-suppressed-warnings ((obsolete image-dired--create-gallery-lists)) + (image-dired--create-gallery-lists)) + (let ((tags image-dired-tag-file-list) + (index-file (format "%s/index.html" image-dired-gallery-dir)) + count tag tag-file + comment file-tags tag-link tag-link-list) + ;; Make sure gallery root exist + (if (file-exists-p image-dired-gallery-dir) + (if (not (file-directory-p image-dired-gallery-dir)) + (error "Variable image-dired-gallery-dir is not a directory")) + ;; FIXME: Should we set umask to 077 here, as we do for thumbnails? + (make-directory image-dired-gallery-dir)) + ;; Open index file + (with-temp-file index-file + (if (file-exists-p index-file) + (insert-file-contents index-file)) + (insert "\n") + (insert " \n") + (insert "

Image-Dired Gallery

\n") + (insert (format "

\n Gallery generated %s\n

\n" + (current-time-string))) + (insert "

Tag index

\n") + (setq count 1) + ;; Pre-generate list of all tag links + (dolist (curr tags) + (setq tag (car curr)) + (when (not (member tag image-dired-gallery-hidden-tags)) + (setq tag-link (format "%s" count tag)) + (if tag-link-list + (setq tag-link-list + (append tag-link-list (list (cons tag tag-link)))) + (setq tag-link-list (list (cons tag tag-link)))) + (setq count (1+ count)))) + (setq count 1) + ;; Main loop where we generated thumbnail pages per tag + (dolist (curr tags) + (setq tag (car curr)) + ;; Don't display hidden tags + (when (not (member tag image-dired-gallery-hidden-tags)) + ;; Insert link to tag page in index + (insert (format " %s
\n" (cdr (assoc tag tag-link-list)))) + ;; Open per-tag file + (setq tag-file (format "%s/%s.html" image-dired-gallery-dir count)) + (with-temp-file tag-file + (if (file-exists-p tag-file) + (insert-file-contents tag-file)) + (erase-buffer) + (insert "\n") + (insert " \n") + (insert "

Index

\n") + (insert (format "

Images with tag "%s"

" tag)) + ;; Main loop for files per tag page + (dolist (file (cdr curr)) + (unless (image-dired-hidden-p file) + ;; Insert thumbnail with link to full image + (insert + (format "\n" + image-dired-gallery-image-root-url + (file-name-nondirectory file) + image-dired-gallery-thumb-image-root-url + (file-name-nondirectory (image-dired-thumb-name file)) file)) + ;; Insert comment, if any + (if (setq comment (cdr (assoc file image-dired-file-comment-list))) + (insert (format "
\n%s
\n" comment)) + (insert "
\n")) + ;; Insert links to other tags, if any + (when (> (length + (setq file-tags (assoc file image-dired-file-tag-list))) 2) + (insert "[ ") + (dolist (extra-tag file-tags) + ;; Only insert if not file name or the main tag + (if (and (not (equal extra-tag tag)) + (not (equal extra-tag file))) + (insert + (format "%s " (cdr (assoc extra-tag tag-link-list)))))) + (insert "]
\n")))) + (insert "

Index

\n") + (insert " \n") + (insert "\n")) + (setq count (1+ count)))) + (insert " \n") + (insert "")))) + (define-obsolete-function-alias 'image-dired-create-display-image-buffer #'ignore "29.1") (define-obsolete-function-alias 'image-dired-create-gallery-lists - #'image-dired--create-gallery-lists "29.1") + 'image-dired--create-gallery-lists "29.1") (define-obsolete-function-alias 'image-dired-add-to-file-comment-list - #'image-dired--add-to-file-comment-list "29.1") + 'image-dired--add-to-file-comment-list "29.1") (define-obsolete-function-alias 'image-dired-add-to-tag-file-lists - #'image-dired--add-to-tag-file-lists "29.1") + 'image-dired--add-to-tag-file-lists "29.1") (define-obsolete-function-alias 'image-dired-hidden-p - #'image-dired--hidden-p "29.1") + 'image-dired--hidden-p "29.1") ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;; TEST-SECTION ;;;;;;;;;;; commit b52c3527bc56e3fb51b57f38c564dce58fff6339 Author: Stefan Kangas Date: Sat Aug 20 22:44:14 2022 +0200 image-dired: Do more interactive mode tagging * lisp/image/image-dired-dired.el (image-dired-dired-toggle-marked-thumbs) (image-dired-next-line-and-display) (image-dired-previous-line-and-display) (image-dired-toggle-append-browsing) (image-dired-mark-and-display-next) (image-dired-toggle-dired-display-properties) (image-dired-dired-next-line, image-dired-dired-previous-line) (image-dired-create-thumbs, image-dired-display-thumbs-append) (image-dired-display-thumb, image-dired-dired-display-external) (image-dired-dired-display-image) (image-dired-copy-with-exif-file-name, image-dired-mark-tagged-files) (image-dired-dired-display-properties): * lisp/image/image-dired-external.el (image-dired-thumbnail-set-image-description): * lisp/image/image-dired-tags.el (image-dired-tag-files) (image-dired-tag-thumbnail, image-dired-delete-tag) (image-dired-tag-thumbnail-remove) (image-dired-dired-comment-files) (image-dired-dired-edit-comment-and-tags): * lisp/image/image-dired.el (image-dired-display-thumbs) (image-dired-track-original-file) (image-dired-toggle-movement-tracking) (image-dired-forward-image, image-dired-backward-image) (image-dired-line-up, image-dired-line-up-dynamic) (image-dired-line-up-interactive) (image-dired-thumbnail-display-external) (image-dired-display-thumbnail-original-image) (image-dired-rotate-original-left) (image-dired-rotate-original-right) (image-dired-comment-thumbnail, image-dired-delete-marked) (image-dired-rotate-thumbnail-left) (image-dired-rotate-thumbnail-right): Do interactive mode tagging. * lisp/image/image-dired.el (image-dired-delete-marked): Signal error if not in image-dired-thumbnail-mode. diff --git a/lisp/image/image-dired-dired.el b/lisp/image/image-dired-dired.el index 002e2d4938..2f3a66f9cd 100644 --- a/lisp/image/image-dired-dired.el +++ b/lisp/image/image-dired-dired.el @@ -53,7 +53,7 @@ If no marked file could be found, insert or hide thumbnails on the current line. ARG, if non-nil, specifies the files to use instead of the marked files. If ARG is an integer, use the next ARG (or previous -ARG, if ARG<0) files." - (interactive "P") + (interactive "P" dired-mode) (dired-map-over-marks (let ((image-pos (dired-move-to-filename)) (image-file (dired-get-filename nil t)) @@ -93,7 +93,7 @@ Otherwise, delete overlays." (defun image-dired-next-line-and-display () "Move to next Dired line and display thumbnail image." - (interactive) + (interactive nil dired-mode) (dired-next-line 1) (image-dired-display-thumbs t (or image-dired-append-when-browsing nil) t) @@ -102,7 +102,7 @@ Otherwise, delete overlays." (defun image-dired-previous-line-and-display () "Move to previous Dired line and display thumbnail image." - (interactive) + (interactive nil dired-mode) (dired-previous-line 1) (image-dired-display-thumbs t (or image-dired-append-when-browsing nil) t) @@ -111,7 +111,7 @@ Otherwise, delete overlays." (defun image-dired-toggle-append-browsing () "Toggle `image-dired-append-when-browsing'." - (interactive) + (interactive nil dired-mode) (setq image-dired-append-when-browsing (not image-dired-append-when-browsing)) (message "Append browsing %s" @@ -121,7 +121,7 @@ Otherwise, delete overlays." (defun image-dired-mark-and-display-next () "Mark current file in Dired and display next thumbnail image." - (interactive) + (interactive nil dired-mode) (dired-mark 1) (image-dired-display-thumbs t (or image-dired-append-when-browsing nil) t) @@ -130,7 +130,7 @@ Otherwise, delete overlays." (defun image-dired-toggle-dired-display-properties () "Toggle `image-dired-dired-disp-props'." - (interactive) + (interactive nil dired-mode) (setq image-dired-dired-disp-props (not image-dired-dired-disp-props)) (message "Dired display properties %s" @@ -164,7 +164,7 @@ but the other way around." "Call `dired-next-line', then track thumbnail. This can safely replace `dired-next-line'. With prefix argument, move ARG lines." - (interactive "P") + (interactive "P" dired-mode) (dired-next-line (or arg 1)) (if image-dired-track-movement (image-dired-track-thumbnail))) @@ -173,16 +173,15 @@ With prefix argument, move ARG lines." "Call `dired-previous-line', then track thumbnail. This can safely replace `dired-previous-line'. With prefix argument, move ARG lines." - (interactive "P") + (interactive "P" dired-mode) (dired-previous-line (or arg 1)) (if image-dired-track-movement (image-dired-track-thumbnail))) - ;;;###autoload (defun image-dired-jump-thumbnail-buffer () "Jump to thumbnail buffer." - (interactive) + (interactive nil dired-mode) (let ((window (image-dired-thumbnail-window)) frame) (if window @@ -259,7 +258,7 @@ Note that n, p and and will be hijacked and bound to "Create thumbnail images for all marked files in Dired. With prefix argument ARG, create thumbnails even if they already exist \(i.e. use this to refresh your thumbnails)." - (interactive "P") + (interactive "P" dired-mode) (let (thumb-name) (dolist (curr-file (dired-get-marked-files)) (setq thumb-name (image-dired-thumb-name curr-file)) @@ -275,19 +274,19 @@ With prefix argument ARG, create thumbnails even if they already exist ;;;###autoload (defun image-dired-display-thumbs-append () "Append thumbnails to `image-dired-thumbnail-buffer'." - (interactive) + (interactive nil dired-mode) (image-dired-display-thumbs nil t t)) ;;;###autoload (defun image-dired-display-thumb () "Shorthand for `image-dired-display-thumbs' with prefix argument." - (interactive) + (interactive nil dired-mode) (image-dired-display-thumbs t nil t)) ;;;###autoload (defun image-dired-dired-display-external () "Display file at point using an external viewer." - (interactive) + (interactive nil dired-mode) (let ((file (dired-get-filename))) (start-process "image-dired-external" nil image-dired-external-viewer file))) @@ -297,7 +296,7 @@ With prefix argument ARG, create thumbnails even if they already exist "Display current image file. See documentation for `image-dired-display-image' for more information. With prefix argument ARG, display image in its original size." - (interactive "P") + (interactive "P" dired-mode) (image-dired-display-image (dired-get-filename) arg)) (defun image-dired-copy-with-exif-file-name () @@ -313,7 +312,7 @@ function. The result is a couple of new files in `image-dired-main-image-directory' called 2005_05_08_12_52_00_dscn0319.jpg, 2005_05_08_14_27_45_dscn0320.jpg etc." - (interactive) + (interactive nil dired-mode) (let (new-name (files (dired-get-marked-files))) (mapc @@ -335,7 +334,7 @@ image file and stored in image-dired's database file. This command lets you input a regexp and this will be matched against all tags on all image files in the database file. The files that have a matching tag will be marked in the Dired buffer." - (interactive "sMark tagged files (regexp): ") + (interactive "sMark tagged files (regexp): " dired-mode) (image-dired-sane-db-file) (let ((hits 0) files) @@ -366,7 +365,7 @@ matching tag will be marked in the Dired buffer." (defun image-dired-dired-display-properties () "Display properties for Dired file in the echo area." - (interactive) + (interactive nil dired-mode) (let* ((file (dired-get-filename)) (file-name (file-name-nondirectory file)) (dired-buf (buffer-name (current-buffer))) diff --git a/lisp/image/image-dired-external.el b/lisp/image/image-dired-external.el index 70e00658dc..c45287bd18 100644 --- a/lisp/image/image-dired-external.el +++ b/lisp/image/image-dired-external.el @@ -448,7 +448,7 @@ YYYY_MM_DD_HH_MM_DD_ORIG_FILE_NAME.jpg. Used from "Set the ImageDescription EXIF tag for the original image. If the image already has a value for this tag, it is used as the default value at the prompt." - (interactive) + (interactive nil image-dired-thumbnail-mode) (if (not (image-dired-image-at-point-p)) (message "No thumbnail at point") (let* ((file (image-dired-original-file-name)) diff --git a/lisp/image/image-dired-tags.el b/lisp/image/image-dired-tags.el index 97003851e0..2ea9d9f5eb 100644 --- a/lisp/image/image-dired-tags.el +++ b/lisp/image/image-dired-tags.el @@ -134,7 +134,7 @@ FILE-TAGS is an alist in the following form: ;;;###autoload (defun image-dired-tag-files (arg) "Tag marked file(s) in Dired. With prefix ARG, tag file at point." - (interactive "P") + (interactive "P" dired-mode) (let ((tag (completing-read "Tags to add (separate tags with a semicolon): " image-dired-tag-history nil nil nil 'image-dired-tag-history)) @@ -150,7 +150,7 @@ FILE-TAGS is an alist in the following form: (defun image-dired-tag-thumbnail () "Tag current or marked thumbnails." - (interactive) + (interactive nil image-dired-thumbnail-mode) (let ((tag (completing-read "Tags to add (separate tags with a semicolon): " image-dired-tag-history nil nil nil 'image-dired-tag-history))) @@ -164,7 +164,7 @@ FILE-TAGS is an alist in the following form: (defun image-dired-delete-tag (arg) "Remove tag for selected file(s). With prefix argument ARG, remove tag from file at point." - (interactive "P") + (interactive "P" dired-mode) (let ((tag (completing-read "Tag to remove: " image-dired-tag-history nil nil nil 'image-dired-tag-history)) files) @@ -175,7 +175,7 @@ With prefix argument ARG, remove tag from file at point." (defun image-dired-tag-thumbnail-remove () "Remove tag from current or marked thumbnails." - (interactive) + (interactive nil image-dired-thumbnail-mode) (let ((tag (completing-read "Tag to remove: " image-dired-tag-history nil nil nil 'image-dired-tag-history))) (image-dired--with-marked @@ -231,7 +231,7 @@ FILE-COMMENTS is an alist on the following form: ;;;###autoload (defun image-dired-dired-comment-files () "Add comment to current or marked files in Dired." - (interactive) + (interactive nil dired-mode) (let ((comment (image-dired-read-comment))) (image-dired-write-comments (mapcar @@ -279,7 +279,7 @@ Optionally use old comment from FILE as initial value." "Edit comment and tags of current or marked image files. Edit comment and tags for all marked image files in an easy-to-use form." - (interactive) + (interactive nil dired-mode) (setq image-dired-widget-list nil) ;; Setup buffer. (let ((files (dired-get-marked-files))) diff --git a/lisp/image/image-dired.el b/lisp/image/image-dired.el index 707e70201c..04bfa6a7ba 100644 --- a/lisp/image/image-dired.el +++ b/lisp/image/image-dired.el @@ -504,7 +504,7 @@ used or not. If non-nil, use `display-buffer' instead of `image-dired-next-line-and-display' and `image-dired-previous-line-and-display' where we do not want the thumbnail buffer to be selected." - (interactive "P") + (interactive "P" nil dired-mode) (setq image-dired--generate-thumbs-start (current-time)) (let ((buf (image-dired-create-thumbnail-buffer)) thumb-name files dired-buf) @@ -566,7 +566,7 @@ never ask for confirmation." "Track the original file in the associated Dired buffer. See documentation for `image-dired-toggle-movement-tracking'. Interactive use only useful if `image-dired-track-movement' is nil." - (interactive) + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) (let* ((dired-buf (image-dired-associated-dired-buffer)) (file-name (image-dired-original-file-name)) (window (image-dired-get-buffer-window dired-buf))) @@ -582,7 +582,7 @@ Tracking of the movements between thumbnail and Dired buffer so that they are \"mirrored\" in the dired buffer. When this is on, moving around in the thumbnail or dired buffer will find the matching position in the other buffer." - (interactive) + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) (setq image-dired-track-movement (not image-dired-track-movement)) (message "Movement tracking %s" (if image-dired-track-movement "on" "off"))) @@ -603,7 +603,7 @@ On reaching end or beginning of buffer, stop and show a message. If optional argument WRAP-AROUND is non-nil, wrap around: if point is on the last image, move to the last one and vice versa." - (interactive "p") + (interactive "p" image-dired-thumbnail-mode) (setq arg (or arg 1)) (let (pos) (dotimes (_ (abs arg)) @@ -633,7 +633,7 @@ point is on the last image, move to the last one and vice versa." Optional prefix ARG says how many images to move; the default is one image. Negative means move forward. On reaching end or beginning of buffer, stop and show a message." - (interactive "p") + (interactive "p" image-dired-thumbnail-mode) (image-dired-forward-image (- (or arg 1)))) (defun image-dired-next-line () @@ -1039,7 +1039,7 @@ With a negative prefix argument, prompt user for the delay." (defun image-dired-line-up () "Line up thumbnails according to `image-dired-thumbs-per-row'. See also `image-dired-line-up-dynamic'." - (interactive) + (interactive nil image-dired-thumbnail-mode) (let ((inhibit-read-only t)) (goto-char (point-min)) (while (and (not (image-dired-image-at-point-p)) @@ -1076,7 +1076,7 @@ See also `image-dired-line-up-dynamic'." (defun image-dired-line-up-dynamic () "Line up thumbnails images dynamically. Calculate how many thumbnails fit." - (interactive) + (interactive nil image-dired-thumbnail-mode) (let* ((char-width (frame-char-width)) (width (image-dired-window-width-pixels (image-dired-thumbnail-window))) (image-dired-thumbs-per-row @@ -1090,7 +1090,7 @@ Calculate how many thumbnails fit." (defun image-dired-line-up-interactive () "Line up thumbnails interactively. Ask user how many thumbnails should be displayed per row." - (interactive) + (interactive nil image-dired-thumbnail-mode) (let ((image-dired-thumbs-per-row (string-to-number (read-string "How many thumbs per row: ")))) (if (not (> image-dired-thumbs-per-row 0)) @@ -1099,7 +1099,7 @@ Ask user how many thumbnails should be displayed per row." (defun image-dired-thumbnail-display-external () "Display original image for thumbnail at point using external viewer." - (interactive) + (interactive nil image-dired-thumbnail-mode) (let ((file (image-dired-original-file-name))) (if (not (image-dired-image-at-point-p)) (message "No thumbnail at point") @@ -1131,7 +1131,7 @@ based on `image-mode'." "Display current thumbnail's original image in display buffer. See documentation for `image-dired-display-image' for more information. With prefix argument ARG, display image in its original size." - (interactive "P") + (interactive "P" image-dired-thumbnail-mode) (let ((file (image-dired-original-file-name))) (if (not (string-equal major-mode "image-dired-thumbnail-mode")) (message "Not in image-dired-thumbnail-mode") @@ -1147,7 +1147,7 @@ The result of the rotation is displayed in the image display area and a confirmation is needed before the original image files is overwritten. This confirmation can be turned off using `image-dired-rotate-original-ask-before-overwrite'." - (interactive) + (interactive nil image-dired-thumbnail-mode) (image-dired-rotate-original "270")) (defun image-dired-rotate-original-right () @@ -1156,7 +1156,7 @@ The result of the rotation is displayed in the image display area and a confirmation is needed before the original image files is overwritten. This confirmation can be turned off using `image-dired-rotate-original-ask-before-overwrite'." - (interactive) + (interactive nil image-dired-thumbnail-mode) (image-dired-rotate-original "90")) (defun image-dired-display-next-thumbnail-original (&optional arg) @@ -1178,7 +1178,7 @@ With prefix ARG, move that many thumbnails." (defun image-dired-comment-thumbnail () "Add comment to current thumbnail in thumbnail buffer." - (interactive) + (interactive nil image-dired-comment-thumbnail image-dired-display-image-mode) (let* ((file (image-dired-original-file-name)) (comment (image-dired-read-comment file))) (image-dired-write-comments (list (cons file comment))) @@ -1240,7 +1240,9 @@ for deletion instead." (defun image-dired-delete-marked () "Delete current or marked thumbnails and associated images." - (interactive) + (interactive nil image-dired-thumbnail-mode) + (unless (derived-mode-p 'image-dired-thumbnail-mode) + (user-error "Not in `image-dired-thumbnail-mode'")) (image-dired--with-marked (image-dired-delete-char) (unless (bobp) @@ -1718,14 +1720,14 @@ of the thumbnail file." (defun image-dired-rotate-thumbnail-left () "Rotate thumbnail left (counter clockwise) 90 degrees." (declare (obsolete image-dired-refresh-thumb "29.1")) - (interactive) + (interactive nil image-dired-thumbnail-mode) (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) (image-dired-rotate-thumbnail "270"))) (defun image-dired-rotate-thumbnail-right () "Rotate thumbnail counter right (clockwise) 90 degrees." (declare (obsolete image-dired-refresh-thumb "29.1")) - (interactive) + (interactive nil image-dired-thumbnail-mode) (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) (image-dired-rotate-thumbnail "90"))) commit 9b4084f4bca159c69941cfa1d86b6e4bffccb803 Author: Stefan Kangas Date: Fri Aug 19 21:14:13 2022 +0200 Split image-dired.el into several files (part 2/2) Use a git trick to split a file while preserving line history (for "git blame", "git log --follow", etc.): 1) Make exact copies of the original file, in the same commit as moving it. 2) Next, trim down the extra copies to contain only the relevant parts. [this commit] * lisp/image-dired.el: * lisp/image/image-dired-dired.el: * lisp/image/image-dired-external.el: * lisp/image/image-dired-tags.el: * lisp/image/image-dired-util.el: * lisp/image/image-dired.el: Trim files down to keep only one copy of each definition. diff --git a/lisp/image/image-dired-dired.el b/lisp/image/image-dired-dired.el index 9f12354111..002e2d4938 100644 --- a/lisp/image/image-dired-dired.el +++ b/lisp/image/image-dired-dired.el @@ -1,10 +1,9 @@ -;;; image-dired.el --- use dired to browse and manipulate your images -*- lexical-binding: t -*- +;;; image-dired-dired.el --- Dired specific commands for Image-Dired -*- lexical-binding: t -*- ;; Copyright (C) 2005-2022 Free Software Foundation, Inc. -;; Version: 0.4.11 -;; Keywords: multimedia ;; Author: Mathias Dahl +;; Keywords: multimedia ;; This file is part of GNU Emacs. @@ -23,415 +22,9 @@ ;;; Commentary: -;; BACKGROUND -;; ========== -;; -;; I needed a program to browse, organize and tag my pictures. I got -;; tired of the old gallery program I used as it did not allow -;; multi-file operations easily. Also, it put things out of my -;; control. Image viewing programs I tested did not allow multi-file -;; operations or did not do what I wanted it to. -;; -;; So, I got the idea to use the wonderful functionality of Emacs and -;; `dired' to do it. It would allow me to do almost anything I wanted, -;; which is basically just to browse all my pictures in an easy way, -;; letting me manipulate and tag them in various ways. `dired' already -;; provide all the file handling and navigation facilities; I only -;; needed to add some functions to display the images. -;; -;; I briefly tried out thumbs.el, and although it seemed more -;; powerful than this package, it did not work the way I wanted to. It -;; was too slow to create thumbnails of all files in a directory (I -;; currently keep all my 2000+ images in the same directory) and -;; browsing the thumbnail buffer was slow too. image-dired.el will not -;; create thumbnails until they are needed and the browsing is done -;; quickly and easily in Dired. I copied a great deal of ideas and -;; code from there though... :) -;; -;; `image-dired' stores the thumbnail files in `image-dired-dir' -;; using the file name format ORIGNAME.thumb.ORIGEXT. For example -;; ~/.emacs.d/image-dired/myimage01.thumb.jpg. The "database" is for -;; now just a plain text file with the following format: -;; -;; file-name-non-directory;comment:comment-text;tag1;tag2;tag3;...;tagN -;; -;; -;; PREREQUISITES -;; ============= -;; -;; * The GraphicsMagick or ImageMagick package; Image-Dired uses -;; whichever is available. -;; -;; A) For GraphicsMagick, `gm' is used. -;; Find it here: http://www.graphicsmagick.org/ -;; -;; B) For ImageMagick, `convert' and `mogrify' are used. -;; Find it here: https://www.imagemagick.org. -;; -;; * For non-lossy rotation of JPEG images, the JpegTRAN program is -;; needed. -;; -;; * For `image-dired-set-exif-data' to work, the command line tool `exiftool' is -;; needed. It can be found here: https://exiftool.org/. This -;; function is, among other things, used for writing comments to -;; image files using `image-dired-thumbnail-set-image-description'. -;; -;; -;; USAGE -;; ===== -;; -;; This information has been moved to the manual. Type `C-h r' to open -;; the Emacs manual and go to the node Thumbnails by typing `g -;; Image-Dired RET'. -;; -;; Quickstart: M-x image-dired RET DIRNAME RET -;; -;; where DIRNAME is a directory containing image files. -;; -;; LIMITATIONS -;; =========== -;; -;; * Supports all image formats that Emacs and convert supports, but -;; the thumbnails are hard-coded to JPEG or PNG format. It uses -;; JPEG by default, but can optionally follow the Thumbnail Managing -;; Standard (v0.9.0, Dec 2020), which mandates PNG. See the user -;; option `image-dired-thumbnail-storage'. -;; -;; * WARNING: The "database" format used might be changed so keep a -;; backup of `image-dired-db-file' when testing new versions. -;; -;; TODO -;; ==== -;; -;; * Investigate if it is possible to also write the tags to the image -;; files. -;; -;; * From thumbs.el: Add an option for clean-up/max-size functionality -;; for thumbnail directory. -;; -;; * From thumbs.el: Add setroot function. -;; -;; * Add `image-dired-display-thumbs-ring' and functions to cycle that. Find out -;; which is best, saving old batch just before inserting new, or -;; saving the current batch in the ring when inserting it. Adding -;; it probably needs rewriting `image-dired-display-thumbs' to be more general. -;; -;; * Find some way of toggling on and off really nice keybindings in -;; Dired (for example, using C-n or instead of C-S-n). -;; Richard suggested that we could keep C-t as prefix for -;; image-dired commands as it is currently not used in Dired. He -;; also suggested that `dired-next-line' and `dired-previous-line' -;; figure out if image-dired is enabled in the current buffer and, -;; if it is, call `image-dired-dired-next-line' and `image-dired-dired-previous-line', -;; respectively. Update: This is partly done; some bindings have -;; now been added to Dired. -;; -;; * In some way keep track of buffers and windows and stuff so that -;; it works as the user expects. -;; -;; * More/better documentation. - ;;; Code: -(require 'dired) -(require 'exif) -(require 'image-mode) -(require 'widget) -(require 'xdg) - -(eval-when-compile - (require 'cl-lib) - (require 'wid-edit)) - - -;;; Customizable variables - -(defgroup image-dired nil - "Use Dired to browse your images as thumbnails, and more." - :prefix "image-dired-" - :link '(info-link "(emacs) Image-Dired") - :group 'multimedia) - -(defcustom image-dired-dir (locate-user-emacs-file "image-dired/") - "Directory where thumbnail images are stored. - -The value of this option will be ignored if Image-Dired is -customized to use the Thumbnail Managing Standard; they will be -saved in \"$XDG_CACHE_HOME/thumbnails/\" instead. See -`image-dired-thumbnail-storage'." - :type 'directory) - -(defcustom image-dired-thumbnail-storage 'use-image-dired-dir - "How `image-dired' stores thumbnail files. -There are two ways that Image-Dired can store and generate -thumbnails. If you set this variable to one of the two following -values, they will be stored in the JPEG format: - -- `use-image-dired-dir' means that the thumbnails are stored in a - central directory. - -- `per-directory' means that each thumbnail is stored in a - subdirectory called \".image-dired\" in the same directory - where the image file is. - -It can also use the \"Thumbnail Managing Standard\", which allows -sharing of thumbnails across different programs. Thumbnails will -be stored in \"$XDG_CACHE_HOME/thumbnails/\" instead of in -`image-dired-dir'. Thumbnails are saved in the PNG format, and -can be one of the following sizes: - -- `standard' means use thumbnails sized 128x128. -- `standard-large' means use thumbnails sized 256x256. -- `standard-x-large' means use thumbnails sized 512x512. -- `standard-xx-large' means use thumbnails sized 1024x1024. - -For more information on the Thumbnail Managing Standard, see: -https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html" - :type '(choice :tag "How to store thumbnail files" - (const :tag "Use image-dired-dir" use-image-dired-dir) - (const :tag "Thumbnail Managing Standard (normal 128x128)" - standard) - (const :tag "Thumbnail Managing Standard (large 256x256)" - standard-large) - (const :tag "Thumbnail Managing Standard (larger 512x512)" - standard-x-large) - (const :tag "Thumbnail Managing Standard (extra large 1024x1024)" - standard-xx-large) - (const :tag "Per-directory" per-directory)) - :version "29.1") - -(defconst image-dired--thumbnail-standard-sizes - '( standard standard-large - standard-x-large standard-xx-large) - "List of symbols representing thumbnail sizes in Thumbnail Managing Standard.") - -(defcustom image-dired-db-file - (expand-file-name ".image-dired_db" image-dired-dir) - "Database file where file names and their associated tags are stored." - :type 'file) - -(defcustom image-dired-cmd-create-thumbnail-program - (if (executable-find "gm") "gm" "convert") - "Executable used to create thumbnail. -Used together with `image-dired-cmd-create-thumbnail-options'." - :type 'file - :version "29.1") - -(defcustom image-dired-cmd-create-thumbnail-options - (let ((opts '("-size" "%wx%h" "%f[0]" - "-resize" "%wx%h>" - "-strip" "jpeg:%t"))) - (if (executable-find "gm") (cons "convert" opts) opts)) - "Options of command used to create thumbnail image. -Used with `image-dired-cmd-create-thumbnail-program'. -Available format specifiers are: %w which is replaced by -`image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', -%f which is replaced by the file name of the original image and %t -which is replaced by the file name of the thumbnail file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-pngnq-program - ;; Prefer pngquant to pngnq-s9 as it is faster on my machine. - ;; The project also seems more active than the alternatives. - ;; Prefer pngnq-s9 to pngnq as it fixes bugs in pngnq. - ;; The pngnq project seems dead (?) since 2011 or so. - (or (executable-find "pngquant") - (executable-find "pngnq-s9") - (executable-find "pngnq")) - "The file name of the `pngquant' or `pngnq' program. -It quantizes colors of PNG images down to 256 colors or fewer -using the NeuQuant algorithm." - :version "29.1" - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-pngnq-options - (if (executable-find "pngquant") - '("--ext" "-nq8.png" "%t") ; same extension as "pngnq" - '("-f" "%t")) - "Arguments to pass `image-dired-cmd-pngnq-program'. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options'." - :type '(repeat (string :tag "Argument")) - :version "29.1") - -(defcustom image-dired-cmd-pngcrush-program (executable-find "pngcrush") - "The file name of the `pngcrush' program. -It optimizes the compression of PNG images. Also it adds PNG textual chunks -with the information required by the Thumbnail Managing Standard." - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-pngcrush-options - `("-q" - "-text" "b" "Description" "Thumbnail of file://%f" - "-text" "b" "Software" ,(emacs-version) - ;; "-text b \"Thumb::Image::Height\" \"%oh\" " - ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" " - ;; "-text b \"Thumb::Image::Width\" \"%ow\" " - "-text" "b" "Thumb::MTime" "%m" - ;; "-text b \"Thumb::Size\" \"%b\" " - "-text" "b" "Thumb::URI" "file://%f" - "%q" "%t") - "Arguments for `image-dired-cmd-pngcrush-program'. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options', with %q for a -temporary file name (typically generated by pnqnq)." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-optipng-program (executable-find "optipng") - "The file name of the `optipng' program." - :version "26.1" - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-optipng-options '("-o5" "%t") - "Arguments passed to `image-dired-cmd-optipng-program'. -Available format specifiers are described in -`image-dired-cmd-create-thumbnail-options'." - :version "26.1" - :type '(repeat (string :tag "Argument")) - :link '(url-link "man:optipng(1)")) - -(defcustom image-dired-cmd-create-standard-thumbnail-options - (append '("-size" "%wx%h" "%f[0]") - (unless (or image-dired-cmd-pngcrush-program - image-dired-cmd-pngnq-program) - (list - "-set" "Thumb::MTime" "%m" - "-set" "Thumb::URI" "file://%f" - "-set" "Description" "Thumbnail of file://%f" - "-set" "Software" (emacs-version))) - '("-thumbnail" "%wx%h>" "png:%t")) - "Options for creating thumbnails according to the Thumbnail Managing Standard. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options', with %m for file modification time." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-rotate-original-program - "jpegtran" - "Executable used to rotate original image. -Used together with `image-dired-cmd-rotate-original-options'." - :type 'file) - -(defcustom image-dired-cmd-rotate-original-options - '("-rotate" "%d" "-copy" "all" "-outfile" "%t" "%o") - "Arguments of command used to rotate original image. -Used with `image-dired-cmd-rotate-original-program'. -Available format specifiers are: %d which is replaced by the -number of (positive) degrees to rotate the image, normally 90 or -270 \(for 90 degrees right and left), %o which is replaced by the -original image file name and %t which is replaced by -`image-dired-temp-image-file'." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-temp-rotate-image-file - (expand-file-name ".image-dired_rotate_temp" image-dired-dir) - "Temporary file for rotate operations." - :type 'file) - -(defcustom image-dired-rotate-original-ask-before-overwrite t - "Confirm overwrite of original file after rotate operation. -If non-nil, ask user for confirmation before overwriting the -original file with `image-dired-temp-rotate-image-file'." - :type 'boolean) - -(defcustom image-dired-cmd-write-exif-data-program - "exiftool" - "Program used to write EXIF data to image. -Used together with `image-dired-cmd-write-exif-data-options'." - :type 'file) - -(defcustom image-dired-cmd-write-exif-data-options - '("-%t=%v" "%f") - "Arguments of command used to write EXIF data. -Used with `image-dired-cmd-write-exif-data-program'. -Available format specifiers are: %f which is replaced by -the image file name, %t which is replaced by the tag name and %v -which is replaced by the tag value." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-thumb-size - (cond - ((eq 'standard image-dired-thumbnail-storage) 128) - ((eq 'standard-large image-dired-thumbnail-storage) 256) - ((eq 'standard-x-large image-dired-thumbnail-storage) 512) - ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) - (t 100)) - "Size of thumbnails, in pixels. -This is the default size for both `image-dired-thumb-width' -and `image-dired-thumb-height'. - -The value of this option will be ignored if Image-Dired is -customized to use the Thumbnail Managing Standard; the standard -sizes will be used instead. See `image-dired-thumbnail-storage'." - :type 'integer) - -(defcustom image-dired-thumb-width image-dired-thumb-size - "Width of thumbnails, in pixels." - :type 'integer) - -(defcustom image-dired-thumb-height image-dired-thumb-size - "Height of thumbnails, in pixels." - :type 'integer) - -(defcustom image-dired-thumb-relief 2 - "Size of button-like border around thumbnails." - :type 'integer) - -(defcustom image-dired-thumb-margin 2 - "Size of the margin around thumbnails. -This is where you see the cursor." - :type 'integer) - -(defcustom image-dired-thumb-visible-marks t - "Make marks and flags visible in thumbnail buffer. -If non-nil, apply the `image-dired-thumb-mark' face to marked -images and `image-dired-thumb-flagged' to images flagged for -deletion." - :type 'boolean - :version "28.1") - -(defface image-dired-thumb-mark - '((((class color) (min-colors 16)) :background "DarkOrange") - (((class color)) :foreground "yellow")) - "Face for marked images in thumbnail buffer." - :version "29.1") - -(defface image-dired-thumb-flagged - '((((class color) (min-colors 88) (background light)) :background "Red3") - (((class color) (min-colors 88) (background dark)) :background "Pink") - (((class color) (min-colors 16) (background light)) :background "Red3") - (((class color) (min-colors 16) (background dark)) :background "Pink") - (((class color) (min-colors 8)) :background "red") - (t :inverse-video t)) - "Face for images flagged for deletion in thumbnail buffer." - :version "29.1") - -(defcustom image-dired-line-up-method 'dynamic - "Default method for line-up of thumbnails in thumbnail buffer. -Used by `image-dired-display-thumbs' and other functions that needs -to line-up thumbnails. Dynamic means to use the available width of -the window containing the thumbnail buffer, Fixed means to use -`image-dired-thumbs-per-row', Interactive is for asking the user, -and No line-up means that no automatic line-up will be done." - :type '(choice :tag "Default line-up method" - (const :tag "Dynamic" dynamic) - (const :tag "Fixed" fixed) - (const :tag "Interactive" interactive) - (const :tag "No line-up" none))) - -(defcustom image-dired-thumbs-per-row 3 - "Number of thumbnails to display per row in thumb buffer." - :type 'integer) - -(defcustom image-dired-track-movement t - "The current state of the tracking and mirroring. -For more information, see the documentation for -`image-dired-toggle-movement-tracking'." - :type 'boolean) +(require 'image-dired) (defcustom image-dired-append-when-browsing nil "Append thumbnails in thumbnail buffer when browsing. @@ -441,6 +34,7 @@ images in the thumbnail buffer. If you enable this and want to clean the thumbnail buffer because it is filled with too many thumbnails, just call `image-dired-display-thumb' to display only the image at point. This value can be toggled using `image-dired-toggle-append-browsing'." + :group 'image-dired :type 'boolean) (defcustom image-dired-dired-disp-props t @@ -449,383 +43,9 @@ Used by `image-dired-next-line-and-display', `image-dired-previous-line-and-display' and `image-dired-mark-and-display-next'. If the database file is large, this can slow down image browsing in Dired and you might want to turn it off." + :group 'image-dired :type 'boolean) -(defcustom image-dired-display-properties-format "%b: %f (%t): %c" - "Display format for thumbnail properties. -%b is replaced with associated Dired buffer name, %f with file -name (without path) of original image file, %t with the list of -tags and %c with the comment." - :type 'string) - -(defcustom image-dired-external-viewer - ;; TODO: Use mailcap, dired-guess-shell-alist-default, - ;; dired-view-command-alist. - (cond ((executable-find "display")) - ((executable-find "xli")) - ((executable-find "qiv") "qiv -t") - ((executable-find "feh") "feh")) - "Name of external viewer. -Including parameters. Used when displaying original image from -`image-dired-thumbnail-mode'." - :version "28.1" - :type '(choice string - (const :tag "Not Set" nil))) - -(defcustom image-dired-main-image-directory - (or (xdg-user-dir "PICTURES") "~/pics/") - "Name of main image directory, if any. -Used by `image-dired-copy-with-exif-file-name'." - :type 'string - :version "29.1") - -(defcustom image-dired-show-all-from-dir-max-files 500 - "Maximum number of files in directory before prompting. - -If there are more image files than this in a selected directory, -the `image-dired-show-all-from-dir' command will ask for -confirmation before creating the thumbnail buffer. If this -variable is nil, it will never ask." - :type '(choice integer - (const :tag "Disable warning" nil)) - :version "29.1") - -(defcustom image-dired-marking-shows-next t - "If non-nil, marking, unmarking or flagging an image shows the next image. - -This affects the following commands: -\\ - `image-dired-flag-thumb-original-file' (bound to \\[image-dired-flag-thumb-original-file]) - `image-dired-mark-thumb-original-file' (bound to \\[image-dired-mark-thumb-original-file]) - `image-dired-unmark-thumb-original-file' (bound to \\[image-dired-unmark-thumb-original-file])" - :type 'boolean - :version "29.1") - - -;;; Util functions - -(defvar image-dired-debug nil - "Non-nil means enable debug messages.") - -(defun image-dired-debug-message (&rest args) - "Display debug message ARGS when `image-dired-debug' is non-nil." - (when image-dired-debug - (apply #'message args))) - -(defmacro image-dired--with-db-file (&rest body) - "Run BODY in a temp buffer containing `image-dired-db-file'. -Return the last form in BODY." - (declare (indent 0) (debug t)) - `(with-temp-buffer - (if (file-exists-p image-dired-db-file) - (insert-file-contents image-dired-db-file)) - ,@body)) - -(defun image-dired-dir () - "Return the current thumbnail directory (from variable `image-dired-dir'). -Create the thumbnail directory if it does not exist." - (let ((image-dired-dir (file-name-as-directory - (expand-file-name image-dired-dir)))) - (unless (file-directory-p image-dired-dir) - (with-file-modes #o700 - (make-directory image-dired-dir t)) - (message "Thumbnail directory created: %s" image-dired-dir)) - image-dired-dir)) - -(defun image-dired-insert-image (file type relief margin) - "Insert image FILE of image TYPE, using RELIEF and MARGIN, at point." - (let ((i `(image :type ,type - :file ,file - :relief ,relief - :margin ,margin))) - (insert-image i))) - -(defun image-dired-get-thumbnail-image (file) - "Return the image descriptor for a thumbnail of image file FILE." - (unless (string-match-p (image-file-name-regexp) file) - (error "%s is not a valid image file" file)) - (let* ((thumb-file (image-dired-thumb-name file)) - (thumb-attr (file-attributes thumb-file))) - (when (or (not thumb-attr) - (time-less-p (file-attribute-modification-time thumb-attr) - (file-attribute-modification-time - (file-attributes file)))) - (image-dired-create-thumb file thumb-file)) - (create-image thumb-file))) - -(defun image-dired-insert-thumbnail (file original-file-name - associated-dired-buffer) - "Insert thumbnail image FILE. -Add text properties ORIGINAL-FILE-NAME and ASSOCIATED-DIRED-BUFFER." - (let (beg end) - (setq beg (point)) - (image-dired-insert-image - file - ;; Thumbnails are created asynchronously, so we might not yet - ;; have a file. But if it exists, it might have been cached from - ;; before and we should use it instead of our current settings. - (or (and (file-exists-p file) - (image-type-from-file-header file)) - (and (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - 'png) - 'jpeg) - image-dired-thumb-relief - image-dired-thumb-margin) - (setq end (point)) - (add-text-properties - beg end - (list 'image-dired-thumbnail t - 'original-file-name original-file-name - 'associated-dired-buffer associated-dired-buffer - 'tags (image-dired-list-tags original-file-name) - 'mouse-face 'highlight - 'comment (image-dired-get-comment original-file-name))))) - -(defun image-dired-thumb-name (file) - "Return absolute file name for thumbnail FILE. -Depending on the value of `image-dired-thumbnail-storage', the -file name of the thumbnail will vary: -- For `use-image-dired-dir', make a SHA1-hash of the image file's - directory name and add that to make the thumbnail file name - unique. -- For `per-directory' storage, just add a subdirectory. -- For `standard' storage, produce the file name according to the - Thumbnail Managing Standard. Among other things, an MD5-hash - of the image file's directory name will be added to the - filename. -See also `image-dired-thumbnail-storage'." - (cond ((memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - (let ((thumbdir (cl-case image-dired-thumbnail-storage - (standard "thumbnails/normal") - (standard-large "thumbnails/large") - (standard-x-large "thumbnails/x-large") - (standard-xx-large "thumbnails/xx-large")))) - (expand-file-name - ;; MD5 is mandated by the Thumbnail Managing Standard. - (concat (md5 (concat "file://" (expand-file-name file))) ".png") - (expand-file-name thumbdir (xdg-cache-home))))) - ((eq 'use-image-dired-dir image-dired-thumbnail-storage) - (let* ((f (expand-file-name file)) - (hash - (md5 (file-name-as-directory (file-name-directory f))))) - (format "%s%s%s.thumb.%s" - (file-name-as-directory (expand-file-name (image-dired-dir))) - (file-name-base f) - (if hash (concat "_" hash) "") - (file-name-extension f)))) - ((eq 'per-directory image-dired-thumbnail-storage) - (let ((f (expand-file-name file))) - (format "%s.image-dired/%s.thumb.%s" - (file-name-directory f) - (file-name-base f) - (file-name-extension f)))))) - -(defun image-dired--check-executable-exists (executable) - (unless (executable-find (symbol-value executable)) - (error "Executable %S not found" executable))) - - -;;; Creating thumbnails - -(defun image-dired-thumb-size (dimension) - "Return thumb size depending on `image-dired-thumbnail-storage'. -DIMENSION should be either the symbol `width' or `height'." - (cond - ((eq 'standard image-dired-thumbnail-storage) 128) - ((eq 'standard-large image-dired-thumbnail-storage) 256) - ((eq 'standard-x-large image-dired-thumbnail-storage) 512) - ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) - (t (cl-ecase dimension - (width image-dired-thumb-width) - (height image-dired-thumb-height))))) - -(defvar image-dired--generate-thumbs-start nil - "Time when `display-thumbs' was called.") - -(defvar image-dired-queue nil - "List of items in the queue. -Each item has the form (ORIGINAL-FILE TARGET-FILE).") - -(defvar image-dired-queue-active-jobs 0 - "Number of active jobs in `image-dired-queue'.") - -(defvar image-dired-queue-active-limit (min 4 (max 2 (/ (num-processors) 2))) - "Maximum number of concurrent jobs permitted for generating images. -Increase at own risk. If you want to experiment with this, -consider setting `image-dired-debug' to a non-nil value to see -the time spent on generating thumbnails. Run `image-clear-cache' -and remove the cached thumbnail files between each trial run.") - -(defun image-dired-pngnq-thumb (spec) - "Quantize thumbnail described by format SPEC with pngnq(1)." - (let ((process - (apply #'start-process "image-dired-pngnq" nil - image-dired-cmd-pngnq-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-pngnq-options)))) - (setf (process-sentinel process) - (lambda (process status) - (if (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - ;; Pass off to pngcrush, or just rename the - ;; THUMB-nq8.png file back to THUMB.png - (if (and image-dired-cmd-pngcrush-program - (executable-find image-dired-cmd-pngcrush-program)) - (image-dired-pngcrush-thumb spec) - (let ((nq8 (cdr (assq ?q spec))) - (thumb (cdr (assq ?t spec)))) - (rename-file nq8 thumb t))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))))) - process)) - -(defun image-dired-pngcrush-thumb (spec) - "Optimize thumbnail described by format SPEC with pngcrush(1)." - ;; If pngnq wasn't run, then the THUMB-nq8.png file does not exist. - ;; pngcrush needs an infile and outfile, so we just copy THUMB to - ;; THUMB-nq8.png and use the latter as a temp file. - (when (not image-dired-cmd-pngnq-program) - (let ((temp (cdr (assq ?q spec))) - (thumb (cdr (assq ?t spec)))) - (copy-file thumb temp))) - (let ((process - (apply #'start-process "image-dired-pngcrush" nil - image-dired-cmd-pngcrush-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-pngcrush-options)))) - (setf (process-sentinel process) - (lambda (process status) - (unless (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))) - (when (memq (process-status process) '(exit signal)) - (let ((temp (cdr (assq ?q spec)))) - (delete-file temp))))) - process)) - -(defun image-dired-optipng-thumb (spec) - "Optimize thumbnail described by format SPEC with optipng(1)." - (let ((process - (apply #'start-process "image-dired-optipng" nil - image-dired-cmd-optipng-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-optipng-options)))) - (setf (process-sentinel process) - (lambda (process status) - (unless (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))))) - process)) - -(defun image-dired-create-thumb-1 (original-file thumbnail-file) - "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE." - (image-dired--check-executable-exists - 'image-dired-cmd-create-thumbnail-program) - (let* ((width (int-to-string (image-dired-thumb-size 'width))) - (height (int-to-string (image-dired-thumb-size 'height))) - (modif-time (format-time-string - "%s" (file-attribute-modification-time - (file-attributes original-file)))) - (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png" - thumbnail-file)) - (spec - (list - (cons ?w width) - (cons ?h height) - (cons ?m modif-time) - (cons ?f original-file) - (cons ?q thumbnail-nq8-file) - (cons ?t thumbnail-file))) - (thumbnail-dir (file-name-directory thumbnail-file)) - process) - (when (not (file-exists-p thumbnail-dir)) - (with-file-modes #o700 - (make-directory thumbnail-dir t)) - (message "Thumbnail directory created: %s" thumbnail-dir)) - - ;; Thumbnail file creation processes begin here and are marshaled - ;; in a queue by `image-dired-create-thumb'. - (setq process - (apply #'start-process "image-dired-create-thumbnail" nil - image-dired-cmd-create-thumbnail-program - (mapcar - (lambda (arg) (format-spec arg spec)) - (if (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - image-dired-cmd-create-standard-thumbnail-options - image-dired-cmd-create-thumbnail-options)))) - - (setf (process-sentinel process) - (lambda (process status) - ;; Trigger next in queue once a thumbnail has been created - (cl-decf image-dired-queue-active-jobs) - (image-dired-thumb-queue-run) - (when (= image-dired-queue-active-jobs 0) - (image-dired-debug-message - (format-time-string - "Generated thumbnails in %s.%3N seconds" - (time-subtract nil - image-dired--generate-thumbs-start)))) - (if (not (and (eq (process-status process) 'exit) - (zerop (process-exit-status process)))) - (message "Thumb could not be created for %s: %s" - (abbreviate-file-name original-file) - (string-replace "\n" "" status)) - (set-file-modes thumbnail-file #o600) - (clear-image-cache thumbnail-file) - ;; PNG thumbnail has been created since we are - ;; following the XDG thumbnail spec, so try to optimize - (when (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - (cond - ((and image-dired-cmd-pngnq-program - (executable-find image-dired-cmd-pngnq-program)) - (image-dired-pngnq-thumb spec)) - ((and image-dired-cmd-pngcrush-program - (executable-find image-dired-cmd-pngcrush-program)) - (image-dired-pngcrush-thumb spec)) - ((and image-dired-cmd-optipng-program - (executable-find image-dired-cmd-optipng-program)) - (image-dired-optipng-thumb spec))))))) - process)) - -(defun image-dired-thumb-queue-run () - "Run a queued job if one exists and not too many jobs are running. -Queued items live in `image-dired-queue'." - (while (and image-dired-queue - (< image-dired-queue-active-jobs - image-dired-queue-active-limit)) - (cl-incf image-dired-queue-active-jobs) - (apply #'image-dired-create-thumb-1 (pop image-dired-queue)))) - -(defun image-dired-create-thumb (original-file thumbnail-file) - "Add a job for generating ORIGINAL-FILE thumbnail to `image-dired-queue'. -The new file will be named THUMBNAIL-FILE." - (setq image-dired-queue - (nconc image-dired-queue - (list (list original-file thumbnail-file)))) - (run-at-time 0 nil #'image-dired-thumb-queue-run)) - -(defmacro image-dired--with-marked (&rest body) - "Eval BODY with point on each marked thumbnail. -If no marked file could be found, execute BODY on the current -thumbnail." - `(with-current-buffer image-dired-thumbnail-buffer - (let (found) - (save-mark-and-excursion - (goto-char (point-min)) - (while (not (eobp)) - (when (image-dired-thumb-file-marked-p) - (setq found t) - ,@body) - (forward-char))) - (unless found - ,@body)))) - ;;;###autoload (defun image-dired-dired-toggle-marked-thumbs (&optional arg) "Toggle thumbnails in front of file names in the Dired buffer. @@ -918,348 +138,6 @@ Otherwise, delete overlays." "on" "off"))) -(defvar image-dired-thumbnail-buffer "*image-dired*" - "Image-Dired's thumbnail buffer.") - -(defun image-dired-create-thumbnail-buffer () - "Create thumb buffer and set `image-dired-thumbnail-mode'." - (let ((buf (get-buffer-create image-dired-thumbnail-buffer))) - (with-current-buffer buf - (setq buffer-read-only t) - (if (not (eq major-mode 'image-dired-thumbnail-mode)) - (image-dired-thumbnail-mode))) - buf)) - -(defvar image-dired-display-image-buffer "*image-dired-display-image*" - "Where larger versions of the images are display.") - -(defvar image-dired-saved-window-configuration nil - "Saved window configuration.") - -;;;###autoload -(defun image-dired-dired-with-window-configuration (dir &optional arg) - "Open directory DIR and create a default window configuration. - -Convenience command that: - - - Opens Dired in folder DIR - - Splits windows in most useful (?) way - - Sets `truncate-lines' to t - -After the command has finished, you would typically mark some -image files in Dired and type -\\[image-dired-display-thumbs] (`image-dired-display-thumbs'). - -If called with prefix argument ARG, skip splitting of windows. - -The current window configuration is saved and can be restored by -calling `image-dired-restore-window-configuration'." - (interactive "DDirectory: \nP") - (let ((buf (image-dired-create-thumbnail-buffer)) - (buf2 (get-buffer-create image-dired-display-image-buffer))) - (setq image-dired-saved-window-configuration - (current-window-configuration)) - (dired dir) - (delete-other-windows) - (when (not arg) - (split-window-right) - (setq truncate-lines t) - (save-excursion - (other-window 1) - (pop-to-buffer-same-window buf) - (select-window (split-window-below)) - (pop-to-buffer-same-window buf2) - (other-window -2))))) - -(defun image-dired-restore-window-configuration () - "Restore window configuration. -Restore any changes to the window configuration made by calling -`image-dired-dired-with-window-configuration'." - (interactive nil image-dired-thumbnail-mode) - (if image-dired-saved-window-configuration - (set-window-configuration image-dired-saved-window-configuration) - (message "No saved window configuration"))) - -(defun image-dired--line-up-with-method () - "Line up thumbnails according to `image-dired-line-up-method'." - (cond ((eq 'dynamic image-dired-line-up-method) - (image-dired-line-up-dynamic)) - ((eq 'fixed image-dired-line-up-method) - (image-dired-line-up)) - ((eq 'interactive image-dired-line-up-method) - (image-dired-line-up-interactive)) - ((eq 'none image-dired-line-up-method) - nil) - (t - (image-dired-line-up-dynamic)))) - -;;;###autoload -(defun image-dired-display-thumbs (&optional arg append do-not-pop) - "Display thumbnails of all marked files, in `image-dired-thumbnail-buffer'. -If a thumbnail image does not exist for a file, it is created on the -fly. With prefix argument ARG, display only thumbnail for file at -point (this is useful if you have marked some files but want to show -another one). - -Recommended usage is to split the current frame horizontally so that -you have the Dired buffer in the left window and the -`image-dired-thumbnail-buffer' buffer in the right window. - -With optional argument APPEND, append thumbnail to thumbnail buffer -instead of erasing it first. - -Optional argument DO-NOT-POP controls if `pop-to-buffer' should be -used or not. If non-nil, use `display-buffer' instead of -`pop-to-buffer'. This is used from functions like -`image-dired-next-line-and-display' and -`image-dired-previous-line-and-display' where we do not want the -thumbnail buffer to be selected." - (interactive "P") - (setq image-dired--generate-thumbs-start (current-time)) - (let ((buf (image-dired-create-thumbnail-buffer)) - thumb-name files dired-buf) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (setq dired-buf (current-buffer)) - (with-current-buffer buf - (let ((inhibit-read-only t)) - (if (not append) - (erase-buffer) - (goto-char (point-max))) - (dolist (curr-file files) - (setq thumb-name (image-dired-thumb-name curr-file)) - (when (not (file-exists-p thumb-name)) - (image-dired-create-thumb curr-file thumb-name)) - (image-dired-insert-thumbnail thumb-name curr-file dired-buf))) - (if do-not-pop - (display-buffer buf) - (pop-to-buffer buf)) - (image-dired--line-up-with-method)))) - -;;;###autoload -(defun image-dired-show-all-from-dir (dir) - "Make a thumbnail buffer for all images in DIR and display it. -Any file matching `image-file-name-regexp' is considered an image -file. - -If the number of image files in DIR exceeds -`image-dired-show-all-from-dir-max-files', ask for confirmation -before creating the thumbnail buffer. If that variable is nil, -never ask for confirmation." - (interactive "DImage-Dired: ") - (dired dir) - (dired-mark-files-regexp (image-file-name-regexp)) - (let ((files (dired-get-marked-files nil nil nil t))) - (cond ((and (null (cdr files))) - (message "No image files in directory")) - ((or (not image-dired-show-all-from-dir-max-files) - (<= (length (cdr files)) image-dired-show-all-from-dir-max-files) - (and (> (length (cdr files)) image-dired-show-all-from-dir-max-files) - (y-or-n-p - (format - "Directory contains more than %d image files. Proceed?" - image-dired-show-all-from-dir-max-files)))) - (image-dired-display-thumbs) - (pop-to-buffer image-dired-thumbnail-buffer) - (setq default-directory dir) - (image-dired-unmark-all-marks)) - (t (message "Image-Dired canceled"))))) - -;;;###autoload -(defalias 'image-dired 'image-dired-show-all-from-dir) - - -;;; Tags - -(defun image-dired-sane-db-file () - "Check if `image-dired-db-file' exists. -If not, try to create it (including any parent directories). -Signal error if there are problems creating it." - (or (file-exists-p image-dired-db-file) - (let (dir buf) - (unless (file-directory-p (setq dir (file-name-directory - image-dired-db-file))) - (with-file-modes #o700 - (make-directory dir t))) - (with-current-buffer (setq buf (create-file-buffer - image-dired-db-file)) - (with-file-modes #o600 - (write-file image-dired-db-file))) - (kill-buffer buf) - (file-exists-p image-dired-db-file)) - (error "Could not create %s" image-dired-db-file))) - -(defvar image-dired-tag-history nil "Variable holding the tag history.") - -(defun image-dired-write-tags (file-tags) - "Write file tags to database. -Write each file and tag in FILE-TAGS to the database. -FILE-TAGS is an alist in the following form: - ((FILE . TAG) ... )" - (image-dired-sane-db-file) - (let (end file tag) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (dolist (elt file-tags) - (setq file (car elt) - tag (cdr elt)) - (goto-char (point-min)) - (if (search-forward-regexp (format "^%s.*$" file) nil t) - (progn - (setq end (point)) - (beginning-of-line) - (when (not (search-forward (format ";%s" tag) end t)) - (end-of-line) - (insert (format ";%s" tag)))) - (goto-char (point-max)) - (insert (format "%s;%s\n" file tag)))) - (save-buffer)))) - -(defun image-dired-remove-tag (files tag) - "For all FILES, remove TAG from the image database." - (image-dired-sane-db-file) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (let (end) - (unless (listp files) - (if (stringp files) - (setq files (list files)) - (error "Files must be a string or a list of strings!"))) - (dolist (file files) - (goto-char (point-min)) - (when (search-forward-regexp (format "^%s;" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (search-forward-regexp - (format "\\(;%s\\)\\($\\|;\\)" tag) end t) - (delete-region (match-beginning 1) (match-end 1)) - ;; Check if file should still be in the database. If - ;; it has no tags or comments, it will be removed. - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (not (search-forward ";" end t)) - (kill-line 1)))))) - (save-buffer))) - -(defun image-dired-list-tags (file) - "Read all tags for image FILE from the image database." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end (tags "")) - (when (search-forward-regexp (format "^%s" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (if (search-forward ";" end t) - (if (search-forward "comment:" end t) - (if (search-forward ";" end t) - (setq tags (buffer-substring (point) end))) - (setq tags (buffer-substring (point) end))))) - (split-string tags ";")))) - -;;;###autoload -(defun image-dired-tag-files (arg) - "Tag marked file(s) in Dired. With prefix ARG, tag file at point." - (interactive "P") - (let ((tag (completing-read - "Tags to add (separate tags with a semicolon): " - image-dired-tag-history nil nil nil 'image-dired-tag-history)) - files) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (image-dired-write-tags - (mapcar - (lambda (x) - (cons x tag)) - files)))) - -(defun image-dired-tag-thumbnail () - "Tag current or marked thumbnails." - (interactive) - (let ((tag (completing-read - "Tags to add (separate tags with a semicolon): " - image-dired-tag-history nil nil nil 'image-dired-tag-history))) - (image-dired--with-marked - (image-dired-write-tags - (list (cons (image-dired-original-file-name) tag))) - (image-dired-update-property - 'tags (image-dired-list-tags (image-dired-original-file-name)))))) - -;;;###autoload -(defun image-dired-delete-tag (arg) - "Remove tag for selected file(s). -With prefix argument ARG, remove tag from file at point." - (interactive "P") - (let ((tag (completing-read "Tag to remove: " image-dired-tag-history - nil nil nil 'image-dired-tag-history)) - files) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (image-dired-remove-tag files tag))) - -(defun image-dired-tag-thumbnail-remove () - "Remove tag from current or marked thumbnails." - (interactive) - (let ((tag (completing-read "Tag to remove: " image-dired-tag-history - nil nil nil 'image-dired-tag-history))) - (image-dired--with-marked - (image-dired-remove-tag (image-dired-original-file-name) tag) - (image-dired-update-property - 'tags (image-dired-list-tags (image-dired-original-file-name)))))) - - -;;; Thumbnail mode (cont.) - -(defun image-dired-original-file-name () - "Get original file name for thumbnail or display image at point." - (get-text-property (point) 'original-file-name)) - -(defun image-dired-file-name-at-point () - "Get abbreviated file name for thumbnail or display image at point." - (let ((f (image-dired-original-file-name))) - (when f - (abbreviate-file-name f)))) - -(defun image-dired-associated-dired-buffer () - "Get associated Dired buffer at point." - (get-text-property (point) 'associated-dired-buffer)) - -(defun image-dired-get-buffer-window (buf) - "Return window where buffer BUF is." - (get-window-with-predicate - (lambda (window) - (equal (window-buffer window) buf)) - nil t)) - -(defun image-dired-track-original-file () - "Track the original file in the associated Dired buffer. -See documentation for `image-dired-toggle-movement-tracking'. -Interactive use only useful if `image-dired-track-movement' is nil." - (interactive) - (let* ((dired-buf (image-dired-associated-dired-buffer)) - (file-name (image-dired-original-file-name)) - (window (image-dired-get-buffer-window dired-buf))) - (and (buffer-live-p dired-buf) file-name - (with-current-buffer dired-buf - (if (not (dired-goto-file file-name)) - (message "Could not track file") - (if window (set-window-point window (point)))))))) - -(defun image-dired-toggle-movement-tracking () - "Turn on and off `image-dired-track-movement'. -Tracking of the movements between thumbnail and Dired buffer so that -they are \"mirrored\" in the dired buffer. When this is on, moving -around in the thumbnail or dired buffer will find the matching -position in the other buffer." - (interactive) - (setq image-dired-track-movement (not image-dired-track-movement)) - (message "Movement tracking %s" (if image-dired-track-movement "on" "off"))) - (defun image-dired-track-thumbnail () "Track current Dired file's thumb in `image-dired-thumbnail-buffer'. This is almost the same as what `image-dired-track-original-file' does, @@ -1300,239 +178,6 @@ With prefix argument, move ARG lines." (if image-dired-track-movement (image-dired-track-thumbnail))) -(defun image-dired--display-thumb-properties-fun () - (let ((old-buf (current-buffer)) - (old-point (point))) - (lambda () - (when (and (equal (current-buffer) old-buf) - (= (point) old-point)) - (ignore-errors - (image-dired-update-header-line)))))) - -(defun image-dired-forward-image (&optional arg wrap-around) - "Move to next image and display properties. -Optional prefix ARG says how many images to move; the default is -one image. Negative means move backwards. -On reaching end or beginning of buffer, stop and show a message. - -If optional argument WRAP-AROUND is non-nil, wrap around: if -point is on the last image, move to the last one and vice versa." - (interactive "p") - (setq arg (or arg 1)) - (let (pos) - (dotimes (_ (abs arg)) - (if (and (not (if (> arg 0) (eobp) (bobp))) - (save-excursion - (forward-char (if (> arg 0) 1 -1)) - (while (and (not (if (> arg 0) (eobp) (bobp))) - (not (image-dired-image-at-point-p))) - (forward-char (if (> arg 0) 1 -1))) - (setq pos (point)) - (image-dired-image-at-point-p))) - (progn (goto-char pos) - (image-dired-update-header-line)) - (if wrap-around - (progn (goto-char (if (> arg 0) - (point-min) - ;; There are two spaces after the last image. - (- (point-max) 2))) - (image-dired-update-header-line)) - (message "At %s image" (if (> arg 0) "last" "first")) - (run-at-time 1 nil (image-dired--display-thumb-properties-fun)))))) - (when image-dired-track-movement - (image-dired-track-original-file))) - -(defun image-dired-backward-image (&optional arg) - "Move to previous image and display properties. -Optional prefix ARG says how many images to move; the default is -one image. Negative means move forward. -On reaching end or beginning of buffer, stop and show a message." - (interactive "p") - (image-dired-forward-image (- (or arg 1)))) - -(defun image-dired-next-line () - "Move to next line and display properties." - (interactive nil image-dired-thumbnail-mode) - (let ((goal-column (current-column))) - (forward-line 1) - (move-to-column goal-column)) - ;; If we end up in an empty spot, back up to the next thumbnail. - (if (not (image-dired-image-at-point-p)) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - - -(defun image-dired-previous-line () - "Move to previous line and display properties." - (interactive nil image-dired-thumbnail-mode) - (let ((goal-column (current-column))) - (forward-line -1) - (move-to-column goal-column)) - ;; If we end up in an empty spot, back up to the next - ;; thumbnail. This should only happen if the user deleted a - ;; thumbnail and did not refresh, so it is not very common. But we - ;; can handle it in a good manner, so why not? - (if (not (image-dired-image-at-point-p)) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-beginning-of-buffer () - "Move to the first image in the buffer and display properties." - (interactive nil image-dired-thumbnail-mode) - (goto-char (point-min)) - (while (and (not (image-at-point-p)) - (not (eobp))) - (forward-char 1)) - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-end-of-buffer () - "Move to the last image in the buffer and display properties." - (interactive nil image-dired-thumbnail-mode) - (goto-char (point-max)) - (while (and (not (image-at-point-p)) - (not (bobp))) - (forward-char -1)) - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-format-properties-string (buf file props comment) - "Format display properties. -BUF is the associated Dired buffer, FILE is the original image file -name, PROPS is a stringified list of tags and COMMENT is the image file's -comment." - (format-spec - image-dired-display-properties-format - (list - (cons ?b (or buf "")) - (cons ?f file) - (cons ?t (or props "")) - (cons ?c (or comment ""))))) - -(defun image-dired-update-header-line () - "Update image information in the header line." - (when (and (not (eobp)) - (memq major-mode '(image-dired-thumbnail-mode - image-dired-display-image-mode))) - (let ((file-name (file-name-nondirectory (image-dired-original-file-name))) - (dired-buf (buffer-name (image-dired-associated-dired-buffer))) - (props (mapconcat #'identity (get-text-property (point) 'tags) ", ")) - (comment (get-text-property (point) 'comment)) - (message-log-max nil)) - (if file-name - (setq header-line-format - (image-dired-format-properties-string - dired-buf - file-name - props - comment)))))) - -(defun image-dired-dired-file-marked-p (&optional marker) - "In Dired, return t if file on current line is marked. -If optional argument MARKER is non-nil, it is a character to look -for. The default is to look for `dired-marker-char'." - (setq marker (or marker dired-marker-char)) - (save-excursion - (beginning-of-line) - (and (looking-at dired-re-mark) - (= (aref (match-string 0) 0) marker)))) - -(defun image-dired-dired-file-flagged-p () - "In Dired, return t if file on current line is flagged for deletion." - (image-dired-dired-file-marked-p dired-del-marker)) - -(defmacro image-dired--with-thumbnail-buffer (&rest body) - (declare (indent defun) (debug t)) - `(if-let ((buf (get-buffer image-dired-thumbnail-buffer))) - (with-current-buffer buf - (if-let ((win (get-buffer-window buf))) - (with-selected-window win - ,@body) - ,@body)) - (user-error "No such buffer: %s" image-dired-thumbnail-buffer))) - -(defmacro image-dired--on-file-in-dired-buffer (&rest body) - "Run BODY with point on file at point in Dired buffer. -Should be called from commands in `image-dired-thumbnail-mode'." - (declare (indent defun) (debug t)) - `(let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (if (not (and dired-buf file-name)) - (message "No image, or image with correct properties, at point") - (with-current-buffer dired-buf - (when (dired-goto-file file-name) - ,@body - (image-dired-thumb-update-marks)))))) - -(defmacro image-dired--do-mark-command (maybe-next &rest body) - "Helper macro for the mark, unmark and flag commands. -Run BODY in Dired buffer. -If optional argument MAYBE-NEXT is non-nil, show next image -according to `image-dired-marking-shows-next'." - (declare (indent defun) (debug t)) - `(image-dired--with-thumbnail-buffer - (image-dired--on-file-in-dired-buffer - ,@body) - ,(when maybe-next - '(if image-dired-marking-shows-next - (image-dired-display-next-thumbnail-original) - (image-dired-next-line))))) - -(defun image-dired-mark-thumb-original-file () - "Mark original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-mark 1))) - -(defun image-dired-unmark-thumb-original-file () - "Unmark original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-unmark 1))) - -(defun image-dired-flag-thumb-original-file () - "Flag original image file for deletion in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-flag-file-deletion 1))) - -(defun image-dired-toggle-mark-thumb-original-file () - "Toggle mark on original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command nil - (if (image-dired-dired-file-marked-p) - (dired-unmark 1) - (dired-mark 1)))) - -(defun image-dired-unmark-all-marks () - "Remove all marks from all files in associated Dired buffer. -Also update the marks in the thumbnail buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command nil - (dired-unmark-all-marks)) - (image-dired--with-thumbnail-buffer - (image-dired-thumb-update-marks))) - -(defun image-dired-jump-original-dired-buffer () - "Jump to the Dired buffer associated with the current image file. -You probably want to use this together with -`image-dired-track-original-file'." - (interactive nil image-dired-thumbnail-mode) - (let ((buf (image-dired-associated-dired-buffer)) - window frame) - (setq window (image-dired-get-buffer-window buf)) - (if window - (progn - (if (not (equal (selected-frame) (setq frame (window-frame window)))) - (select-frame-set-input-focus frame)) - (select-window window)) - (message "Associated dired buffer not visible")))) ;;;###autoload (defun image-dired-jump-thumbnail-buffer () @@ -1547,155 +192,6 @@ You probably want to use this together with (select-window window)) (message "Thumbnail buffer not visible")))) -(defvar image-dired-thumbnail-mode-line-up-map - (let ((map (make-sparse-keymap))) - ;; map it to "g" so that the user can press it more quickly - (define-key map "g" #'image-dired-line-up-dynamic) - ;; "f" for "fixed" number of thumbs per row - (define-key map "f" #'image-dired-line-up) - ;; "i" for "interactive" - (define-key map "i" #'image-dired-line-up-interactive) - map) - "Keymap for line-up commands in `image-dired-thumbnail-mode'.") - -(defvar image-dired-thumbnail-mode-tag-map - (let ((map (make-sparse-keymap))) - ;; map it to "t" so that the user can press it more quickly - (define-key map "t" #'image-dired-tag-thumbnail) - ;; "r" for "remove" - (define-key map "r" #'image-dired-tag-thumbnail-remove) - map) - "Keymap for tag commands in `image-dired-thumbnail-mode'.") - -(defvar image-dired-thumbnail-mode-map - (let ((map (make-sparse-keymap))) - (define-key map [right] #'image-dired-forward-image) - (define-key map [left] #'image-dired-backward-image) - (define-key map [up] #'image-dired-previous-line) - (define-key map [down] #'image-dired-next-line) - (define-key map "\C-f" #'image-dired-forward-image) - (define-key map "\C-b" #'image-dired-backward-image) - (define-key map "\C-p" #'image-dired-previous-line) - (define-key map "\C-n" #'image-dired-next-line) - - (define-key map "<" #'image-dired-beginning-of-buffer) - (define-key map ">" #'image-dired-end-of-buffer) - (define-key map (kbd "M-<") #'image-dired-beginning-of-buffer) - (define-key map (kbd "M->") #'image-dired-end-of-buffer) - - (define-key map "d" #'image-dired-flag-thumb-original-file) - (define-key map [delete] #'image-dired-flag-thumb-original-file) - (define-key map "m" #'image-dired-mark-thumb-original-file) - (define-key map "u" #'image-dired-unmark-thumb-original-file) - (define-key map "U" #'image-dired-unmark-all-marks) - (define-key map "." #'image-dired-track-original-file) - (define-key map [tab] #'image-dired-jump-original-dired-buffer) - - ;; add line-up map - (define-key map "g" image-dired-thumbnail-mode-line-up-map) - ;; add tag map - (define-key map "t" image-dired-thumbnail-mode-tag-map) - - (define-key map "\C-m" #'image-dired-display-thumbnail-original-image) - (define-key map [C-return] #'image-dired-thumbnail-display-external) - - (define-key map "L" #'image-dired-rotate-original-left) - (define-key map "R" #'image-dired-rotate-original-right) - - (define-key map "D" #'image-dired-thumbnail-set-image-description) - (define-key map "S" #'image-dired-slideshow-start) - (define-key map "\C-d" #'image-dired-delete-char) - (define-key map " " #'image-dired-display-next-thumbnail-original) - (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) - (define-key map "c" #'image-dired-comment-thumbnail) - - ;; Mouse - (define-key map [mouse-2] #'image-dired-mouse-display-image) - (define-key map [mouse-1] #'image-dired-mouse-select-thumbnail) - (define-key map [mouse-3] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-1] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-2] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-3] #'image-dired-mouse-select-thumbnail) - ;; Seems I must first set C-down-mouse-1 to undefined, or else it - ;; will trigger the buffer menu. If I try to instead bind - ;; C-down-mouse-1 to `image-dired-mouse-toggle-mark', I get a message - ;; about C-mouse-1 not being defined afterwards. Annoying, but I - ;; probably do not completely understand mouse events. - (define-key map [C-down-mouse-1] #'undefined) - (define-key map [C-mouse-1] #'image-dired-mouse-toggle-mark) - map) - "Keymap for `image-dired-thumbnail-mode'.") - -(easy-menu-define image-dired-thumbnail-mode-menu image-dired-thumbnail-mode-map - "Menu for `image-dired-thumbnail-mode'." - '("Image-Dired" - ["Display image" image-dired-display-thumbnail-original-image] - ["Display in external viewer" image-dired-thumbnail-display-external] - ["Jump to Dired buffer" image-dired-jump-original-dired-buffer] - "---" - ["Mark image" image-dired-mark-thumb-original-file] - ["Unmark image" image-dired-unmark-thumb-original-file] - ["Unmark all images" image-dired-unmark-all-marks] - ["Flag for deletion" image-dired-flag-thumb-original-file] - ["Delete marked images" image-dired-delete-marked] - "---" - ["Rotate original right" image-dired-rotate-original-right] - ["Rotate original left" image-dired-rotate-original-left] - "---" - ["Comment thumbnail" image-dired-comment-thumbnail] - ["Tag current or marked thumbnails" image-dired-tag-thumbnail] - ["Remove tag from current or marked thumbnails" - image-dired-tag-thumbnail-remove] - ["Start slideshow" image-dired-slideshow-start] - "---" - ("View Options" - ["Toggle movement tracking" image-dired-toggle-movement-tracking - :style toggle - :selected image-dired-track-movement] - "---" - ["Line up thumbnails" image-dired-line-up] - ["Dynamic line up" image-dired-line-up-dynamic] - ["Refresh thumb" image-dired-refresh-thumb]) - ["Quit" quit-window])) - -(defvar image-dired-display-image-mode-map - (let ((map (make-sparse-keymap))) - (define-key map "S" #'image-dired-slideshow-start) - (define-key map (kbd "SPC") #'image-dired-display-next-thumbnail-original) - (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) - (define-key map "n" #'image-dired-display-next-thumbnail-original) - (define-key map "p" #'image-dired-display-previous-thumbnail-original) - (define-key map "m" #'image-dired-mark-thumb-original-file) - (define-key map "d" #'image-dired-flag-thumb-original-file) - (define-key map "u" #'image-dired-unmark-thumb-original-file) - (define-key map "U" #'image-dired-unmark-all-marks) - ;; Disable keybindings from `image-mode-map' that doesn't make sense here. - (define-key map "o" nil) ; image-save - map) - "Keymap for `image-dired-display-image-mode'.") - -(define-derived-mode image-dired-thumbnail-mode - special-mode "image-dired-thumbnail" - "Browse and manipulate thumbnail images using Dired. -Use `image-dired-minor-mode' to get a nice setup." - :interactive nil - (buffer-disable-undo) - (add-hook 'file-name-at-point-functions 'image-dired-file-name-at-point nil t) - (setq-local window-resize-pixelwise t) - (setq-local bookmark-make-record-function #'image-dired-bookmark-make-record) - ;; Use approximately as much vertical spacing as horizontal. - (setq-local line-spacing (frame-char-width))) - - -;;; Display image mode - -(define-derived-mode image-dired-display-image-mode - image-mode "image-dired-image-display" - "Mode for displaying and manipulating original image. -Resized or in full-size." - :interactive nil - (add-hook 'file-name-at-point-functions #'image-dired-file-name-at-point nil t)) - (defvar image-dired-minor-mode-map (let ((map (make-sparse-keymap))) ;; (set-keymap-parent map dired-mode-map) @@ -1776,74 +272,6 @@ With prefix argument ARG, create thumbnails even if they already exist arg) (image-dired-create-thumb curr-file thumb-name))))) - -;;; Slideshow - -(defcustom image-dired-slideshow-delay 5.0 - "Seconds to wait before showing the next image in a slideshow. -This is used by `image-dired-slideshow-start'." - :type 'float - :version "29.1") - -(define-obsolete-variable-alias 'image-dired-slideshow-timer - 'image-dired--slideshow-timer "29.1") -(defvar image-dired--slideshow-timer nil - "Slideshow timer.") - -(defvar image-dired--slideshow-initial nil) - -(defun image-dired-slideshow-step () - "Step to next image in a slideshow." - (if-let ((buf (get-buffer image-dired-thumbnail-buffer))) - (with-current-buffer buf - (image-dired-display-next-thumbnail-original)) - (image-dired-slideshow-stop))) - -(defun image-dired-slideshow-start (&optional arg) - "Start a slideshow, waiting `image-dired-slideshow-delay' between images. - -With prefix argument ARG, wait that many seconds before going to -the next image. - -With a negative prefix argument, prompt user for the delay." - (interactive "P" image-dired-thumbnail-mode image-dired-display-image-mode) - (let ((delay (if (not arg) - image-dired-slideshow-delay - (if (> arg 0) - arg - (string-to-number - (let ((delay (number-to-string image-dired-slideshow-delay))) - (read-string - (format-prompt "Delay, in seconds. Decimals are accepted" delay)) - delay)))))) - (setq image-dired--slideshow-timer - (run-with-timer - 0 delay - 'image-dired-slideshow-step)) - (add-hook 'post-command-hook 'image-dired-slideshow-stop) - (setq image-dired--slideshow-initial t) - (message "Running slideshow; use any command to stop"))) - -(defun image-dired-slideshow-stop () - "Cancel slideshow." - ;; Make sure we don't immediately stop after - ;; `image-dired-slideshow-start'. - (unless image-dired--slideshow-initial - (remove-hook 'post-command-hook 'image-dired-slideshow-stop) - (cancel-timer image-dired--slideshow-timer)) - (setq image-dired--slideshow-initial nil)) - - -;;; Thumbnail mode (cont. 3) - -(defun image-dired-delete-char () - "Remove current thumbnail from thumbnail buffer and line up." - (interactive nil image-dired-thumbnail-mode) - (let ((inhibit-read-only t)) - (delete-char 1) - (when (= (following-char) ?\s) - (delete-char 1)))) - ;;;###autoload (defun image-dired-display-thumbs-append () "Append thumbnails to `image-dired-thumbnail-buffer'." @@ -1856,78 +284,6 @@ With a negative prefix argument, prompt user for the delay." (interactive) (image-dired-display-thumbs t nil t)) -(defun image-dired-line-up () - "Line up thumbnails according to `image-dired-thumbs-per-row'. -See also `image-dired-line-up-dynamic'." - (interactive) - (let ((inhibit-read-only t)) - (goto-char (point-min)) - (while (and (not (image-dired-image-at-point-p)) - (not (eobp))) - (delete-char 1)) - (while (not (eobp)) - (forward-char) - (while (and (not (image-dired-image-at-point-p)) - (not (eobp))) - (delete-char 1))) - (goto-char (point-min)) - (let ((seen 0) - (thumb-prev-pos 0) - (thumb-width-chars - (ceiling (/ (+ (* 2 image-dired-thumb-relief) - (* 2 image-dired-thumb-margin) - (image-dired-thumb-size 'width)) - (float (frame-char-width)))))) - (while (not (eobp)) - (forward-char) - (if (= image-dired-thumbs-per-row 1) - (insert "\n") - (cl-incf thumb-prev-pos thumb-width-chars) - (insert (propertize " " 'display `(space :align-to ,thumb-prev-pos))) - (cl-incf seen) - (when (and (= seen (- image-dired-thumbs-per-row 1)) - (not (eobp))) - (forward-char) - (insert "\n") - (setq seen 0) - (setq thumb-prev-pos 0))))) - (goto-char (point-min)))) - -(defun image-dired-line-up-dynamic () - "Line up thumbnails images dynamically. -Calculate how many thumbnails fit." - (interactive) - (let* ((char-width (frame-char-width)) - (width (image-dired-window-width-pixels (image-dired-thumbnail-window))) - (image-dired-thumbs-per-row - (/ width - (+ (* 2 image-dired-thumb-relief) - (* 2 image-dired-thumb-margin) - (image-dired-thumb-size 'width) - char-width)))) - (image-dired-line-up))) - -(defun image-dired-line-up-interactive () - "Line up thumbnails interactively. -Ask user how many thumbnails should be displayed per row." - (interactive) - (let ((image-dired-thumbs-per-row - (string-to-number (read-string "How many thumbs per row: ")))) - (if (not (> image-dired-thumbs-per-row 0)) - (message "Number must be greater than 0") - (image-dired-line-up)))) - -(defun image-dired-thumbnail-display-external () - "Display original image for thumbnail at point using external viewer." - (interactive) - (let ((file (image-dired-original-file-name))) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (if (not file) - (message "No original file name found") - (start-process "image-dired-thumb-external" nil - image-dired-external-viewer file))))) - ;;;###autoload (defun image-dired-dired-display-external () "Display file at point using an external viewer." @@ -1936,69 +292,6 @@ Ask user how many thumbnails should be displayed per row." (start-process "image-dired-external" nil image-dired-external-viewer file))) -(defun image-dired-window-width-pixels (window) - "Calculate WINDOW width in pixels." - (* (window-width window) (frame-char-width))) - -(defun image-dired-display-window () - "Return window where `image-dired-display-image-buffer' is visible." - (get-window-with-predicate - (lambda (window) - (equal (buffer-name (window-buffer window)) image-dired-display-image-buffer)) - nil t)) - -(defun image-dired-thumbnail-window () - "Return window where `image-dired-thumbnail-buffer' is visible." - (get-window-with-predicate - (lambda (window) - (equal (buffer-name (window-buffer window)) image-dired-thumbnail-buffer)) - nil t)) - -(defun image-dired-associated-dired-buffer-window () - "Return window where associated Dired buffer is visible." - (let (buf) - (if (image-dired-image-at-point-p) - (progn - (setq buf (image-dired-associated-dired-buffer)) - (get-window-with-predicate - (lambda (window) - (equal (window-buffer window) buf)))) - (error "No thumbnail image at point")))) - -(defun image-dired-display-image (file &optional _ignored) - "Display image FILE in image buffer. -Use this when you want to display the image, in a new window. -The window will use `image-dired-display-image-mode' which is -based on `image-mode'." - (declare (advertised-calling-convention (file) "29.1")) - (setq file (expand-file-name file)) - (when (not (file-exists-p file)) - (error "No such file: %s" file)) - (let ((buf (get-buffer image-dired-display-image-buffer)) - (cur-win (selected-window))) - (when buf - (kill-buffer buf)) - (when-let ((buf (find-file-noselect file nil t))) - (pop-to-buffer buf) - (rename-buffer image-dired-display-image-buffer) - (image-dired-display-image-mode) - (select-window cur-win)))) - -(defun image-dired-display-thumbnail-original-image (&optional arg) - "Display current thumbnail's original image in display buffer. -See documentation for `image-dired-display-image' for more information. -With prefix argument ARG, display image in its original size." - (interactive "P") - (let ((file (image-dired-original-file-name))) - (if (not (string-equal major-mode "image-dired-thumbnail-mode")) - (message "Not in image-dired-thumbnail-mode") - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (if (not file) - (message "No original file name found") - (image-dired-display-image file arg)))))) - - ;;;###autoload (defun image-dired-dired-display-image (&optional arg) "Display current image file. @@ -2007,121 +300,6 @@ With prefix argument ARG, display image in its original size." (interactive "P") (image-dired-display-image (dired-get-filename) arg)) -(defun image-dired-image-at-point-p () - "Return non-nil if there is an `image-dired' thumbnail at point." - (get-text-property (point) 'image-dired-thumbnail)) - -(defun image-dired-refresh-thumb () - "Force creation of new image for current thumbnail." - (interactive nil image-dired-thumbnail-mode) - (let* ((file (image-dired-original-file-name)) - (thumb (expand-file-name (image-dired-thumb-name file)))) - (clear-image-cache (expand-file-name thumb)) - (image-dired-create-thumb file thumb))) - -(defun image-dired-rotate-original (degrees) - "Rotate original image DEGREES degrees." - (image-dired--check-executable-exists - 'image-dired-cmd-rotate-original-program) - (if (not (image-dired-image-at-point-p)) - (message "No image at point") - (let* ((file (image-dired-original-file-name)) - (spec - (list - (cons ?d degrees) - (cons ?o (expand-file-name file)) - (cons ?t image-dired-temp-rotate-image-file)))) - (unless (eq 'jpeg (image-type file)) - (user-error "Only JPEG images can be rotated")) - (if (not (= 0 (apply #'call-process image-dired-cmd-rotate-original-program - nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-rotate-original-options)))) - (error "Could not rotate image") - (image-dired-display-image image-dired-temp-rotate-image-file) - (if (or (and image-dired-rotate-original-ask-before-overwrite - (y-or-n-p - "Rotate to temp file OK. Overwrite original image? ")) - (not image-dired-rotate-original-ask-before-overwrite)) - (progn - (copy-file image-dired-temp-rotate-image-file file t) - (image-dired-refresh-thumb)) - (image-dired-display-image file)))))) - -(defun image-dired-rotate-original-left () - "Rotate original image left (counter clockwise) 90 degrees. -The result of the rotation is displayed in the image display area -and a confirmation is needed before the original image files is -overwritten. This confirmation can be turned off using -`image-dired-rotate-original-ask-before-overwrite'." - (interactive) - (image-dired-rotate-original "270")) - -(defun image-dired-rotate-original-right () - "Rotate original image right (clockwise) 90 degrees. -The result of the rotation is displayed in the image display area -and a confirmation is needed before the original image files is -overwritten. This confirmation can be turned off using -`image-dired-rotate-original-ask-before-overwrite'." - (interactive) - (image-dired-rotate-original "90")) - - -;;; EXIF support - -(defun image-dired-get-exif-file-name (file) - "Use the image's EXIF information to return a unique file name. -The file name should be unique as long as you do not take more than -one picture per second. The original file name is suffixed at the end -for traceability. The format of the returned file name is -YYYY_MM_DD_HH_MM_DD_ORIG_FILE_NAME.jpg. Used from -`image-dired-copy-with-exif-file-name'." - (let (data no-exif-data-found) - (if (not (eq 'jpeg (image-type (expand-file-name file)))) - (setq no-exif-data-found t - data (format-time-string - "%Y:%m:%d %H:%M:%S" - (file-attribute-modification-time - (file-attributes (expand-file-name file))))) - (setq data (exif-field 'date-time (exif-parse-file - (expand-file-name file))))) - (while (string-match "[ :]" data) - (setq data (replace-match "_" nil nil data))) - (format "%s%s%s" data - (if no-exif-data-found - "_noexif_" - "_") - (file-name-nondirectory file)))) - -(defun image-dired-thumbnail-set-image-description () - "Set the ImageDescription EXIF tag for the original image. -If the image already has a value for this tag, it is used as the -default value at the prompt." - (interactive) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (let* ((file (image-dired-original-file-name)) - (old-value (or (exif-field 'description (exif-parse-file file)) ""))) - (if (eq 0 - (image-dired-set-exif-data file "ImageDescription" - (read-string "Value of ImageDescription: " - old-value))) - (message "Successfully wrote ImageDescription tag") - (error "Could not write ImageDescription tag"))))) - -(defun image-dired-set-exif-data (file tag-name tag-value) - "In FILE, set EXIF tag TAG-NAME to value TAG-VALUE." - (image-dired--check-executable-exists - 'image-dired-cmd-write-exif-data-program) - (let ((spec - (list - (cons ?f (expand-file-name file)) - (cons ?t tag-name) - (cons ?v tag-value)))) - (apply #'call-process image-dired-cmd-write-exif-data-program nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-write-exif-data-options)))) - (defun image-dired-copy-with-exif-file-name () "Copy file with unique name to main image directory. Copy current or all marked files in Dired to a new file in your @@ -2149,117 +327,6 @@ function. The result is a couple of new files in (copy-file curr-file new-name)) files))) -;;; Thumbnail mode (cont.) - -(defun image-dired-display-next-thumbnail-original (&optional arg) - "Move to the next image in the thumbnail buffer and display it. -With prefix ARG, move that many thumbnails." - (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--with-thumbnail-buffer - (image-dired-forward-image arg t) - (image-dired-display-thumbnail-original-image))) - -(defun image-dired-display-previous-thumbnail-original (arg) - "Move to the previous image in the thumbnail buffer and display it. -With prefix ARG, move that many thumbnails." - (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired-display-next-thumbnail-original (- arg))) - - -;;; Image Comments - -(defun image-dired-write-comments (file-comments) - "Write file comments to database. -Write file comments to one or more files. -FILE-COMMENTS is an alist on the following form: - ((FILE . COMMENT) ... )" - (image-dired-sane-db-file) - (let (end comment-beg-pos comment-end-pos file comment) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (dolist (elt file-comments) - (setq file (car elt) - comment (cdr elt)) - (goto-char (point-min)) - (if (search-forward-regexp (format "^%s.*$" file) nil t) - (progn - (setq end (point)) - (beginning-of-line) - ;; Delete old comment, if any - (when (search-forward ";comment:" end t) - (setq comment-beg-pos (match-beginning 0)) - ;; Any tags after the comment? - (if (search-forward ";" end t) - (setq comment-end-pos (- (point) 1)) - (setq comment-end-pos end)) - ;; Delete comment tag and comment - (delete-region comment-beg-pos comment-end-pos)) - ;; Insert new comment - (beginning-of-line) - (unless (search-forward ";" end t) - (end-of-line) - (insert ";")) - (insert (format "comment:%s;" comment))) - ;; File does not exist in database - add it. - (goto-char (point-max)) - (insert (format "%s;comment:%s\n" file comment)))) - (save-buffer)))) - -(defun image-dired-update-property (prop value) - "Update text property PROP with value VALUE at point." - (let ((inhibit-read-only t)) - (put-text-property - (point) (1+ (point)) - prop - value))) - -;;;###autoload -(defun image-dired-dired-comment-files () - "Add comment to current or marked files in Dired." - (interactive) - (let ((comment (image-dired-read-comment))) - (image-dired-write-comments - (mapcar - (lambda (curr-file) - (cons curr-file comment)) - (dired-get-marked-files))))) - -(defun image-dired-comment-thumbnail () - "Add comment to current thumbnail in thumbnail buffer." - (interactive) - (let* ((file (image-dired-original-file-name)) - (comment (image-dired-read-comment file))) - (image-dired-write-comments (list (cons file comment))) - (image-dired-update-property 'comment comment)) - (image-dired-update-header-line)) - -(defun image-dired-read-comment (&optional file) - "Read comment for an image. -Optionally use old comment from FILE as initial value." - (let ((comment - (read-string - "Comment: " - (if file (image-dired-get-comment file))))) - comment)) - -(defun image-dired-get-comment (file) - "Get comment for file FILE." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end comment-beg-pos comment-end-pos comment) - (when (search-forward-regexp (format "^%s" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (search-forward ";comment:" end t) - (setq comment-beg-pos (point)) - (if (search-forward ";" end t) - (setq comment-end-pos (- (point) 1)) - (setq comment-end-pos end)) - (setq comment (buffer-substring - comment-beg-pos comment-end-pos)))) - comment))) - ;;;###autoload (defun image-dired-mark-tagged-files (regexp) "Use REGEXP to mark files with matching tag. @@ -2297,117 +364,6 @@ matching tag will be marked in the Dired buffer." (dired-mark 1)))) (message "%d files with matching tag marked" hits))) - - -;;; Mouse support - -(defun image-dired-mouse-display-image (event) - "Use mouse EVENT, call `image-dired-display-image' to display image. -Track this in associated Dired buffer if `image-dired-track-movement' is -non-nil." - (interactive "e") - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (unless (image-at-point-p) - (image-dired-backward-image)) - (let ((file (image-dired-original-file-name))) - (when file - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-display-image file)))) - -(defun image-dired-mouse-select-thumbnail (event) - "Use mouse EVENT to select thumbnail image. -Track this in associated Dired buffer if `image-dired-track-movement' is -non-nil." - (interactive "e") - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (unless (image-at-point-p) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - - - -;;; Dired marks and tags - -(defun image-dired-thumb-file-marked-p (&optional flagged) - "Check if file is marked in associated Dired buffer. -If optional argument FLAGGED is non-nil, check if file is flagged -for deletion instead." - (let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (when (and dired-buf file-name) - (with-current-buffer dired-buf - (save-excursion - (when (dired-goto-file file-name) - (if flagged - (image-dired-dired-file-flagged-p) - (image-dired-dired-file-marked-p)))))))) - -(defun image-dired-thumb-file-flagged-p () - "Check if file is flagged for deletion in associated Dired buffer." - (image-dired-thumb-file-marked-p t)) - -(defun image-dired-delete-marked () - "Delete current or marked thumbnails and associated images." - (interactive) - (image-dired--with-marked - (image-dired-delete-char) - (unless (bobp) - (backward-char))) - (image-dired--line-up-with-method) - (with-current-buffer (image-dired-associated-dired-buffer) - (dired-do-delete))) - -(defun image-dired-thumb-update-marks () - "Update the marks in the thumbnail buffer." - (when image-dired-thumb-visible-marks - (with-current-buffer image-dired-thumbnail-buffer - (save-mark-and-excursion - (goto-char (point-min)) - (let ((inhibit-read-only t)) - (while (not (eobp)) - (with-silent-modifications - (cond ((image-dired-thumb-file-marked-p) - (add-face-text-property (point) (1+ (point)) - 'image-dired-thumb-mark)) - ((image-dired-thumb-file-flagged-p) - (add-face-text-property (point) (1+ (point)) - 'image-dired-thumb-flagged)) - (t (remove-text-properties (point) (1+ (point)) - '(face image-dired-thumb-mark))))) - (forward-char))))))) - -(defun image-dired-mouse-toggle-mark-1 () - "Toggle Dired mark for current thumbnail. -Track this in associated Dired buffer if -`image-dired-track-movement' is non-nil." - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-toggle-mark-thumb-original-file)) - -(defun image-dired-mouse-toggle-mark (event) - "Use mouse EVENT to toggle Dired mark for thumbnail. -Toggle marks of all thumbnails in region, if it's active. -Track this in associated Dired buffer if -`image-dired-track-movement' is non-nil." - (interactive "e") - (if (use-region-p) - (let ((end (region-end))) - (save-excursion - (goto-char (region-beginning)) - (while (<= (point) end) - (when (image-dired-image-at-point-p) - (image-dired-mouse-toggle-mark-1)) - (forward-char)))) - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (image-dired-mouse-toggle-mark-1)) - (image-dired-thumb-update-marks)) - (defun image-dired-dired-display-properties () "Display properties for Dired file in the echo area." (interactive) @@ -2425,656 +381,10 @@ Track this in associated Dired buffer if props comment))))) - - -;;; Gallery support - -;; TODO: -;; * Support gallery creation when using per-directory thumbnail -;; storage. -;; * Enhanced gallery creation with basic CSS-support and pagination -;; of tag pages with many pictures. - -(defgroup image-dired-gallery nil - "Image-Dired support for generating a HTML gallery." - :prefix "image-dired-" - :group 'image-dired - :version "29.1") - -(defcustom image-dired-gallery-dir - (expand-file-name ".image-dired_gallery" image-dired-dir) - "Directory to store generated gallery html pages. -The name of this directory needs to be \"shared\" to the public -so that it can access the index.html page that image-dired creates." - :type 'directory) - -(defcustom image-dired-gallery-image-root-url - "https://example.org/image-diredpics" - "URL where the full size images are to be found on your web server. -Note that this URL has to be configured on your web server. -Image-Dired expects to find pictures in this directory. -This is used by `image-dired-gallery-generate'." - :type 'string - :version "29.1") - -(defcustom image-dired-gallery-thumb-image-root-url - "https://example.org/image-diredthumbs" - "URL where the thumbnail images are to be found on your web server. -Note that URL path has to be configured on your web server. -Image-Dired expects to find pictures in this directory. -This is used by `image-dired-gallery-generate'." - :type 'string - :version "29.1") - -(defcustom image-dired-gallery-hidden-tags - (list "private" "hidden" "pending") - "List of \"hidden\" tags. -Used by `image-dired-gallery-generate' to leave out \"hidden\" images." - :type '(repeat string)) - -(defvar image-dired-tag-file-list nil - "List to store tag-file structure.") - -(defvar image-dired-file-tag-list nil - "List to store file-tag structure.") - -(defvar image-dired-file-comment-list nil - "List to store file comments.") - -(defun image-dired--add-to-tag-file-lists (tag file) - "Helper function used from `image-dired--create-gallery-lists'. - -Add TAG to FILE in one list and FILE to TAG in the other. - -Lisp structures look like the following: - -image-dired-file-tag-list: - - ((\"filename1\" \"tag1\" \"tag2\" \"tag3\" ...) - (\"filename2\" \"tag1\" \"tag2\" \"tag3\" ...) - ...) - -image-dired-tag-file-list: - - ((\"tag1\" \"filename1\" \"filename2\" \"filename3\" ...) - (\"tag2\" \"filename1\" \"filename2\" \"filename3\" ...) - ...)" - ;; Add tag to file list - (let (curr) - (if image-dired-file-tag-list - (if (setq curr (assoc file image-dired-file-tag-list)) - (setcdr curr (cons tag (cdr curr))) - (setcdr image-dired-file-tag-list - (cons (list file tag) (cdr image-dired-file-tag-list)))) - (setq image-dired-file-tag-list (list (list file tag)))) - ;; Add file to tag list - (if image-dired-tag-file-list - (if (setq curr (assoc tag image-dired-tag-file-list)) - (if (not (member file curr)) - (setcdr curr (cons file (cdr curr)))) - (setcdr image-dired-tag-file-list - (cons (list tag file) (cdr image-dired-tag-file-list)))) - (setq image-dired-tag-file-list (list (list tag file)))))) - -(defun image-dired--add-to-file-comment-list (file comment) - "Helper function used from `image-dired--create-gallery-lists'. - -For FILE, add COMMENT to list. - -Lisp structure looks like the following: - -image-dired-file-comment-list: - - ((\"filename1\" . \"comment1\") - (\"filename2\" . \"comment2\") - ...)" - (if image-dired-file-comment-list - (if (not (assoc file image-dired-file-comment-list)) - (setcdr image-dired-file-comment-list - (cons (cons file comment) - (cdr image-dired-file-comment-list)))) - (setq image-dired-file-comment-list (list (cons file comment))))) - -(defun image-dired--create-gallery-lists () - "Create temporary lists used by `image-dired-gallery-generate'." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end beg file row-tags) - (setq image-dired-tag-file-list nil) - (setq image-dired-file-tag-list nil) - (setq image-dired-file-comment-list nil) - (goto-char (point-min)) - (while (search-forward-regexp "^." nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (setq beg (point)) - (unless (search-forward ";" end nil) - (error "Something is really wrong, check format of database")) - (setq row-tags (split-string - (buffer-substring beg end) ";")) - (setq file (car row-tags)) - (dolist (x (cdr row-tags)) - (if (not (string-match "^comment:\\(.*\\)" x)) - (image-dired--add-to-tag-file-lists x file) - (image-dired--add-to-file-comment-list file (match-string 1 x))))))) - ;; Sort tag-file list - (setq image-dired-tag-file-list - (sort image-dired-tag-file-list - (lambda (x y) - (string< (car x) (car y)))))) - -(defun image-dired--hidden-p (file) - "Return t if image FILE has a \"hidden\" tag." - (cl-loop for tag in (cdr (assoc file image-dired-file-tag-list)) - if (member tag image-dired-gallery-hidden-tags) return t)) - -(defun image-dired-gallery-generate () - "Generate gallery pages. -First we create a couple of Lisp structures from the database to make -it easier to generate, then HTML-files are created in -`image-dired-gallery-dir'." - (interactive) - (if (eq 'per-directory image-dired-thumbnail-storage) - (error "Currently, gallery generation is not supported \ -when using per-directory thumbnail file storage")) - (image-dired--create-gallery-lists) - (let ((tags image-dired-tag-file-list) - (index-file (format "%s/index.html" image-dired-gallery-dir)) - count tag tag-file - comment file-tags tag-link tag-link-list) - ;; Make sure gallery root exist - (if (file-exists-p image-dired-gallery-dir) - (if (not (file-directory-p image-dired-gallery-dir)) - (error "Variable image-dired-gallery-dir is not a directory")) - ;; FIXME: Should we set umask to 077 here, as we do for thumbnails? - (make-directory image-dired-gallery-dir)) - ;; Open index file - (with-temp-file index-file - (if (file-exists-p index-file) - (insert-file-contents index-file)) - (insert "\n") - (insert " \n") - (insert "

Image-Dired Gallery

\n") - (insert (format "

\n Gallery generated %s\n

\n" - (current-time-string))) - (insert "

Tag index

\n") - (setq count 1) - ;; Pre-generate list of all tag links - (dolist (curr tags) - (setq tag (car curr)) - (when (not (member tag image-dired-gallery-hidden-tags)) - (setq tag-link (format "%s" count tag)) - (if tag-link-list - (setq tag-link-list - (append tag-link-list (list (cons tag tag-link)))) - (setq tag-link-list (list (cons tag tag-link)))) - (setq count (1+ count)))) - (setq count 1) - ;; Main loop where we generated thumbnail pages per tag - (dolist (curr tags) - (setq tag (car curr)) - ;; Don't display hidden tags - (when (not (member tag image-dired-gallery-hidden-tags)) - ;; Insert link to tag page in index - (insert (format " %s
\n" (cdr (assoc tag tag-link-list)))) - ;; Open per-tag file - (setq tag-file (format "%s/%s.html" image-dired-gallery-dir count)) - (with-temp-file tag-file - (if (file-exists-p tag-file) - (insert-file-contents tag-file)) - (erase-buffer) - (insert "\n") - (insert " \n") - (insert "

Index

\n") - (insert (format "

Images with tag "%s"

" tag)) - ;; Main loop for files per tag page - (dolist (file (cdr curr)) - (unless (image-dired-hidden-p file) - ;; Insert thumbnail with link to full image - (insert - (format "\n" - image-dired-gallery-image-root-url - (file-name-nondirectory file) - image-dired-gallery-thumb-image-root-url - (file-name-nondirectory (image-dired-thumb-name file)) file)) - ;; Insert comment, if any - (if (setq comment (cdr (assoc file image-dired-file-comment-list))) - (insert (format "
\n%s
\n" comment)) - (insert "
\n")) - ;; Insert links to other tags, if any - (when (> (length - (setq file-tags (assoc file image-dired-file-tag-list))) 2) - (insert "[ ") - (dolist (extra-tag file-tags) - ;; Only insert if not file name or the main tag - (if (and (not (equal extra-tag tag)) - (not (equal extra-tag file))) - (insert - (format "%s " (cdr (assoc extra-tag tag-link-list)))))) - (insert "]
\n")))) - (insert "

Index

\n") - (insert " \n") - (insert "\n")) - (setq count (1+ count)))) - (insert " \n") - (insert "")))) - - -;;; Tag support - -(defvar image-dired-widget-list nil - "List to keep track of meta data in edit buffer.") - -(declare-function widget-forward "wid-edit" (arg)) - -;;;###autoload -(defun image-dired-dired-edit-comment-and-tags () - "Edit comment and tags of current or marked image files. -Edit comment and tags for all marked image files in an -easy-to-use form." - (interactive) - (setq image-dired-widget-list nil) - ;; Setup buffer. - (let ((files (dired-get-marked-files))) - (pop-to-buffer-same-window "*Image-Dired Edit Meta Data*") - (kill-all-local-variables) - (let ((inhibit-read-only t)) - (erase-buffer)) - (remove-overlays) - ;; Some help for the user. - (widget-insert -"\nEdit comments and tags for each image. Separate multiple tags -with a comma. Move forward between fields using TAB or RET. -Move to the previous field using backtab (S-TAB). Save by -activating the Save button at the bottom of the form or cancel -the operation by activating the Cancel button.\n\n") - ;; Here comes all images and a comment and tag field for each - ;; image. - (let (thumb-file img comment-widget tag-widget) - - (dolist (file files) - - (setq thumb-file (image-dired-thumb-name file) - img (create-image thumb-file)) - - (insert-image img) - (widget-insert "\n\nComment: ") - (setq comment-widget - (widget-create 'editable-field - :size 60 - :format "%v " - :value (or (image-dired-get-comment file) ""))) - (widget-insert "\nTags: ") - (setq tag-widget - (widget-create 'editable-field - :size 60 - :format "%v " - :value (or (mapconcat - #'identity - (image-dired-list-tags file) - ",") ""))) - ;; Save information in all widgets so that we can use it when - ;; the user saves the form. - (setq image-dired-widget-list - (append image-dired-widget-list - (list (list file comment-widget tag-widget)))) - (widget-insert "\n\n"))) - - ;; Footer with Save and Cancel button. - (widget-insert "\n") - (widget-create 'push-button - :notify - (lambda (&rest _ignore) - (image-dired-save-information-from-widgets) - (bury-buffer) - (message "Done")) - "Save") - (widget-insert " ") - (widget-create 'push-button - :notify - (lambda (&rest _ignore) - (bury-buffer) - (message "Operation canceled")) - "Cancel") - (widget-insert "\n") - (use-local-map widget-keymap) - (widget-setup) - ;; Jump to the first widget. - (widget-forward 1))) - -(defun image-dired-save-information-from-widgets () - "Save information found in `image-dired-widget-list'. -Use the information in `image-dired-widget-list' to save comments and -tags to their respective image file. Internal function used by -`image-dired-dired-edit-comment-and-tags'." - (let (file comment tag-string tag-list lst) - (image-dired-write-comments - (mapcar - (lambda (widget) - (setq file (car widget) - comment (widget-value (cadr widget))) - (cons file comment)) - image-dired-widget-list)) - (image-dired-write-tags - (dolist (widget image-dired-widget-list lst) - (setq file (car widget) - tag-string (widget-value (car (cddr widget))) - tag-list (split-string tag-string ",")) - (dolist (tag tag-list) - (push (cons file tag) lst)))))) - - -;;; bookmark.el support - -(declare-function bookmark-make-record-default - "bookmark" (&optional no-file no-context posn)) -(declare-function bookmark-prop-get "bookmark" (bookmark prop)) - -(defun image-dired-bookmark-name () - "Create a default bookmark name for the current EWW buffer." - (file-name-nondirectory - (directory-file-name - (file-name-directory (image-dired-original-file-name))))) - -(defun image-dired-bookmark-make-record () - "Create a bookmark for the current EWW buffer." - `(,(image-dired-bookmark-name) - ,@(bookmark-make-record-default t) - (location . ,(file-name-directory (image-dired-original-file-name))) - (image-dired-file . ,(file-name-nondirectory (image-dired-original-file-name))) - (handler . image-dired-bookmark-jump))) - -;;;###autoload -(defun image-dired-bookmark-jump (bookmark) - "Default bookmark handler for Image-Dired buffers." - ;; User already cached thumbnails, so disable any checking. - (let ((image-dired-show-all-from-dir-max-files nil)) - (image-dired (bookmark-prop-get bookmark 'location)) - ;; TODO: Go to the bookmarked file, if it exists. - ;; (bookmark-prop-get bookmark 'image-dired-file) - (goto-char (point-min)))) - -(put 'image-dired-bookmark-jump 'bookmark-handler-type "Image-Dired") - -;;; Obsolete - -;;;###autoload -(define-obsolete-function-alias 'tumme #'image-dired "24.4") - -;;;###autoload -(define-obsolete-function-alias 'image-dired-setup-dired-keybindings - #'image-dired-minor-mode "26.1") - -(defcustom image-dired-temp-image-file - (expand-file-name ".image-dired_temp" image-dired-dir) - "Name of temporary image file used by various commands." - :type 'file) -(make-obsolete-variable 'image-dired-temp-image-file - "no longer used." "29.1") - -(defcustom image-dired-cmd-create-temp-image-program - (if (executable-find "gm") "gm" "convert") - "Executable used to create temporary image. -Used together with `image-dired-cmd-create-temp-image-options'." - :type 'file - :version "29.1") -(make-obsolete-variable 'image-dired-cmd-create-temp-image-program - "no longer used." "29.1") - -(defcustom image-dired-cmd-create-temp-image-options - (let ((opts '("-size" "%wx%h" "%f[0]" - "-resize" "%wx%h>" - "-strip" "jpeg:%t"))) - (if (executable-find "gm") (cons "convert" opts) opts)) - "Options of command used to create temporary image for display window. -Used together with `image-dired-cmd-create-temp-image-program', -Available format specifiers are: %w and %h which are replaced by -the calculated max size for width and height in the image display window, -%f which is replaced by the file name of the original image and %t which -is replaced by the file name of the temporary file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-create-temp-image-options - "no longer used." "29.1") - -(defcustom image-dired-display-window-width-correction 1 - "Number to be used to correct image display window width. -Change if the default (1) does not work (i.e. if the image does not -completely fit)." - :type 'integer) -(make-obsolete-variable 'image-dired-display-window-width-correction - "no longer used." "29.1") - -(defcustom image-dired-display-window-height-correction 0 - "Number to be used to correct image display window height. -Change if the default (0) does not work (i.e. if the image does not -completely fit)." - :type 'integer) -(make-obsolete-variable 'image-dired-display-window-height-correction - "no longer used." "29.1") - -(defun image-dired-display-window-width (window) - "Return width, in pixels, of WINDOW." - (declare (obsolete nil "29.1")) - (- (image-dired-window-width-pixels window) - image-dired-display-window-width-correction)) - -(defun image-dired-display-window-height (window) - "Return height, in pixels, of WINDOW." - (declare (obsolete nil "29.1")) - (- (image-dired-window-height-pixels window) - image-dired-display-window-height-correction)) - -(defun image-dired-window-height-pixels (window) - "Calculate WINDOW height in pixels." - (declare (obsolete nil "29.1")) - ;; Note: The mode-line consumes one line - (* (- (window-height window) 1) (frame-char-height))) - -(defcustom image-dired-cmd-read-exif-data-program "exiftool" - "Program used to read EXIF data to image. -Used together with `image-dired-cmd-read-exif-data-options'." - :type 'file) -(make-obsolete-variable 'image-dired-cmd-read-exif-data-program - "use `exif-parse-file' and `exif-field' instead." "29.1") - -(defcustom image-dired-cmd-read-exif-data-options '("-s" "-s" "-s" "-%t" "%f") - "Arguments of command used to read EXIF data. -Used with `image-dired-cmd-read-exif-data-program'. -Available format specifiers are: %f which is replaced -by the image file name and %t which is replaced by the tag name." - :version "26.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-read-exif-data-options - "use `exif-parse-file' and `exif-field' instead." "29.1") - -(defun image-dired-get-exif-data (file tag-name) - "From FILE, return EXIF tag TAG-NAME." - (declare (obsolete "use `exif-parse-file' and `exif-field' instead." "29.1")) - (image-dired--check-executable-exists - 'image-dired-cmd-read-exif-data-program) - (let ((buf (get-buffer-create "*image-dired-get-exif-data*")) - (spec (list (cons ?f file) (cons ?t tag-name))) - tag-value) - (with-current-buffer buf - (delete-region (point-min) (point-max)) - (if (not (eq (apply #'call-process image-dired-cmd-read-exif-data-program - nil t nil - (mapcar - (lambda (arg) (format-spec arg spec)) - image-dired-cmd-read-exif-data-options)) - 0)) - (error "Could not get EXIF tag") - (goto-char (point-min)) - ;; Clean buffer from newlines and carriage returns before - ;; getting final info - (while (search-forward-regexp "[\n\r]" nil t) - (replace-match "" nil t)) - (setq tag-value (buffer-substring (point-min) (point-max))))) - tag-value)) - -(defcustom image-dired-cmd-rotate-thumbnail-program - (if (executable-find "gm") "gm" "mogrify") - "Executable used to rotate thumbnail. -Used together with `image-dired-cmd-rotate-thumbnail-options'." - :type 'file - :version "29.1") -(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-program nil "29.1") - -(defcustom image-dired-cmd-rotate-thumbnail-options - (let ((opts '("-rotate" "%d" "%t"))) - (if (executable-find "gm") (cons "mogrify" opts) opts)) - "Arguments of command used to rotate thumbnail image. -Used with `image-dired-cmd-rotate-thumbnail-program'. -Available format specifiers are: %d which is replaced by the -number of (positive) degrees to rotate the image, normally 90 or 270 -\(for 90 degrees right and left), %t which is replaced by the file name -of the thumbnail file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-options nil "29.1") - -(defun image-dired-rotate-thumbnail (degrees) - "Rotate thumbnail DEGREES degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (image-dired--check-executable-exists - 'image-dired-cmd-rotate-thumbnail-program) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (let* ((file (image-dired-thumb-name (image-dired-original-file-name))) - (thumb (expand-file-name file)) - (spec (list (cons ?d degrees) (cons ?t thumb)))) - (apply #'call-process image-dired-cmd-rotate-thumbnail-program nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-rotate-thumbnail-options)) - (clear-image-cache thumb)))) - -(defun image-dired-rotate-thumbnail-left () - "Rotate thumbnail left (counter clockwise) 90 degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (interactive) - (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) - (image-dired-rotate-thumbnail "270"))) - -(defun image-dired-rotate-thumbnail-right () - "Rotate thumbnail counter right (clockwise) 90 degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (interactive) - (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) - (image-dired-rotate-thumbnail "90"))) - -(defun image-dired-modify-mark-on-thumb-original-file (command) - "Modify mark in Dired buffer. -COMMAND is one of `mark' for marking file in Dired, `unmark' for -unmarking file in Dired or `flag' for flagging file for delete in -Dired." - (declare (obsolete image-dired--on-file-in-dired-buffer "29.1")) - (let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (if (not (and dired-buf file-name)) - (message "No image, or image with correct properties, at point") - (with-current-buffer dired-buf - (message "%s" file-name) - (when (dired-goto-file file-name) - (cond ((eq command 'mark) (dired-mark 1)) - ((eq command 'unmark) (dired-unmark 1)) - ((eq command 'toggle) - (if (image-dired-dired-file-marked-p) - (dired-unmark 1) - (dired-mark 1))) - ((eq command 'flag) (dired-flag-file-deletion 1))) - (image-dired-thumb-update-marks)))))) - -(defun image-dired-display-current-image-full () - "Display current image in full size." - (declare (obsolete image-transform-original "29.1")) - (interactive nil image-dired-thumbnail-mode) - (let ((file (image-dired-original-file-name))) - (if file - (progn - (image-dired-display-image file) - (with-current-buffer image-dired-display-image-buffer - (image-transform-original))) - (error "No original file name at point")))) - -(defun image-dired-display-current-image-sized () - "Display current image in sized to fit window dimensions." - (declare (obsolete image-mode-fit-frame "29.1")) - (interactive nil image-dired-thumbnail-mode) - (let ((file (image-dired-original-file-name))) - (if file - (progn - (image-dired-display-image file)) - (error "No original file name at point")))) - -(defun image-dired-add-to-tag-file-list (tag file) - "Add relation between TAG and FILE." - (declare (obsolete nil "29.1")) - (let (curr) - (if image-dired-tag-file-list - (if (setq curr (assoc tag image-dired-tag-file-list)) - (if (not (member file curr)) - (setcdr curr (cons file (cdr curr)))) - (setcdr image-dired-tag-file-list - (cons (list tag file) (cdr image-dired-tag-file-list)))) - (setq image-dired-tag-file-list (list (list tag file)))))) - -(defun image-dired-display-thumb-properties () - "Display thumbnail properties in the echo area." - (declare (obsolete image-dired-update-header-line "29.1")) - (image-dired-update-header-line)) - -(defvar image-dired-slideshow-count 0 - "Keeping track on number of images in slideshow.") -(make-obsolete-variable 'image-dired-slideshow-count "no longer used." "29.1") - -(defvar image-dired-slideshow-times 0 - "Number of pictures to display in slideshow.") -(make-obsolete-variable 'image-dired-slideshow-times "no longer used." "29.1") - -(define-obsolete-function-alias 'image-dired-create-display-image-buffer - #'ignore "29.1") -(define-obsolete-function-alias 'image-dired-create-gallery-lists - #'image-dired--create-gallery-lists "29.1") -(define-obsolete-function-alias 'image-dired-add-to-file-comment-list - #'image-dired--add-to-file-comment-list "29.1") -(define-obsolete-function-alias 'image-dired-add-to-tag-file-lists - #'image-dired--add-to-tag-file-lists "29.1") -(define-obsolete-function-alias 'image-dired-hidden-p - #'image-dired--hidden-p "29.1") - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;;;;;;; TEST-SECTION ;;;;;;;;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; (defvar image-dired-dir-max-size 12300000) - -;; (defun image-dired-test-clean-old-files () -;; "Clean `image-dired-dir' from old thumbnail files. -;; \"Oldness\" measured using last access time. If the total size of all -;; thumbnail files in `image-dired-dir' is larger than 'image-dired-dir-max-size', -;; old files are deleted until the max size is reached." -;; (let* ((files -;; (sort -;; (mapcar -;; (lambda (f) -;; (let ((fattribs (file-attributes f))) -;; `(,(file-attribute-access-time fattribs) -;; ,(file-attribute-size fattribs) ,f))) -;; (directory-files (image-dired-dir) t ".+\\.thumb\\..+$")) -;; ;; Sort function. Compare time between two files. -;; (lambda (l1 l2) -;; (time-less-p (car l1) (car l2))))) -;; (dirsize (apply '+ (mapcar (lambda (x) (cadr x)) files)))) -;; (while (> dirsize image-dired-dir-max-size) -;; (y-or-n-p -;; (format "Size of thumbnail directory: %d, delete old file %s? " -;; dirsize (cadr (cdar files)))) -;; (delete-file (cadr (cdar files))) -;; (setq dirsize (- dirsize (car (cdar files)))) -;; (setq files (cdr files))))) +(provide 'image-dired-dired) -(provide 'image-dired) +;; Local Variables: +;; nameless-current-name: "image-dired" +;; End: -;;; image-dired.el ends here +;;; image-dired-dired.el ends here diff --git a/lisp/image/image-dired-external.el b/lisp/image/image-dired-external.el index 9f12354111..70e00658dc 100644 --- a/lisp/image/image-dired-external.el +++ b/lisp/image/image-dired-external.el @@ -1,10 +1,9 @@ -;;; image-dired.el --- use dired to browse and manipulate your images -*- lexical-binding: t -*- +;;; image-dired-external.el --- External process support for Image-Dired -*- lexical-binding: t -*- ;; Copyright (C) 2005-2022 Free Software Foundation, Inc. -;; Version: 0.4.11 -;; Keywords: multimedia ;; Author: Mathias Dahl +;; Keywords: multimedia ;; This file is part of GNU Emacs. @@ -23,199 +22,29 @@ ;;; Commentary: -;; BACKGROUND -;; ========== -;; -;; I needed a program to browse, organize and tag my pictures. I got -;; tired of the old gallery program I used as it did not allow -;; multi-file operations easily. Also, it put things out of my -;; control. Image viewing programs I tested did not allow multi-file -;; operations or did not do what I wanted it to. -;; -;; So, I got the idea to use the wonderful functionality of Emacs and -;; `dired' to do it. It would allow me to do almost anything I wanted, -;; which is basically just to browse all my pictures in an easy way, -;; letting me manipulate and tag them in various ways. `dired' already -;; provide all the file handling and navigation facilities; I only -;; needed to add some functions to display the images. -;; -;; I briefly tried out thumbs.el, and although it seemed more -;; powerful than this package, it did not work the way I wanted to. It -;; was too slow to create thumbnails of all files in a directory (I -;; currently keep all my 2000+ images in the same directory) and -;; browsing the thumbnail buffer was slow too. image-dired.el will not -;; create thumbnails until they are needed and the browsing is done -;; quickly and easily in Dired. I copied a great deal of ideas and -;; code from there though... :) -;; -;; `image-dired' stores the thumbnail files in `image-dired-dir' -;; using the file name format ORIGNAME.thumb.ORIGEXT. For example -;; ~/.emacs.d/image-dired/myimage01.thumb.jpg. The "database" is for -;; now just a plain text file with the following format: -;; -;; file-name-non-directory;comment:comment-text;tag1;tag2;tag3;...;tagN -;; -;; -;; PREREQUISITES -;; ============= -;; -;; * The GraphicsMagick or ImageMagick package; Image-Dired uses -;; whichever is available. -;; -;; A) For GraphicsMagick, `gm' is used. -;; Find it here: http://www.graphicsmagick.org/ -;; -;; B) For ImageMagick, `convert' and `mogrify' are used. -;; Find it here: https://www.imagemagick.org. -;; -;; * For non-lossy rotation of JPEG images, the JpegTRAN program is -;; needed. -;; -;; * For `image-dired-set-exif-data' to work, the command line tool `exiftool' is -;; needed. It can be found here: https://exiftool.org/. This -;; function is, among other things, used for writing comments to -;; image files using `image-dired-thumbnail-set-image-description'. -;; -;; -;; USAGE -;; ===== -;; -;; This information has been moved to the manual. Type `C-h r' to open -;; the Emacs manual and go to the node Thumbnails by typing `g -;; Image-Dired RET'. -;; -;; Quickstart: M-x image-dired RET DIRNAME RET -;; -;; where DIRNAME is a directory containing image files. -;; -;; LIMITATIONS -;; =========== -;; -;; * Supports all image formats that Emacs and convert supports, but -;; the thumbnails are hard-coded to JPEG or PNG format. It uses -;; JPEG by default, but can optionally follow the Thumbnail Managing -;; Standard (v0.9.0, Dec 2020), which mandates PNG. See the user -;; option `image-dired-thumbnail-storage'. -;; -;; * WARNING: The "database" format used might be changed so keep a -;; backup of `image-dired-db-file' when testing new versions. -;; -;; TODO -;; ==== -;; -;; * Investigate if it is possible to also write the tags to the image -;; files. -;; -;; * From thumbs.el: Add an option for clean-up/max-size functionality -;; for thumbnail directory. -;; -;; * From thumbs.el: Add setroot function. -;; -;; * Add `image-dired-display-thumbs-ring' and functions to cycle that. Find out -;; which is best, saving old batch just before inserting new, or -;; saving the current batch in the ring when inserting it. Adding -;; it probably needs rewriting `image-dired-display-thumbs' to be more general. -;; -;; * Find some way of toggling on and off really nice keybindings in -;; Dired (for example, using C-n or instead of C-S-n). -;; Richard suggested that we could keep C-t as prefix for -;; image-dired commands as it is currently not used in Dired. He -;; also suggested that `dired-next-line' and `dired-previous-line' -;; figure out if image-dired is enabled in the current buffer and, -;; if it is, call `image-dired-dired-next-line' and `image-dired-dired-previous-line', -;; respectively. Update: This is partly done; some bindings have -;; now been added to Dired. -;; -;; * In some way keep track of buffers and windows and stuff so that -;; it works as the user expects. -;; -;; * More/better documentation. - ;;; Code: (require 'dired) (require 'exif) -(require 'image-mode) -(require 'widget) -(require 'xdg) - -(eval-when-compile - (require 'cl-lib) - (require 'wid-edit)) - - -;;; Customizable variables -(defgroup image-dired nil - "Use Dired to browse your images as thumbnails, and more." - :prefix "image-dired-" - :link '(info-link "(emacs) Image-Dired") - :group 'multimedia) +(require 'image-dired-util) -(defcustom image-dired-dir (locate-user-emacs-file "image-dired/") - "Directory where thumbnail images are stored. +(declare-function image-dired-display-image "image-dired") -The value of this option will be ignored if Image-Dired is -customized to use the Thumbnail Managing Standard; they will be -saved in \"$XDG_CACHE_HOME/thumbnails/\" instead. See -`image-dired-thumbnail-storage'." - :type 'directory) - -(defcustom image-dired-thumbnail-storage 'use-image-dired-dir - "How `image-dired' stores thumbnail files. -There are two ways that Image-Dired can store and generate -thumbnails. If you set this variable to one of the two following -values, they will be stored in the JPEG format: - -- `use-image-dired-dir' means that the thumbnails are stored in a - central directory. - -- `per-directory' means that each thumbnail is stored in a - subdirectory called \".image-dired\" in the same directory - where the image file is. - -It can also use the \"Thumbnail Managing Standard\", which allows -sharing of thumbnails across different programs. Thumbnails will -be stored in \"$XDG_CACHE_HOME/thumbnails/\" instead of in -`image-dired-dir'. Thumbnails are saved in the PNG format, and -can be one of the following sizes: - -- `standard' means use thumbnails sized 128x128. -- `standard-large' means use thumbnails sized 256x256. -- `standard-x-large' means use thumbnails sized 512x512. -- `standard-xx-large' means use thumbnails sized 1024x1024. - -For more information on the Thumbnail Managing Standard, see: -https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html" - :type '(choice :tag "How to store thumbnail files" - (const :tag "Use image-dired-dir" use-image-dired-dir) - (const :tag "Thumbnail Managing Standard (normal 128x128)" - standard) - (const :tag "Thumbnail Managing Standard (large 256x256)" - standard-large) - (const :tag "Thumbnail Managing Standard (larger 512x512)" - standard-x-large) - (const :tag "Thumbnail Managing Standard (extra large 1024x1024)" - standard-xx-large) - (const :tag "Per-directory" per-directory)) - :version "29.1") - -(defconst image-dired--thumbnail-standard-sizes - '( standard standard-large - standard-x-large standard-xx-large) - "List of symbols representing thumbnail sizes in Thumbnail Managing Standard.") - -(defcustom image-dired-db-file - (expand-file-name ".image-dired_db" image-dired-dir) - "Database file where file names and their associated tags are stored." - :type 'file) +(defvar image-dired-dir) +(defvar image-dired-main-image-directory) +(defvar image-dired-rotate-original-ask-before-overwrite) +(defvar image-dired-thumb-height) +(defvar image-dired-thumb-width) +(defvar image-dired-thumbnail-storage) (defcustom image-dired-cmd-create-thumbnail-program (if (executable-find "gm") "gm" "convert") "Executable used to create thumbnail. Used together with `image-dired-cmd-create-thumbnail-options'." :type 'file - :version "29.1") + :version "29.1" + :group 'image-dired) (defcustom image-dired-cmd-create-thumbnail-options (let ((opts '("-size" "%wx%h" "%f[0]" @@ -228,6 +57,7 @@ Available format specifiers are: %w which is replaced by `image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', %f which is replaced by the file name of the original image and %t which is replaced by the file name of the thumbnail file." + :group 'image-dired :version "29.1" :type '(repeat (string :tag "Argument"))) @@ -242,6 +72,7 @@ which is replaced by the file name of the thumbnail file." "The file name of the `pngquant' or `pngnq' program. It quantizes colors of PNG images down to 256 colors or fewer using the NeuQuant algorithm." + :group 'image-dired :version "29.1" :type '(choice (const :tag "Not Set" nil) file)) @@ -252,6 +83,7 @@ using the NeuQuant algorithm." "Arguments to pass `image-dired-cmd-pngnq-program'. Available format specifiers are the same as in `image-dired-cmd-create-thumbnail-options'." + :group 'image-dired :type '(repeat (string :tag "Argument")) :version "29.1") @@ -259,6 +91,7 @@ Available format specifiers are the same as in "The file name of the `pngcrush' program. It optimizes the compression of PNG images. Also it adds PNG textual chunks with the information required by the Thumbnail Managing Standard." + :group 'image-dired :type '(choice (const :tag "Not Set" nil) file)) (defcustom image-dired-cmd-pngcrush-options @@ -276,11 +109,13 @@ with the information required by the Thumbnail Managing Standard." Available format specifiers are the same as in `image-dired-cmd-create-thumbnail-options', with %q for a temporary file name (typically generated by pnqnq)." + :group 'image-dired :version "26.1" :type '(repeat (string :tag "Argument"))) (defcustom image-dired-cmd-optipng-program (executable-find "optipng") "The file name of the `optipng' program." + :group 'image-dired :version "26.1" :type '(choice (const :tag "Not Set" nil) file)) @@ -288,6 +123,7 @@ temporary file name (typically generated by pnqnq)." "Arguments passed to `image-dired-cmd-optipng-program'. Available format specifiers are described in `image-dired-cmd-create-thumbnail-options'." + :group 'image-dired :version "26.1" :type '(repeat (string :tag "Argument")) :link '(url-link "man:optipng(1)")) @@ -305,6 +141,7 @@ Available format specifiers are described in "Options for creating thumbnails according to the Thumbnail Managing Standard. Available format specifiers are the same as in `image-dired-cmd-create-thumbnail-options', with %m for file modification time." + :group 'image-dired :version "26.1" :type '(repeat (string :tag "Argument"))) @@ -312,6 +149,7 @@ Available format specifiers are the same as in "jpegtran" "Executable used to rotate original image. Used together with `image-dired-cmd-rotate-original-options'." + :group 'image-dired :type 'file) (defcustom image-dired-cmd-rotate-original-options @@ -323,24 +161,21 @@ number of (positive) degrees to rotate the image, normally 90 or 270 \(for 90 degrees right and left), %o which is replaced by the original image file name and %t which is replaced by `image-dired-temp-image-file'." + :group 'image-dired :version "26.1" :type '(repeat (string :tag "Argument"))) (defcustom image-dired-temp-rotate-image-file - (expand-file-name ".image-dired_rotate_temp" image-dired-dir) + (expand-file-name ".image-dired_rotate_temp" + (locate-user-emacs-file "image-dired/")) "Temporary file for rotate operations." + :group 'image-dired :type 'file) -(defcustom image-dired-rotate-original-ask-before-overwrite t - "Confirm overwrite of original file after rotate operation. -If non-nil, ask user for confirmation before overwriting the -original file with `image-dired-temp-rotate-image-file'." - :type 'boolean) - -(defcustom image-dired-cmd-write-exif-data-program - "exiftool" +(defcustom image-dired-cmd-write-exif-data-program "exiftool" "Program used to write EXIF data to image. Used together with `image-dired-cmd-write-exif-data-options'." + :group 'image-dired :type 'file) (defcustom image-dired-cmd-write-exif-data-options @@ -350,278 +185,13 @@ Used with `image-dired-cmd-write-exif-data-program'. Available format specifiers are: %f which is replaced by the image file name, %t which is replaced by the tag name and %v which is replaced by the tag value." + :group 'image-dired :version "26.1" :type '(repeat (string :tag "Argument"))) -(defcustom image-dired-thumb-size - (cond - ((eq 'standard image-dired-thumbnail-storage) 128) - ((eq 'standard-large image-dired-thumbnail-storage) 256) - ((eq 'standard-x-large image-dired-thumbnail-storage) 512) - ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) - (t 100)) - "Size of thumbnails, in pixels. -This is the default size for both `image-dired-thumb-width' -and `image-dired-thumb-height'. - -The value of this option will be ignored if Image-Dired is -customized to use the Thumbnail Managing Standard; the standard -sizes will be used instead. See `image-dired-thumbnail-storage'." - :type 'integer) - -(defcustom image-dired-thumb-width image-dired-thumb-size - "Width of thumbnails, in pixels." - :type 'integer) - -(defcustom image-dired-thumb-height image-dired-thumb-size - "Height of thumbnails, in pixels." - :type 'integer) - -(defcustom image-dired-thumb-relief 2 - "Size of button-like border around thumbnails." - :type 'integer) - -(defcustom image-dired-thumb-margin 2 - "Size of the margin around thumbnails. -This is where you see the cursor." - :type 'integer) - -(defcustom image-dired-thumb-visible-marks t - "Make marks and flags visible in thumbnail buffer. -If non-nil, apply the `image-dired-thumb-mark' face to marked -images and `image-dired-thumb-flagged' to images flagged for -deletion." - :type 'boolean - :version "28.1") - -(defface image-dired-thumb-mark - '((((class color) (min-colors 16)) :background "DarkOrange") - (((class color)) :foreground "yellow")) - "Face for marked images in thumbnail buffer." - :version "29.1") - -(defface image-dired-thumb-flagged - '((((class color) (min-colors 88) (background light)) :background "Red3") - (((class color) (min-colors 88) (background dark)) :background "Pink") - (((class color) (min-colors 16) (background light)) :background "Red3") - (((class color) (min-colors 16) (background dark)) :background "Pink") - (((class color) (min-colors 8)) :background "red") - (t :inverse-video t)) - "Face for images flagged for deletion in thumbnail buffer." - :version "29.1") - -(defcustom image-dired-line-up-method 'dynamic - "Default method for line-up of thumbnails in thumbnail buffer. -Used by `image-dired-display-thumbs' and other functions that needs -to line-up thumbnails. Dynamic means to use the available width of -the window containing the thumbnail buffer, Fixed means to use -`image-dired-thumbs-per-row', Interactive is for asking the user, -and No line-up means that no automatic line-up will be done." - :type '(choice :tag "Default line-up method" - (const :tag "Dynamic" dynamic) - (const :tag "Fixed" fixed) - (const :tag "Interactive" interactive) - (const :tag "No line-up" none))) - -(defcustom image-dired-thumbs-per-row 3 - "Number of thumbnails to display per row in thumb buffer." - :type 'integer) - -(defcustom image-dired-track-movement t - "The current state of the tracking and mirroring. -For more information, see the documentation for -`image-dired-toggle-movement-tracking'." - :type 'boolean) - -(defcustom image-dired-append-when-browsing nil - "Append thumbnails in thumbnail buffer when browsing. -If non-nil, using `image-dired-next-line-and-display' and -`image-dired-previous-line-and-display' will leave a trail of thumbnail -images in the thumbnail buffer. If you enable this and want to clean -the thumbnail buffer because it is filled with too many thumbnails, -just call `image-dired-display-thumb' to display only the image at point. -This value can be toggled using `image-dired-toggle-append-browsing'." - :type 'boolean) - -(defcustom image-dired-dired-disp-props t - "If non-nil, display properties for Dired file when browsing. -Used by `image-dired-next-line-and-display', -`image-dired-previous-line-and-display' and `image-dired-mark-and-display-next'. -If the database file is large, this can slow down image browsing in -Dired and you might want to turn it off." - :type 'boolean) - -(defcustom image-dired-display-properties-format "%b: %f (%t): %c" - "Display format for thumbnail properties. -%b is replaced with associated Dired buffer name, %f with file -name (without path) of original image file, %t with the list of -tags and %c with the comment." - :type 'string) - -(defcustom image-dired-external-viewer - ;; TODO: Use mailcap, dired-guess-shell-alist-default, - ;; dired-view-command-alist. - (cond ((executable-find "display")) - ((executable-find "xli")) - ((executable-find "qiv") "qiv -t") - ((executable-find "feh") "feh")) - "Name of external viewer. -Including parameters. Used when displaying original image from -`image-dired-thumbnail-mode'." - :version "28.1" - :type '(choice string - (const :tag "Not Set" nil))) - -(defcustom image-dired-main-image-directory - (or (xdg-user-dir "PICTURES") "~/pics/") - "Name of main image directory, if any. -Used by `image-dired-copy-with-exif-file-name'." - :type 'string - :version "29.1") - -(defcustom image-dired-show-all-from-dir-max-files 500 - "Maximum number of files in directory before prompting. - -If there are more image files than this in a selected directory, -the `image-dired-show-all-from-dir' command will ask for -confirmation before creating the thumbnail buffer. If this -variable is nil, it will never ask." - :type '(choice integer - (const :tag "Disable warning" nil)) - :version "29.1") - -(defcustom image-dired-marking-shows-next t - "If non-nil, marking, unmarking or flagging an image shows the next image. - -This affects the following commands: -\\ - `image-dired-flag-thumb-original-file' (bound to \\[image-dired-flag-thumb-original-file]) - `image-dired-mark-thumb-original-file' (bound to \\[image-dired-mark-thumb-original-file]) - `image-dired-unmark-thumb-original-file' (bound to \\[image-dired-unmark-thumb-original-file])" - :type 'boolean - :version "29.1") - ;;; Util functions -(defvar image-dired-debug nil - "Non-nil means enable debug messages.") - -(defun image-dired-debug-message (&rest args) - "Display debug message ARGS when `image-dired-debug' is non-nil." - (when image-dired-debug - (apply #'message args))) - -(defmacro image-dired--with-db-file (&rest body) - "Run BODY in a temp buffer containing `image-dired-db-file'. -Return the last form in BODY." - (declare (indent 0) (debug t)) - `(with-temp-buffer - (if (file-exists-p image-dired-db-file) - (insert-file-contents image-dired-db-file)) - ,@body)) - -(defun image-dired-dir () - "Return the current thumbnail directory (from variable `image-dired-dir'). -Create the thumbnail directory if it does not exist." - (let ((image-dired-dir (file-name-as-directory - (expand-file-name image-dired-dir)))) - (unless (file-directory-p image-dired-dir) - (with-file-modes #o700 - (make-directory image-dired-dir t)) - (message "Thumbnail directory created: %s" image-dired-dir)) - image-dired-dir)) - -(defun image-dired-insert-image (file type relief margin) - "Insert image FILE of image TYPE, using RELIEF and MARGIN, at point." - (let ((i `(image :type ,type - :file ,file - :relief ,relief - :margin ,margin))) - (insert-image i))) - -(defun image-dired-get-thumbnail-image (file) - "Return the image descriptor for a thumbnail of image file FILE." - (unless (string-match-p (image-file-name-regexp) file) - (error "%s is not a valid image file" file)) - (let* ((thumb-file (image-dired-thumb-name file)) - (thumb-attr (file-attributes thumb-file))) - (when (or (not thumb-attr) - (time-less-p (file-attribute-modification-time thumb-attr) - (file-attribute-modification-time - (file-attributes file)))) - (image-dired-create-thumb file thumb-file)) - (create-image thumb-file))) - -(defun image-dired-insert-thumbnail (file original-file-name - associated-dired-buffer) - "Insert thumbnail image FILE. -Add text properties ORIGINAL-FILE-NAME and ASSOCIATED-DIRED-BUFFER." - (let (beg end) - (setq beg (point)) - (image-dired-insert-image - file - ;; Thumbnails are created asynchronously, so we might not yet - ;; have a file. But if it exists, it might have been cached from - ;; before and we should use it instead of our current settings. - (or (and (file-exists-p file) - (image-type-from-file-header file)) - (and (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - 'png) - 'jpeg) - image-dired-thumb-relief - image-dired-thumb-margin) - (setq end (point)) - (add-text-properties - beg end - (list 'image-dired-thumbnail t - 'original-file-name original-file-name - 'associated-dired-buffer associated-dired-buffer - 'tags (image-dired-list-tags original-file-name) - 'mouse-face 'highlight - 'comment (image-dired-get-comment original-file-name))))) - -(defun image-dired-thumb-name (file) - "Return absolute file name for thumbnail FILE. -Depending on the value of `image-dired-thumbnail-storage', the -file name of the thumbnail will vary: -- For `use-image-dired-dir', make a SHA1-hash of the image file's - directory name and add that to make the thumbnail file name - unique. -- For `per-directory' storage, just add a subdirectory. -- For `standard' storage, produce the file name according to the - Thumbnail Managing Standard. Among other things, an MD5-hash - of the image file's directory name will be added to the - filename. -See also `image-dired-thumbnail-storage'." - (cond ((memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - (let ((thumbdir (cl-case image-dired-thumbnail-storage - (standard "thumbnails/normal") - (standard-large "thumbnails/large") - (standard-x-large "thumbnails/x-large") - (standard-xx-large "thumbnails/xx-large")))) - (expand-file-name - ;; MD5 is mandated by the Thumbnail Managing Standard. - (concat (md5 (concat "file://" (expand-file-name file))) ".png") - (expand-file-name thumbdir (xdg-cache-home))))) - ((eq 'use-image-dired-dir image-dired-thumbnail-storage) - (let* ((f (expand-file-name file)) - (hash - (md5 (file-name-as-directory (file-name-directory f))))) - (format "%s%s%s.thumb.%s" - (file-name-as-directory (expand-file-name (image-dired-dir))) - (file-name-base f) - (if hash (concat "_" hash) "") - (file-name-extension f)))) - ((eq 'per-directory image-dired-thumbnail-storage) - (let ((f (expand-file-name file))) - (format "%s.image-dired/%s.thumb.%s" - (file-name-directory f) - (file-name-base f) - (file-name-extension f)))))) - (defun image-dired--check-executable-exists (executable) (unless (executable-find (symbol-value executable)) (error "Executable %S not found" executable))) @@ -810,1207 +380,6 @@ The new file will be named THUMBNAIL-FILE." (list (list original-file thumbnail-file)))) (run-at-time 0 nil #'image-dired-thumb-queue-run)) -(defmacro image-dired--with-marked (&rest body) - "Eval BODY with point on each marked thumbnail. -If no marked file could be found, execute BODY on the current -thumbnail." - `(with-current-buffer image-dired-thumbnail-buffer - (let (found) - (save-mark-and-excursion - (goto-char (point-min)) - (while (not (eobp)) - (when (image-dired-thumb-file-marked-p) - (setq found t) - ,@body) - (forward-char))) - (unless found - ,@body)))) - -;;;###autoload -(defun image-dired-dired-toggle-marked-thumbs (&optional arg) - "Toggle thumbnails in front of file names in the Dired buffer. -If no marked file could be found, insert or hide thumbnails on the -current line. ARG, if non-nil, specifies the files to use instead -of the marked files. If ARG is an integer, use the next ARG (or -previous -ARG, if ARG<0) files." - (interactive "P") - (dired-map-over-marks - (let ((image-pos (dired-move-to-filename)) - (image-file (dired-get-filename nil t)) - thumb-file - overlay) - (when (and image-file - (string-match-p (image-file-name-regexp) image-file)) - (setq thumb-file (image-dired-get-thumbnail-image image-file)) - ;; If image is not already added, then add it. - (let ((thumb-ov (cl-loop for ov in (overlays-in (point) (1+ (point))) - if (overlay-get ov 'thumb-file) return ov))) - (if thumb-ov - (delete-overlay thumb-ov) - (put-image thumb-file image-pos) - (setq overlay - (cl-loop for ov in (overlays-in (point) (1+ (point))) - if (overlay-get ov 'put-image) return ov)) - (overlay-put overlay 'image-file image-file) - (overlay-put overlay 'thumb-file thumb-file))))) - arg ; Show or hide image on ARG next files. - 'show-progress) ; Update dired display after each image is updated. - (add-hook 'dired-after-readin-hook - 'image-dired-dired-after-readin-hook nil t)) - -(defun image-dired-dired-after-readin-hook () - "Relocate existing thumbnail overlays in Dired buffer after reverting. -Move them to their corresponding files if they still exist. -Otherwise, delete overlays." - (mapc (lambda (overlay) - (when (overlay-get overlay 'put-image) - (let* ((image-file (overlay-get overlay 'image-file)) - (image-pos (dired-goto-file image-file))) - (if image-pos - (move-overlay overlay image-pos image-pos) - (delete-overlay overlay))))) - (overlays-in (point-min) (point-max)))) - -(defun image-dired-next-line-and-display () - "Move to next Dired line and display thumbnail image." - (interactive) - (dired-next-line 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-previous-line-and-display () - "Move to previous Dired line and display thumbnail image." - (interactive) - (dired-previous-line 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-toggle-append-browsing () - "Toggle `image-dired-append-when-browsing'." - (interactive) - (setq image-dired-append-when-browsing - (not image-dired-append-when-browsing)) - (message "Append browsing %s" - (if image-dired-append-when-browsing - "on" - "off"))) - -(defun image-dired-mark-and-display-next () - "Mark current file in Dired and display next thumbnail image." - (interactive) - (dired-mark 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-toggle-dired-display-properties () - "Toggle `image-dired-dired-disp-props'." - (interactive) - (setq image-dired-dired-disp-props - (not image-dired-dired-disp-props)) - (message "Dired display properties %s" - (if image-dired-dired-disp-props - "on" - "off"))) - -(defvar image-dired-thumbnail-buffer "*image-dired*" - "Image-Dired's thumbnail buffer.") - -(defun image-dired-create-thumbnail-buffer () - "Create thumb buffer and set `image-dired-thumbnail-mode'." - (let ((buf (get-buffer-create image-dired-thumbnail-buffer))) - (with-current-buffer buf - (setq buffer-read-only t) - (if (not (eq major-mode 'image-dired-thumbnail-mode)) - (image-dired-thumbnail-mode))) - buf)) - -(defvar image-dired-display-image-buffer "*image-dired-display-image*" - "Where larger versions of the images are display.") - -(defvar image-dired-saved-window-configuration nil - "Saved window configuration.") - -;;;###autoload -(defun image-dired-dired-with-window-configuration (dir &optional arg) - "Open directory DIR and create a default window configuration. - -Convenience command that: - - - Opens Dired in folder DIR - - Splits windows in most useful (?) way - - Sets `truncate-lines' to t - -After the command has finished, you would typically mark some -image files in Dired and type -\\[image-dired-display-thumbs] (`image-dired-display-thumbs'). - -If called with prefix argument ARG, skip splitting of windows. - -The current window configuration is saved and can be restored by -calling `image-dired-restore-window-configuration'." - (interactive "DDirectory: \nP") - (let ((buf (image-dired-create-thumbnail-buffer)) - (buf2 (get-buffer-create image-dired-display-image-buffer))) - (setq image-dired-saved-window-configuration - (current-window-configuration)) - (dired dir) - (delete-other-windows) - (when (not arg) - (split-window-right) - (setq truncate-lines t) - (save-excursion - (other-window 1) - (pop-to-buffer-same-window buf) - (select-window (split-window-below)) - (pop-to-buffer-same-window buf2) - (other-window -2))))) - -(defun image-dired-restore-window-configuration () - "Restore window configuration. -Restore any changes to the window configuration made by calling -`image-dired-dired-with-window-configuration'." - (interactive nil image-dired-thumbnail-mode) - (if image-dired-saved-window-configuration - (set-window-configuration image-dired-saved-window-configuration) - (message "No saved window configuration"))) - -(defun image-dired--line-up-with-method () - "Line up thumbnails according to `image-dired-line-up-method'." - (cond ((eq 'dynamic image-dired-line-up-method) - (image-dired-line-up-dynamic)) - ((eq 'fixed image-dired-line-up-method) - (image-dired-line-up)) - ((eq 'interactive image-dired-line-up-method) - (image-dired-line-up-interactive)) - ((eq 'none image-dired-line-up-method) - nil) - (t - (image-dired-line-up-dynamic)))) - -;;;###autoload -(defun image-dired-display-thumbs (&optional arg append do-not-pop) - "Display thumbnails of all marked files, in `image-dired-thumbnail-buffer'. -If a thumbnail image does not exist for a file, it is created on the -fly. With prefix argument ARG, display only thumbnail for file at -point (this is useful if you have marked some files but want to show -another one). - -Recommended usage is to split the current frame horizontally so that -you have the Dired buffer in the left window and the -`image-dired-thumbnail-buffer' buffer in the right window. - -With optional argument APPEND, append thumbnail to thumbnail buffer -instead of erasing it first. - -Optional argument DO-NOT-POP controls if `pop-to-buffer' should be -used or not. If non-nil, use `display-buffer' instead of -`pop-to-buffer'. This is used from functions like -`image-dired-next-line-and-display' and -`image-dired-previous-line-and-display' where we do not want the -thumbnail buffer to be selected." - (interactive "P") - (setq image-dired--generate-thumbs-start (current-time)) - (let ((buf (image-dired-create-thumbnail-buffer)) - thumb-name files dired-buf) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (setq dired-buf (current-buffer)) - (with-current-buffer buf - (let ((inhibit-read-only t)) - (if (not append) - (erase-buffer) - (goto-char (point-max))) - (dolist (curr-file files) - (setq thumb-name (image-dired-thumb-name curr-file)) - (when (not (file-exists-p thumb-name)) - (image-dired-create-thumb curr-file thumb-name)) - (image-dired-insert-thumbnail thumb-name curr-file dired-buf))) - (if do-not-pop - (display-buffer buf) - (pop-to-buffer buf)) - (image-dired--line-up-with-method)))) - -;;;###autoload -(defun image-dired-show-all-from-dir (dir) - "Make a thumbnail buffer for all images in DIR and display it. -Any file matching `image-file-name-regexp' is considered an image -file. - -If the number of image files in DIR exceeds -`image-dired-show-all-from-dir-max-files', ask for confirmation -before creating the thumbnail buffer. If that variable is nil, -never ask for confirmation." - (interactive "DImage-Dired: ") - (dired dir) - (dired-mark-files-regexp (image-file-name-regexp)) - (let ((files (dired-get-marked-files nil nil nil t))) - (cond ((and (null (cdr files))) - (message "No image files in directory")) - ((or (not image-dired-show-all-from-dir-max-files) - (<= (length (cdr files)) image-dired-show-all-from-dir-max-files) - (and (> (length (cdr files)) image-dired-show-all-from-dir-max-files) - (y-or-n-p - (format - "Directory contains more than %d image files. Proceed?" - image-dired-show-all-from-dir-max-files)))) - (image-dired-display-thumbs) - (pop-to-buffer image-dired-thumbnail-buffer) - (setq default-directory dir) - (image-dired-unmark-all-marks)) - (t (message "Image-Dired canceled"))))) - -;;;###autoload -(defalias 'image-dired 'image-dired-show-all-from-dir) - - -;;; Tags - -(defun image-dired-sane-db-file () - "Check if `image-dired-db-file' exists. -If not, try to create it (including any parent directories). -Signal error if there are problems creating it." - (or (file-exists-p image-dired-db-file) - (let (dir buf) - (unless (file-directory-p (setq dir (file-name-directory - image-dired-db-file))) - (with-file-modes #o700 - (make-directory dir t))) - (with-current-buffer (setq buf (create-file-buffer - image-dired-db-file)) - (with-file-modes #o600 - (write-file image-dired-db-file))) - (kill-buffer buf) - (file-exists-p image-dired-db-file)) - (error "Could not create %s" image-dired-db-file))) - -(defvar image-dired-tag-history nil "Variable holding the tag history.") - -(defun image-dired-write-tags (file-tags) - "Write file tags to database. -Write each file and tag in FILE-TAGS to the database. -FILE-TAGS is an alist in the following form: - ((FILE . TAG) ... )" - (image-dired-sane-db-file) - (let (end file tag) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (dolist (elt file-tags) - (setq file (car elt) - tag (cdr elt)) - (goto-char (point-min)) - (if (search-forward-regexp (format "^%s.*$" file) nil t) - (progn - (setq end (point)) - (beginning-of-line) - (when (not (search-forward (format ";%s" tag) end t)) - (end-of-line) - (insert (format ";%s" tag)))) - (goto-char (point-max)) - (insert (format "%s;%s\n" file tag)))) - (save-buffer)))) - -(defun image-dired-remove-tag (files tag) - "For all FILES, remove TAG from the image database." - (image-dired-sane-db-file) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (let (end) - (unless (listp files) - (if (stringp files) - (setq files (list files)) - (error "Files must be a string or a list of strings!"))) - (dolist (file files) - (goto-char (point-min)) - (when (search-forward-regexp (format "^%s;" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (search-forward-regexp - (format "\\(;%s\\)\\($\\|;\\)" tag) end t) - (delete-region (match-beginning 1) (match-end 1)) - ;; Check if file should still be in the database. If - ;; it has no tags or comments, it will be removed. - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (not (search-forward ";" end t)) - (kill-line 1)))))) - (save-buffer))) - -(defun image-dired-list-tags (file) - "Read all tags for image FILE from the image database." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end (tags "")) - (when (search-forward-regexp (format "^%s" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (if (search-forward ";" end t) - (if (search-forward "comment:" end t) - (if (search-forward ";" end t) - (setq tags (buffer-substring (point) end))) - (setq tags (buffer-substring (point) end))))) - (split-string tags ";")))) - -;;;###autoload -(defun image-dired-tag-files (arg) - "Tag marked file(s) in Dired. With prefix ARG, tag file at point." - (interactive "P") - (let ((tag (completing-read - "Tags to add (separate tags with a semicolon): " - image-dired-tag-history nil nil nil 'image-dired-tag-history)) - files) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (image-dired-write-tags - (mapcar - (lambda (x) - (cons x tag)) - files)))) - -(defun image-dired-tag-thumbnail () - "Tag current or marked thumbnails." - (interactive) - (let ((tag (completing-read - "Tags to add (separate tags with a semicolon): " - image-dired-tag-history nil nil nil 'image-dired-tag-history))) - (image-dired--with-marked - (image-dired-write-tags - (list (cons (image-dired-original-file-name) tag))) - (image-dired-update-property - 'tags (image-dired-list-tags (image-dired-original-file-name)))))) - -;;;###autoload -(defun image-dired-delete-tag (arg) - "Remove tag for selected file(s). -With prefix argument ARG, remove tag from file at point." - (interactive "P") - (let ((tag (completing-read "Tag to remove: " image-dired-tag-history - nil nil nil 'image-dired-tag-history)) - files) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (image-dired-remove-tag files tag))) - -(defun image-dired-tag-thumbnail-remove () - "Remove tag from current or marked thumbnails." - (interactive) - (let ((tag (completing-read "Tag to remove: " image-dired-tag-history - nil nil nil 'image-dired-tag-history))) - (image-dired--with-marked - (image-dired-remove-tag (image-dired-original-file-name) tag) - (image-dired-update-property - 'tags (image-dired-list-tags (image-dired-original-file-name)))))) - - -;;; Thumbnail mode (cont.) - -(defun image-dired-original-file-name () - "Get original file name for thumbnail or display image at point." - (get-text-property (point) 'original-file-name)) - -(defun image-dired-file-name-at-point () - "Get abbreviated file name for thumbnail or display image at point." - (let ((f (image-dired-original-file-name))) - (when f - (abbreviate-file-name f)))) - -(defun image-dired-associated-dired-buffer () - "Get associated Dired buffer at point." - (get-text-property (point) 'associated-dired-buffer)) - -(defun image-dired-get-buffer-window (buf) - "Return window where buffer BUF is." - (get-window-with-predicate - (lambda (window) - (equal (window-buffer window) buf)) - nil t)) - -(defun image-dired-track-original-file () - "Track the original file in the associated Dired buffer. -See documentation for `image-dired-toggle-movement-tracking'. -Interactive use only useful if `image-dired-track-movement' is nil." - (interactive) - (let* ((dired-buf (image-dired-associated-dired-buffer)) - (file-name (image-dired-original-file-name)) - (window (image-dired-get-buffer-window dired-buf))) - (and (buffer-live-p dired-buf) file-name - (with-current-buffer dired-buf - (if (not (dired-goto-file file-name)) - (message "Could not track file") - (if window (set-window-point window (point)))))))) - -(defun image-dired-toggle-movement-tracking () - "Turn on and off `image-dired-track-movement'. -Tracking of the movements between thumbnail and Dired buffer so that -they are \"mirrored\" in the dired buffer. When this is on, moving -around in the thumbnail or dired buffer will find the matching -position in the other buffer." - (interactive) - (setq image-dired-track-movement (not image-dired-track-movement)) - (message "Movement tracking %s" (if image-dired-track-movement "on" "off"))) - -(defun image-dired-track-thumbnail () - "Track current Dired file's thumb in `image-dired-thumbnail-buffer'. -This is almost the same as what `image-dired-track-original-file' does, -but the other way around." - (let ((file (dired-get-filename)) - prop-val found window) - (when (get-buffer image-dired-thumbnail-buffer) - (with-current-buffer image-dired-thumbnail-buffer - (goto-char (point-min)) - (while (and (not (eobp)) - (not found)) - (if (and (setq prop-val - (get-text-property (point) 'original-file-name)) - (string= prop-val file)) - (setq found t)) - (if (not found) - (forward-char 1))) - (when found - (if (setq window (image-dired-thumbnail-window)) - (set-window-point window (point))) - (image-dired-update-header-line)))))) - -(defun image-dired-dired-next-line (&optional arg) - "Call `dired-next-line', then track thumbnail. -This can safely replace `dired-next-line'. -With prefix argument, move ARG lines." - (interactive "P") - (dired-next-line (or arg 1)) - (if image-dired-track-movement - (image-dired-track-thumbnail))) - -(defun image-dired-dired-previous-line (&optional arg) - "Call `dired-previous-line', then track thumbnail. -This can safely replace `dired-previous-line'. -With prefix argument, move ARG lines." - (interactive "P") - (dired-previous-line (or arg 1)) - (if image-dired-track-movement - (image-dired-track-thumbnail))) - -(defun image-dired--display-thumb-properties-fun () - (let ((old-buf (current-buffer)) - (old-point (point))) - (lambda () - (when (and (equal (current-buffer) old-buf) - (= (point) old-point)) - (ignore-errors - (image-dired-update-header-line)))))) - -(defun image-dired-forward-image (&optional arg wrap-around) - "Move to next image and display properties. -Optional prefix ARG says how many images to move; the default is -one image. Negative means move backwards. -On reaching end or beginning of buffer, stop and show a message. - -If optional argument WRAP-AROUND is non-nil, wrap around: if -point is on the last image, move to the last one and vice versa." - (interactive "p") - (setq arg (or arg 1)) - (let (pos) - (dotimes (_ (abs arg)) - (if (and (not (if (> arg 0) (eobp) (bobp))) - (save-excursion - (forward-char (if (> arg 0) 1 -1)) - (while (and (not (if (> arg 0) (eobp) (bobp))) - (not (image-dired-image-at-point-p))) - (forward-char (if (> arg 0) 1 -1))) - (setq pos (point)) - (image-dired-image-at-point-p))) - (progn (goto-char pos) - (image-dired-update-header-line)) - (if wrap-around - (progn (goto-char (if (> arg 0) - (point-min) - ;; There are two spaces after the last image. - (- (point-max) 2))) - (image-dired-update-header-line)) - (message "At %s image" (if (> arg 0) "last" "first")) - (run-at-time 1 nil (image-dired--display-thumb-properties-fun)))))) - (when image-dired-track-movement - (image-dired-track-original-file))) - -(defun image-dired-backward-image (&optional arg) - "Move to previous image and display properties. -Optional prefix ARG says how many images to move; the default is -one image. Negative means move forward. -On reaching end or beginning of buffer, stop and show a message." - (interactive "p") - (image-dired-forward-image (- (or arg 1)))) - -(defun image-dired-next-line () - "Move to next line and display properties." - (interactive nil image-dired-thumbnail-mode) - (let ((goal-column (current-column))) - (forward-line 1) - (move-to-column goal-column)) - ;; If we end up in an empty spot, back up to the next thumbnail. - (if (not (image-dired-image-at-point-p)) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - - -(defun image-dired-previous-line () - "Move to previous line and display properties." - (interactive nil image-dired-thumbnail-mode) - (let ((goal-column (current-column))) - (forward-line -1) - (move-to-column goal-column)) - ;; If we end up in an empty spot, back up to the next - ;; thumbnail. This should only happen if the user deleted a - ;; thumbnail and did not refresh, so it is not very common. But we - ;; can handle it in a good manner, so why not? - (if (not (image-dired-image-at-point-p)) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-beginning-of-buffer () - "Move to the first image in the buffer and display properties." - (interactive nil image-dired-thumbnail-mode) - (goto-char (point-min)) - (while (and (not (image-at-point-p)) - (not (eobp))) - (forward-char 1)) - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-end-of-buffer () - "Move to the last image in the buffer and display properties." - (interactive nil image-dired-thumbnail-mode) - (goto-char (point-max)) - (while (and (not (image-at-point-p)) - (not (bobp))) - (forward-char -1)) - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-format-properties-string (buf file props comment) - "Format display properties. -BUF is the associated Dired buffer, FILE is the original image file -name, PROPS is a stringified list of tags and COMMENT is the image file's -comment." - (format-spec - image-dired-display-properties-format - (list - (cons ?b (or buf "")) - (cons ?f file) - (cons ?t (or props "")) - (cons ?c (or comment ""))))) - -(defun image-dired-update-header-line () - "Update image information in the header line." - (when (and (not (eobp)) - (memq major-mode '(image-dired-thumbnail-mode - image-dired-display-image-mode))) - (let ((file-name (file-name-nondirectory (image-dired-original-file-name))) - (dired-buf (buffer-name (image-dired-associated-dired-buffer))) - (props (mapconcat #'identity (get-text-property (point) 'tags) ", ")) - (comment (get-text-property (point) 'comment)) - (message-log-max nil)) - (if file-name - (setq header-line-format - (image-dired-format-properties-string - dired-buf - file-name - props - comment)))))) - -(defun image-dired-dired-file-marked-p (&optional marker) - "In Dired, return t if file on current line is marked. -If optional argument MARKER is non-nil, it is a character to look -for. The default is to look for `dired-marker-char'." - (setq marker (or marker dired-marker-char)) - (save-excursion - (beginning-of-line) - (and (looking-at dired-re-mark) - (= (aref (match-string 0) 0) marker)))) - -(defun image-dired-dired-file-flagged-p () - "In Dired, return t if file on current line is flagged for deletion." - (image-dired-dired-file-marked-p dired-del-marker)) - -(defmacro image-dired--with-thumbnail-buffer (&rest body) - (declare (indent defun) (debug t)) - `(if-let ((buf (get-buffer image-dired-thumbnail-buffer))) - (with-current-buffer buf - (if-let ((win (get-buffer-window buf))) - (with-selected-window win - ,@body) - ,@body)) - (user-error "No such buffer: %s" image-dired-thumbnail-buffer))) - -(defmacro image-dired--on-file-in-dired-buffer (&rest body) - "Run BODY with point on file at point in Dired buffer. -Should be called from commands in `image-dired-thumbnail-mode'." - (declare (indent defun) (debug t)) - `(let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (if (not (and dired-buf file-name)) - (message "No image, or image with correct properties, at point") - (with-current-buffer dired-buf - (when (dired-goto-file file-name) - ,@body - (image-dired-thumb-update-marks)))))) - -(defmacro image-dired--do-mark-command (maybe-next &rest body) - "Helper macro for the mark, unmark and flag commands. -Run BODY in Dired buffer. -If optional argument MAYBE-NEXT is non-nil, show next image -according to `image-dired-marking-shows-next'." - (declare (indent defun) (debug t)) - `(image-dired--with-thumbnail-buffer - (image-dired--on-file-in-dired-buffer - ,@body) - ,(when maybe-next - '(if image-dired-marking-shows-next - (image-dired-display-next-thumbnail-original) - (image-dired-next-line))))) - -(defun image-dired-mark-thumb-original-file () - "Mark original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-mark 1))) - -(defun image-dired-unmark-thumb-original-file () - "Unmark original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-unmark 1))) - -(defun image-dired-flag-thumb-original-file () - "Flag original image file for deletion in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-flag-file-deletion 1))) - -(defun image-dired-toggle-mark-thumb-original-file () - "Toggle mark on original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command nil - (if (image-dired-dired-file-marked-p) - (dired-unmark 1) - (dired-mark 1)))) - -(defun image-dired-unmark-all-marks () - "Remove all marks from all files in associated Dired buffer. -Also update the marks in the thumbnail buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command nil - (dired-unmark-all-marks)) - (image-dired--with-thumbnail-buffer - (image-dired-thumb-update-marks))) - -(defun image-dired-jump-original-dired-buffer () - "Jump to the Dired buffer associated with the current image file. -You probably want to use this together with -`image-dired-track-original-file'." - (interactive nil image-dired-thumbnail-mode) - (let ((buf (image-dired-associated-dired-buffer)) - window frame) - (setq window (image-dired-get-buffer-window buf)) - (if window - (progn - (if (not (equal (selected-frame) (setq frame (window-frame window)))) - (select-frame-set-input-focus frame)) - (select-window window)) - (message "Associated dired buffer not visible")))) - -;;;###autoload -(defun image-dired-jump-thumbnail-buffer () - "Jump to thumbnail buffer." - (interactive) - (let ((window (image-dired-thumbnail-window)) - frame) - (if window - (progn - (if (not (equal (selected-frame) (setq frame (window-frame window)))) - (select-frame-set-input-focus frame)) - (select-window window)) - (message "Thumbnail buffer not visible")))) - -(defvar image-dired-thumbnail-mode-line-up-map - (let ((map (make-sparse-keymap))) - ;; map it to "g" so that the user can press it more quickly - (define-key map "g" #'image-dired-line-up-dynamic) - ;; "f" for "fixed" number of thumbs per row - (define-key map "f" #'image-dired-line-up) - ;; "i" for "interactive" - (define-key map "i" #'image-dired-line-up-interactive) - map) - "Keymap for line-up commands in `image-dired-thumbnail-mode'.") - -(defvar image-dired-thumbnail-mode-tag-map - (let ((map (make-sparse-keymap))) - ;; map it to "t" so that the user can press it more quickly - (define-key map "t" #'image-dired-tag-thumbnail) - ;; "r" for "remove" - (define-key map "r" #'image-dired-tag-thumbnail-remove) - map) - "Keymap for tag commands in `image-dired-thumbnail-mode'.") - -(defvar image-dired-thumbnail-mode-map - (let ((map (make-sparse-keymap))) - (define-key map [right] #'image-dired-forward-image) - (define-key map [left] #'image-dired-backward-image) - (define-key map [up] #'image-dired-previous-line) - (define-key map [down] #'image-dired-next-line) - (define-key map "\C-f" #'image-dired-forward-image) - (define-key map "\C-b" #'image-dired-backward-image) - (define-key map "\C-p" #'image-dired-previous-line) - (define-key map "\C-n" #'image-dired-next-line) - - (define-key map "<" #'image-dired-beginning-of-buffer) - (define-key map ">" #'image-dired-end-of-buffer) - (define-key map (kbd "M-<") #'image-dired-beginning-of-buffer) - (define-key map (kbd "M->") #'image-dired-end-of-buffer) - - (define-key map "d" #'image-dired-flag-thumb-original-file) - (define-key map [delete] #'image-dired-flag-thumb-original-file) - (define-key map "m" #'image-dired-mark-thumb-original-file) - (define-key map "u" #'image-dired-unmark-thumb-original-file) - (define-key map "U" #'image-dired-unmark-all-marks) - (define-key map "." #'image-dired-track-original-file) - (define-key map [tab] #'image-dired-jump-original-dired-buffer) - - ;; add line-up map - (define-key map "g" image-dired-thumbnail-mode-line-up-map) - ;; add tag map - (define-key map "t" image-dired-thumbnail-mode-tag-map) - - (define-key map "\C-m" #'image-dired-display-thumbnail-original-image) - (define-key map [C-return] #'image-dired-thumbnail-display-external) - - (define-key map "L" #'image-dired-rotate-original-left) - (define-key map "R" #'image-dired-rotate-original-right) - - (define-key map "D" #'image-dired-thumbnail-set-image-description) - (define-key map "S" #'image-dired-slideshow-start) - (define-key map "\C-d" #'image-dired-delete-char) - (define-key map " " #'image-dired-display-next-thumbnail-original) - (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) - (define-key map "c" #'image-dired-comment-thumbnail) - - ;; Mouse - (define-key map [mouse-2] #'image-dired-mouse-display-image) - (define-key map [mouse-1] #'image-dired-mouse-select-thumbnail) - (define-key map [mouse-3] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-1] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-2] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-3] #'image-dired-mouse-select-thumbnail) - ;; Seems I must first set C-down-mouse-1 to undefined, or else it - ;; will trigger the buffer menu. If I try to instead bind - ;; C-down-mouse-1 to `image-dired-mouse-toggle-mark', I get a message - ;; about C-mouse-1 not being defined afterwards. Annoying, but I - ;; probably do not completely understand mouse events. - (define-key map [C-down-mouse-1] #'undefined) - (define-key map [C-mouse-1] #'image-dired-mouse-toggle-mark) - map) - "Keymap for `image-dired-thumbnail-mode'.") - -(easy-menu-define image-dired-thumbnail-mode-menu image-dired-thumbnail-mode-map - "Menu for `image-dired-thumbnail-mode'." - '("Image-Dired" - ["Display image" image-dired-display-thumbnail-original-image] - ["Display in external viewer" image-dired-thumbnail-display-external] - ["Jump to Dired buffer" image-dired-jump-original-dired-buffer] - "---" - ["Mark image" image-dired-mark-thumb-original-file] - ["Unmark image" image-dired-unmark-thumb-original-file] - ["Unmark all images" image-dired-unmark-all-marks] - ["Flag for deletion" image-dired-flag-thumb-original-file] - ["Delete marked images" image-dired-delete-marked] - "---" - ["Rotate original right" image-dired-rotate-original-right] - ["Rotate original left" image-dired-rotate-original-left] - "---" - ["Comment thumbnail" image-dired-comment-thumbnail] - ["Tag current or marked thumbnails" image-dired-tag-thumbnail] - ["Remove tag from current or marked thumbnails" - image-dired-tag-thumbnail-remove] - ["Start slideshow" image-dired-slideshow-start] - "---" - ("View Options" - ["Toggle movement tracking" image-dired-toggle-movement-tracking - :style toggle - :selected image-dired-track-movement] - "---" - ["Line up thumbnails" image-dired-line-up] - ["Dynamic line up" image-dired-line-up-dynamic] - ["Refresh thumb" image-dired-refresh-thumb]) - ["Quit" quit-window])) - -(defvar image-dired-display-image-mode-map - (let ((map (make-sparse-keymap))) - (define-key map "S" #'image-dired-slideshow-start) - (define-key map (kbd "SPC") #'image-dired-display-next-thumbnail-original) - (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) - (define-key map "n" #'image-dired-display-next-thumbnail-original) - (define-key map "p" #'image-dired-display-previous-thumbnail-original) - (define-key map "m" #'image-dired-mark-thumb-original-file) - (define-key map "d" #'image-dired-flag-thumb-original-file) - (define-key map "u" #'image-dired-unmark-thumb-original-file) - (define-key map "U" #'image-dired-unmark-all-marks) - ;; Disable keybindings from `image-mode-map' that doesn't make sense here. - (define-key map "o" nil) ; image-save - map) - "Keymap for `image-dired-display-image-mode'.") - -(define-derived-mode image-dired-thumbnail-mode - special-mode "image-dired-thumbnail" - "Browse and manipulate thumbnail images using Dired. -Use `image-dired-minor-mode' to get a nice setup." - :interactive nil - (buffer-disable-undo) - (add-hook 'file-name-at-point-functions 'image-dired-file-name-at-point nil t) - (setq-local window-resize-pixelwise t) - (setq-local bookmark-make-record-function #'image-dired-bookmark-make-record) - ;; Use approximately as much vertical spacing as horizontal. - (setq-local line-spacing (frame-char-width))) - - -;;; Display image mode - -(define-derived-mode image-dired-display-image-mode - image-mode "image-dired-image-display" - "Mode for displaying and manipulating original image. -Resized or in full-size." - :interactive nil - (add-hook 'file-name-at-point-functions #'image-dired-file-name-at-point nil t)) - -(defvar image-dired-minor-mode-map - (let ((map (make-sparse-keymap))) - ;; (set-keymap-parent map dired-mode-map) - ;; Hijack previous and next line movement. Let C-p and C-b be - ;; though... - (define-key map "p" #'image-dired-dired-previous-line) - (define-key map "n" #'image-dired-dired-next-line) - (define-key map [up] #'image-dired-dired-previous-line) - (define-key map [down] #'image-dired-dired-next-line) - - (define-key map (kbd "C-S-n") #'image-dired-next-line-and-display) - (define-key map (kbd "C-S-p") #'image-dired-previous-line-and-display) - (define-key map (kbd "C-S-m") #'image-dired-mark-and-display-next) - - (define-key map "\C-td" #'image-dired-display-thumbs) - (define-key map [tab] #'image-dired-jump-thumbnail-buffer) - (define-key map "\C-ti" #'image-dired-dired-display-image) - (define-key map "\C-tx" #'image-dired-dired-display-external) - (define-key map "\C-ta" #'image-dired-display-thumbs-append) - (define-key map "\C-t." #'image-dired-display-thumb) - (define-key map "\C-tc" #'image-dired-dired-comment-files) - (define-key map "\C-tf" #'image-dired-mark-tagged-files) - map) - "Keymap for `image-dired-minor-mode'.") - -(easy-menu-define image-dired-minor-mode-menu image-dired-minor-mode-map - "Menu for `image-dired-minor-mode'." - '("Image-dired" - ["Display thumb for next file" image-dired-next-line-and-display] - ["Display thumb for previous file" image-dired-previous-line-and-display] - ["Mark and display next" image-dired-mark-and-display-next] - "---" - ["Create thumbnails for marked files" image-dired-create-thumbs] - "---" - ["Display thumbnails append" image-dired-display-thumbs-append] - ["Display this thumbnail" image-dired-display-thumb] - ["Display image" image-dired-dired-display-image] - ["Display in external viewer" image-dired-dired-display-external] - "---" - ["Toggle display properties" image-dired-toggle-dired-display-properties - :style toggle - :selected image-dired-dired-disp-props] - ["Toggle append browsing" image-dired-toggle-append-browsing - :style toggle - :selected image-dired-append-when-browsing] - ["Toggle movement tracking" image-dired-toggle-movement-tracking - :style toggle - :selected image-dired-track-movement] - "---" - ["Jump to thumbnail buffer" image-dired-jump-thumbnail-buffer] - ["Mark tagged files" image-dired-mark-tagged-files] - ["Comment files" image-dired-dired-comment-files] - ["Copy with EXIF file name" image-dired-copy-with-exif-file-name])) - -;;;###autoload -(define-minor-mode image-dired-minor-mode - "Setup easy-to-use keybindings for the commands to be used in Dired mode. -Note that n, p and and will be hijacked and bound to -`image-dired-dired-next-line' and `image-dired-dired-previous-line'." - :keymap image-dired-minor-mode-map) - -(declare-function clear-image-cache "image.c" (&optional filter)) - -(defun image-dired-create-thumbs (&optional arg) - "Create thumbnail images for all marked files in Dired. -With prefix argument ARG, create thumbnails even if they already exist -\(i.e. use this to refresh your thumbnails)." - (interactive "P") - (let (thumb-name) - (dolist (curr-file (dired-get-marked-files)) - (setq thumb-name (image-dired-thumb-name curr-file)) - ;; If the user overrides the exist check, we must clear the - ;; image cache so that if the user wants to display the - ;; thumbnail, it is not fetched from cache. - (when arg - (clear-image-cache (expand-file-name thumb-name))) - (when (or (not (file-exists-p thumb-name)) - arg) - (image-dired-create-thumb curr-file thumb-name))))) - - -;;; Slideshow - -(defcustom image-dired-slideshow-delay 5.0 - "Seconds to wait before showing the next image in a slideshow. -This is used by `image-dired-slideshow-start'." - :type 'float - :version "29.1") - -(define-obsolete-variable-alias 'image-dired-slideshow-timer - 'image-dired--slideshow-timer "29.1") -(defvar image-dired--slideshow-timer nil - "Slideshow timer.") - -(defvar image-dired--slideshow-initial nil) - -(defun image-dired-slideshow-step () - "Step to next image in a slideshow." - (if-let ((buf (get-buffer image-dired-thumbnail-buffer))) - (with-current-buffer buf - (image-dired-display-next-thumbnail-original)) - (image-dired-slideshow-stop))) - -(defun image-dired-slideshow-start (&optional arg) - "Start a slideshow, waiting `image-dired-slideshow-delay' between images. - -With prefix argument ARG, wait that many seconds before going to -the next image. - -With a negative prefix argument, prompt user for the delay." - (interactive "P" image-dired-thumbnail-mode image-dired-display-image-mode) - (let ((delay (if (not arg) - image-dired-slideshow-delay - (if (> arg 0) - arg - (string-to-number - (let ((delay (number-to-string image-dired-slideshow-delay))) - (read-string - (format-prompt "Delay, in seconds. Decimals are accepted" delay)) - delay)))))) - (setq image-dired--slideshow-timer - (run-with-timer - 0 delay - 'image-dired-slideshow-step)) - (add-hook 'post-command-hook 'image-dired-slideshow-stop) - (setq image-dired--slideshow-initial t) - (message "Running slideshow; use any command to stop"))) - -(defun image-dired-slideshow-stop () - "Cancel slideshow." - ;; Make sure we don't immediately stop after - ;; `image-dired-slideshow-start'. - (unless image-dired--slideshow-initial - (remove-hook 'post-command-hook 'image-dired-slideshow-stop) - (cancel-timer image-dired--slideshow-timer)) - (setq image-dired--slideshow-initial nil)) - - -;;; Thumbnail mode (cont. 3) - -(defun image-dired-delete-char () - "Remove current thumbnail from thumbnail buffer and line up." - (interactive nil image-dired-thumbnail-mode) - (let ((inhibit-read-only t)) - (delete-char 1) - (when (= (following-char) ?\s) - (delete-char 1)))) - -;;;###autoload -(defun image-dired-display-thumbs-append () - "Append thumbnails to `image-dired-thumbnail-buffer'." - (interactive) - (image-dired-display-thumbs nil t t)) - -;;;###autoload -(defun image-dired-display-thumb () - "Shorthand for `image-dired-display-thumbs' with prefix argument." - (interactive) - (image-dired-display-thumbs t nil t)) - -(defun image-dired-line-up () - "Line up thumbnails according to `image-dired-thumbs-per-row'. -See also `image-dired-line-up-dynamic'." - (interactive) - (let ((inhibit-read-only t)) - (goto-char (point-min)) - (while (and (not (image-dired-image-at-point-p)) - (not (eobp))) - (delete-char 1)) - (while (not (eobp)) - (forward-char) - (while (and (not (image-dired-image-at-point-p)) - (not (eobp))) - (delete-char 1))) - (goto-char (point-min)) - (let ((seen 0) - (thumb-prev-pos 0) - (thumb-width-chars - (ceiling (/ (+ (* 2 image-dired-thumb-relief) - (* 2 image-dired-thumb-margin) - (image-dired-thumb-size 'width)) - (float (frame-char-width)))))) - (while (not (eobp)) - (forward-char) - (if (= image-dired-thumbs-per-row 1) - (insert "\n") - (cl-incf thumb-prev-pos thumb-width-chars) - (insert (propertize " " 'display `(space :align-to ,thumb-prev-pos))) - (cl-incf seen) - (when (and (= seen (- image-dired-thumbs-per-row 1)) - (not (eobp))) - (forward-char) - (insert "\n") - (setq seen 0) - (setq thumb-prev-pos 0))))) - (goto-char (point-min)))) - -(defun image-dired-line-up-dynamic () - "Line up thumbnails images dynamically. -Calculate how many thumbnails fit." - (interactive) - (let* ((char-width (frame-char-width)) - (width (image-dired-window-width-pixels (image-dired-thumbnail-window))) - (image-dired-thumbs-per-row - (/ width - (+ (* 2 image-dired-thumb-relief) - (* 2 image-dired-thumb-margin) - (image-dired-thumb-size 'width) - char-width)))) - (image-dired-line-up))) - -(defun image-dired-line-up-interactive () - "Line up thumbnails interactively. -Ask user how many thumbnails should be displayed per row." - (interactive) - (let ((image-dired-thumbs-per-row - (string-to-number (read-string "How many thumbs per row: ")))) - (if (not (> image-dired-thumbs-per-row 0)) - (message "Number must be greater than 0") - (image-dired-line-up)))) - -(defun image-dired-thumbnail-display-external () - "Display original image for thumbnail at point using external viewer." - (interactive) - (let ((file (image-dired-original-file-name))) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (if (not file) - (message "No original file name found") - (start-process "image-dired-thumb-external" nil - image-dired-external-viewer file))))) - -;;;###autoload -(defun image-dired-dired-display-external () - "Display file at point using an external viewer." - (interactive) - (let ((file (dired-get-filename))) - (start-process "image-dired-external" nil - image-dired-external-viewer file))) - -(defun image-dired-window-width-pixels (window) - "Calculate WINDOW width in pixels." - (* (window-width window) (frame-char-width))) - -(defun image-dired-display-window () - "Return window where `image-dired-display-image-buffer' is visible." - (get-window-with-predicate - (lambda (window) - (equal (buffer-name (window-buffer window)) image-dired-display-image-buffer)) - nil t)) - -(defun image-dired-thumbnail-window () - "Return window where `image-dired-thumbnail-buffer' is visible." - (get-window-with-predicate - (lambda (window) - (equal (buffer-name (window-buffer window)) image-dired-thumbnail-buffer)) - nil t)) - -(defun image-dired-associated-dired-buffer-window () - "Return window where associated Dired buffer is visible." - (let (buf) - (if (image-dired-image-at-point-p) - (progn - (setq buf (image-dired-associated-dired-buffer)) - (get-window-with-predicate - (lambda (window) - (equal (window-buffer window) buf)))) - (error "No thumbnail image at point")))) - -(defun image-dired-display-image (file &optional _ignored) - "Display image FILE in image buffer. -Use this when you want to display the image, in a new window. -The window will use `image-dired-display-image-mode' which is -based on `image-mode'." - (declare (advertised-calling-convention (file) "29.1")) - (setq file (expand-file-name file)) - (when (not (file-exists-p file)) - (error "No such file: %s" file)) - (let ((buf (get-buffer image-dired-display-image-buffer)) - (cur-win (selected-window))) - (when buf - (kill-buffer buf)) - (when-let ((buf (find-file-noselect file nil t))) - (pop-to-buffer buf) - (rename-buffer image-dired-display-image-buffer) - (image-dired-display-image-mode) - (select-window cur-win)))) - -(defun image-dired-display-thumbnail-original-image (&optional arg) - "Display current thumbnail's original image in display buffer. -See documentation for `image-dired-display-image' for more information. -With prefix argument ARG, display image in its original size." - (interactive "P") - (let ((file (image-dired-original-file-name))) - (if (not (string-equal major-mode "image-dired-thumbnail-mode")) - (message "Not in image-dired-thumbnail-mode") - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (if (not file) - (message "No original file name found") - (image-dired-display-image file arg)))))) - - -;;;###autoload -(defun image-dired-dired-display-image (&optional arg) - "Display current image file. -See documentation for `image-dired-display-image' for more information. -With prefix argument ARG, display image in its original size." - (interactive "P") - (image-dired-display-image (dired-get-filename) arg)) - -(defun image-dired-image-at-point-p () - "Return non-nil if there is an `image-dired' thumbnail at point." - (get-text-property (point) 'image-dired-thumbnail)) - (defun image-dired-refresh-thumb () "Force creation of new image for current thumbnail." (interactive nil image-dired-thumbnail-mode) @@ -2048,24 +417,6 @@ With prefix argument ARG, display image in its original size." (image-dired-refresh-thumb)) (image-dired-display-image file)))))) -(defun image-dired-rotate-original-left () - "Rotate original image left (counter clockwise) 90 degrees. -The result of the rotation is displayed in the image display area -and a confirmation is needed before the original image files is -overwritten. This confirmation can be turned off using -`image-dired-rotate-original-ask-before-overwrite'." - (interactive) - (image-dired-rotate-original "270")) - -(defun image-dired-rotate-original-right () - "Rotate original image right (clockwise) 90 degrees. -The result of the rotation is displayed in the image display area -and a confirmation is needed before the original image files is -overwritten. This confirmation can be turned off using -`image-dired-rotate-original-ask-before-overwrite'." - (interactive) - (image-dired-rotate-original "90")) - ;;; EXIF support @@ -2122,959 +473,10 @@ default value at the prompt." (mapcar (lambda (arg) (format-spec arg spec)) image-dired-cmd-write-exif-data-options)))) -(defun image-dired-copy-with-exif-file-name () - "Copy file with unique name to main image directory. -Copy current or all marked files in Dired to a new file in your -main image directory, using a file name generated by -`image-dired-get-exif-file-name'. A typical usage for this if when -copying images from a digital camera into the image directory. - - Typically, you would open up the folder with the incoming -digital images, mark the files to be copied, and execute this -function. The result is a couple of new files in -`image-dired-main-image-directory' called -2005_05_08_12_52_00_dscn0319.jpg, -2005_05_08_14_27_45_dscn0320.jpg etc." - (interactive) - (let (new-name - (files (dired-get-marked-files))) - (mapc - (lambda (curr-file) - (setq new-name - (format "%s/%s" - (file-name-as-directory - (expand-file-name image-dired-main-image-directory)) - (image-dired-get-exif-file-name curr-file))) - (message "Copying %s to %s" curr-file new-name) - (copy-file curr-file new-name)) - files))) - -;;; Thumbnail mode (cont.) - -(defun image-dired-display-next-thumbnail-original (&optional arg) - "Move to the next image in the thumbnail buffer and display it. -With prefix ARG, move that many thumbnails." - (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--with-thumbnail-buffer - (image-dired-forward-image arg t) - (image-dired-display-thumbnail-original-image))) - -(defun image-dired-display-previous-thumbnail-original (arg) - "Move to the previous image in the thumbnail buffer and display it. -With prefix ARG, move that many thumbnails." - (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired-display-next-thumbnail-original (- arg))) - - -;;; Image Comments - -(defun image-dired-write-comments (file-comments) - "Write file comments to database. -Write file comments to one or more files. -FILE-COMMENTS is an alist on the following form: - ((FILE . COMMENT) ... )" - (image-dired-sane-db-file) - (let (end comment-beg-pos comment-end-pos file comment) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (dolist (elt file-comments) - (setq file (car elt) - comment (cdr elt)) - (goto-char (point-min)) - (if (search-forward-regexp (format "^%s.*$" file) nil t) - (progn - (setq end (point)) - (beginning-of-line) - ;; Delete old comment, if any - (when (search-forward ";comment:" end t) - (setq comment-beg-pos (match-beginning 0)) - ;; Any tags after the comment? - (if (search-forward ";" end t) - (setq comment-end-pos (- (point) 1)) - (setq comment-end-pos end)) - ;; Delete comment tag and comment - (delete-region comment-beg-pos comment-end-pos)) - ;; Insert new comment - (beginning-of-line) - (unless (search-forward ";" end t) - (end-of-line) - (insert ";")) - (insert (format "comment:%s;" comment))) - ;; File does not exist in database - add it. - (goto-char (point-max)) - (insert (format "%s;comment:%s\n" file comment)))) - (save-buffer)))) - -(defun image-dired-update-property (prop value) - "Update text property PROP with value VALUE at point." - (let ((inhibit-read-only t)) - (put-text-property - (point) (1+ (point)) - prop - value))) - -;;;###autoload -(defun image-dired-dired-comment-files () - "Add comment to current or marked files in Dired." - (interactive) - (let ((comment (image-dired-read-comment))) - (image-dired-write-comments - (mapcar - (lambda (curr-file) - (cons curr-file comment)) - (dired-get-marked-files))))) - -(defun image-dired-comment-thumbnail () - "Add comment to current thumbnail in thumbnail buffer." - (interactive) - (let* ((file (image-dired-original-file-name)) - (comment (image-dired-read-comment file))) - (image-dired-write-comments (list (cons file comment))) - (image-dired-update-property 'comment comment)) - (image-dired-update-header-line)) - -(defun image-dired-read-comment (&optional file) - "Read comment for an image. -Optionally use old comment from FILE as initial value." - (let ((comment - (read-string - "Comment: " - (if file (image-dired-get-comment file))))) - comment)) - -(defun image-dired-get-comment (file) - "Get comment for file FILE." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end comment-beg-pos comment-end-pos comment) - (when (search-forward-regexp (format "^%s" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (search-forward ";comment:" end t) - (setq comment-beg-pos (point)) - (if (search-forward ";" end t) - (setq comment-end-pos (- (point) 1)) - (setq comment-end-pos end)) - (setq comment (buffer-substring - comment-beg-pos comment-end-pos)))) - comment))) - -;;;###autoload -(defun image-dired-mark-tagged-files (regexp) - "Use REGEXP to mark files with matching tag. -A `tag' is a keyword, a piece of meta data, associated with an -image file and stored in image-dired's database file. This command -lets you input a regexp and this will be matched against all tags -on all image files in the database file. The files that have a -matching tag will be marked in the Dired buffer." - (interactive "sMark tagged files (regexp): ") - (image-dired-sane-db-file) - (let ((hits 0) - files) - (image-dired--with-db-file - ;; Collect matches - (while (search-forward-regexp "\\(^[^;\n]+\\);\\(.*\\)" nil t) - (let ((file (match-string 1)) - (tags (split-string (match-string 2) ";"))) - (when (seq-find (lambda (tag) - (string-match-p regexp tag)) - tags) - (push file files))))) - ;; Mark files - (dolist (curr-file files) - ;; I tried using `dired-mark-files-regexp' but it was waaaay to - ;; slow. Don't bother about hits found in other directories - ;; than the current one. - (when (string= (file-name-as-directory - (expand-file-name default-directory)) - (file-name-as-directory - (file-name-directory curr-file))) - (setq curr-file (file-name-nondirectory curr-file)) - (goto-char (point-min)) - (when (search-forward-regexp (format "\\s %s$" curr-file) nil t) - (setq hits (+ hits 1)) - (dired-mark 1)))) - (message "%d files with matching tag marked" hits))) - - - -;;; Mouse support - -(defun image-dired-mouse-display-image (event) - "Use mouse EVENT, call `image-dired-display-image' to display image. -Track this in associated Dired buffer if `image-dired-track-movement' is -non-nil." - (interactive "e") - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (unless (image-at-point-p) - (image-dired-backward-image)) - (let ((file (image-dired-original-file-name))) - (when file - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-display-image file)))) - -(defun image-dired-mouse-select-thumbnail (event) - "Use mouse EVENT to select thumbnail image. -Track this in associated Dired buffer if `image-dired-track-movement' is -non-nil." - (interactive "e") - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (unless (image-at-point-p) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - - - -;;; Dired marks and tags - -(defun image-dired-thumb-file-marked-p (&optional flagged) - "Check if file is marked in associated Dired buffer. -If optional argument FLAGGED is non-nil, check if file is flagged -for deletion instead." - (let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (when (and dired-buf file-name) - (with-current-buffer dired-buf - (save-excursion - (when (dired-goto-file file-name) - (if flagged - (image-dired-dired-file-flagged-p) - (image-dired-dired-file-marked-p)))))))) - -(defun image-dired-thumb-file-flagged-p () - "Check if file is flagged for deletion in associated Dired buffer." - (image-dired-thumb-file-marked-p t)) - -(defun image-dired-delete-marked () - "Delete current or marked thumbnails and associated images." - (interactive) - (image-dired--with-marked - (image-dired-delete-char) - (unless (bobp) - (backward-char))) - (image-dired--line-up-with-method) - (with-current-buffer (image-dired-associated-dired-buffer) - (dired-do-delete))) - -(defun image-dired-thumb-update-marks () - "Update the marks in the thumbnail buffer." - (when image-dired-thumb-visible-marks - (with-current-buffer image-dired-thumbnail-buffer - (save-mark-and-excursion - (goto-char (point-min)) - (let ((inhibit-read-only t)) - (while (not (eobp)) - (with-silent-modifications - (cond ((image-dired-thumb-file-marked-p) - (add-face-text-property (point) (1+ (point)) - 'image-dired-thumb-mark)) - ((image-dired-thumb-file-flagged-p) - (add-face-text-property (point) (1+ (point)) - 'image-dired-thumb-flagged)) - (t (remove-text-properties (point) (1+ (point)) - '(face image-dired-thumb-mark))))) - (forward-char))))))) - -(defun image-dired-mouse-toggle-mark-1 () - "Toggle Dired mark for current thumbnail. -Track this in associated Dired buffer if -`image-dired-track-movement' is non-nil." - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-toggle-mark-thumb-original-file)) - -(defun image-dired-mouse-toggle-mark (event) - "Use mouse EVENT to toggle Dired mark for thumbnail. -Toggle marks of all thumbnails in region, if it's active. -Track this in associated Dired buffer if -`image-dired-track-movement' is non-nil." - (interactive "e") - (if (use-region-p) - (let ((end (region-end))) - (save-excursion - (goto-char (region-beginning)) - (while (<= (point) end) - (when (image-dired-image-at-point-p) - (image-dired-mouse-toggle-mark-1)) - (forward-char)))) - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (image-dired-mouse-toggle-mark-1)) - (image-dired-thumb-update-marks)) - -(defun image-dired-dired-display-properties () - "Display properties for Dired file in the echo area." - (interactive) - (let* ((file (dired-get-filename)) - (file-name (file-name-nondirectory file)) - (dired-buf (buffer-name (current-buffer))) - (props (mapconcat #'identity (image-dired-list-tags file) ", ")) - (comment (image-dired-get-comment file)) - (message-log-max nil)) - (if file-name - (message "%s" - (image-dired-format-properties-string - dired-buf - file-name - props - comment))))) - - - -;;; Gallery support - -;; TODO: -;; * Support gallery creation when using per-directory thumbnail -;; storage. -;; * Enhanced gallery creation with basic CSS-support and pagination -;; of tag pages with many pictures. - -(defgroup image-dired-gallery nil - "Image-Dired support for generating a HTML gallery." - :prefix "image-dired-" - :group 'image-dired - :version "29.1") - -(defcustom image-dired-gallery-dir - (expand-file-name ".image-dired_gallery" image-dired-dir) - "Directory to store generated gallery html pages. -The name of this directory needs to be \"shared\" to the public -so that it can access the index.html page that image-dired creates." - :type 'directory) - -(defcustom image-dired-gallery-image-root-url - "https://example.org/image-diredpics" - "URL where the full size images are to be found on your web server. -Note that this URL has to be configured on your web server. -Image-Dired expects to find pictures in this directory. -This is used by `image-dired-gallery-generate'." - :type 'string - :version "29.1") - -(defcustom image-dired-gallery-thumb-image-root-url - "https://example.org/image-diredthumbs" - "URL where the thumbnail images are to be found on your web server. -Note that URL path has to be configured on your web server. -Image-Dired expects to find pictures in this directory. -This is used by `image-dired-gallery-generate'." - :type 'string - :version "29.1") - -(defcustom image-dired-gallery-hidden-tags - (list "private" "hidden" "pending") - "List of \"hidden\" tags. -Used by `image-dired-gallery-generate' to leave out \"hidden\" images." - :type '(repeat string)) - -(defvar image-dired-tag-file-list nil - "List to store tag-file structure.") - -(defvar image-dired-file-tag-list nil - "List to store file-tag structure.") - -(defvar image-dired-file-comment-list nil - "List to store file comments.") - -(defun image-dired--add-to-tag-file-lists (tag file) - "Helper function used from `image-dired--create-gallery-lists'. - -Add TAG to FILE in one list and FILE to TAG in the other. - -Lisp structures look like the following: - -image-dired-file-tag-list: - - ((\"filename1\" \"tag1\" \"tag2\" \"tag3\" ...) - (\"filename2\" \"tag1\" \"tag2\" \"tag3\" ...) - ...) - -image-dired-tag-file-list: - - ((\"tag1\" \"filename1\" \"filename2\" \"filename3\" ...) - (\"tag2\" \"filename1\" \"filename2\" \"filename3\" ...) - ...)" - ;; Add tag to file list - (let (curr) - (if image-dired-file-tag-list - (if (setq curr (assoc file image-dired-file-tag-list)) - (setcdr curr (cons tag (cdr curr))) - (setcdr image-dired-file-tag-list - (cons (list file tag) (cdr image-dired-file-tag-list)))) - (setq image-dired-file-tag-list (list (list file tag)))) - ;; Add file to tag list - (if image-dired-tag-file-list - (if (setq curr (assoc tag image-dired-tag-file-list)) - (if (not (member file curr)) - (setcdr curr (cons file (cdr curr)))) - (setcdr image-dired-tag-file-list - (cons (list tag file) (cdr image-dired-tag-file-list)))) - (setq image-dired-tag-file-list (list (list tag file)))))) - -(defun image-dired--add-to-file-comment-list (file comment) - "Helper function used from `image-dired--create-gallery-lists'. - -For FILE, add COMMENT to list. - -Lisp structure looks like the following: - -image-dired-file-comment-list: - - ((\"filename1\" . \"comment1\") - (\"filename2\" . \"comment2\") - ...)" - (if image-dired-file-comment-list - (if (not (assoc file image-dired-file-comment-list)) - (setcdr image-dired-file-comment-list - (cons (cons file comment) - (cdr image-dired-file-comment-list)))) - (setq image-dired-file-comment-list (list (cons file comment))))) - -(defun image-dired--create-gallery-lists () - "Create temporary lists used by `image-dired-gallery-generate'." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end beg file row-tags) - (setq image-dired-tag-file-list nil) - (setq image-dired-file-tag-list nil) - (setq image-dired-file-comment-list nil) - (goto-char (point-min)) - (while (search-forward-regexp "^." nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (setq beg (point)) - (unless (search-forward ";" end nil) - (error "Something is really wrong, check format of database")) - (setq row-tags (split-string - (buffer-substring beg end) ";")) - (setq file (car row-tags)) - (dolist (x (cdr row-tags)) - (if (not (string-match "^comment:\\(.*\\)" x)) - (image-dired--add-to-tag-file-lists x file) - (image-dired--add-to-file-comment-list file (match-string 1 x))))))) - ;; Sort tag-file list - (setq image-dired-tag-file-list - (sort image-dired-tag-file-list - (lambda (x y) - (string< (car x) (car y)))))) - -(defun image-dired--hidden-p (file) - "Return t if image FILE has a \"hidden\" tag." - (cl-loop for tag in (cdr (assoc file image-dired-file-tag-list)) - if (member tag image-dired-gallery-hidden-tags) return t)) - -(defun image-dired-gallery-generate () - "Generate gallery pages. -First we create a couple of Lisp structures from the database to make -it easier to generate, then HTML-files are created in -`image-dired-gallery-dir'." - (interactive) - (if (eq 'per-directory image-dired-thumbnail-storage) - (error "Currently, gallery generation is not supported \ -when using per-directory thumbnail file storage")) - (image-dired--create-gallery-lists) - (let ((tags image-dired-tag-file-list) - (index-file (format "%s/index.html" image-dired-gallery-dir)) - count tag tag-file - comment file-tags tag-link tag-link-list) - ;; Make sure gallery root exist - (if (file-exists-p image-dired-gallery-dir) - (if (not (file-directory-p image-dired-gallery-dir)) - (error "Variable image-dired-gallery-dir is not a directory")) - ;; FIXME: Should we set umask to 077 here, as we do for thumbnails? - (make-directory image-dired-gallery-dir)) - ;; Open index file - (with-temp-file index-file - (if (file-exists-p index-file) - (insert-file-contents index-file)) - (insert "\n") - (insert " \n") - (insert "

Image-Dired Gallery

\n") - (insert (format "

\n Gallery generated %s\n

\n" - (current-time-string))) - (insert "

Tag index

\n") - (setq count 1) - ;; Pre-generate list of all tag links - (dolist (curr tags) - (setq tag (car curr)) - (when (not (member tag image-dired-gallery-hidden-tags)) - (setq tag-link (format "%s" count tag)) - (if tag-link-list - (setq tag-link-list - (append tag-link-list (list (cons tag tag-link)))) - (setq tag-link-list (list (cons tag tag-link)))) - (setq count (1+ count)))) - (setq count 1) - ;; Main loop where we generated thumbnail pages per tag - (dolist (curr tags) - (setq tag (car curr)) - ;; Don't display hidden tags - (when (not (member tag image-dired-gallery-hidden-tags)) - ;; Insert link to tag page in index - (insert (format " %s
\n" (cdr (assoc tag tag-link-list)))) - ;; Open per-tag file - (setq tag-file (format "%s/%s.html" image-dired-gallery-dir count)) - (with-temp-file tag-file - (if (file-exists-p tag-file) - (insert-file-contents tag-file)) - (erase-buffer) - (insert "\n") - (insert " \n") - (insert "

Index

\n") - (insert (format "

Images with tag "%s"

" tag)) - ;; Main loop for files per tag page - (dolist (file (cdr curr)) - (unless (image-dired-hidden-p file) - ;; Insert thumbnail with link to full image - (insert - (format "\n" - image-dired-gallery-image-root-url - (file-name-nondirectory file) - image-dired-gallery-thumb-image-root-url - (file-name-nondirectory (image-dired-thumb-name file)) file)) - ;; Insert comment, if any - (if (setq comment (cdr (assoc file image-dired-file-comment-list))) - (insert (format "
\n%s
\n" comment)) - (insert "
\n")) - ;; Insert links to other tags, if any - (when (> (length - (setq file-tags (assoc file image-dired-file-tag-list))) 2) - (insert "[ ") - (dolist (extra-tag file-tags) - ;; Only insert if not file name or the main tag - (if (and (not (equal extra-tag tag)) - (not (equal extra-tag file))) - (insert - (format "%s " (cdr (assoc extra-tag tag-link-list)))))) - (insert "]
\n")))) - (insert "

Index

\n") - (insert " \n") - (insert "\n")) - (setq count (1+ count)))) - (insert " \n") - (insert "")))) - - -;;; Tag support - -(defvar image-dired-widget-list nil - "List to keep track of meta data in edit buffer.") - -(declare-function widget-forward "wid-edit" (arg)) - -;;;###autoload -(defun image-dired-dired-edit-comment-and-tags () - "Edit comment and tags of current or marked image files. -Edit comment and tags for all marked image files in an -easy-to-use form." - (interactive) - (setq image-dired-widget-list nil) - ;; Setup buffer. - (let ((files (dired-get-marked-files))) - (pop-to-buffer-same-window "*Image-Dired Edit Meta Data*") - (kill-all-local-variables) - (let ((inhibit-read-only t)) - (erase-buffer)) - (remove-overlays) - ;; Some help for the user. - (widget-insert -"\nEdit comments and tags for each image. Separate multiple tags -with a comma. Move forward between fields using TAB or RET. -Move to the previous field using backtab (S-TAB). Save by -activating the Save button at the bottom of the form or cancel -the operation by activating the Cancel button.\n\n") - ;; Here comes all images and a comment and tag field for each - ;; image. - (let (thumb-file img comment-widget tag-widget) - - (dolist (file files) - - (setq thumb-file (image-dired-thumb-name file) - img (create-image thumb-file)) - - (insert-image img) - (widget-insert "\n\nComment: ") - (setq comment-widget - (widget-create 'editable-field - :size 60 - :format "%v " - :value (or (image-dired-get-comment file) ""))) - (widget-insert "\nTags: ") - (setq tag-widget - (widget-create 'editable-field - :size 60 - :format "%v " - :value (or (mapconcat - #'identity - (image-dired-list-tags file) - ",") ""))) - ;; Save information in all widgets so that we can use it when - ;; the user saves the form. - (setq image-dired-widget-list - (append image-dired-widget-list - (list (list file comment-widget tag-widget)))) - (widget-insert "\n\n"))) - - ;; Footer with Save and Cancel button. - (widget-insert "\n") - (widget-create 'push-button - :notify - (lambda (&rest _ignore) - (image-dired-save-information-from-widgets) - (bury-buffer) - (message "Done")) - "Save") - (widget-insert " ") - (widget-create 'push-button - :notify - (lambda (&rest _ignore) - (bury-buffer) - (message "Operation canceled")) - "Cancel") - (widget-insert "\n") - (use-local-map widget-keymap) - (widget-setup) - ;; Jump to the first widget. - (widget-forward 1))) - -(defun image-dired-save-information-from-widgets () - "Save information found in `image-dired-widget-list'. -Use the information in `image-dired-widget-list' to save comments and -tags to their respective image file. Internal function used by -`image-dired-dired-edit-comment-and-tags'." - (let (file comment tag-string tag-list lst) - (image-dired-write-comments - (mapcar - (lambda (widget) - (setq file (car widget) - comment (widget-value (cadr widget))) - (cons file comment)) - image-dired-widget-list)) - (image-dired-write-tags - (dolist (widget image-dired-widget-list lst) - (setq file (car widget) - tag-string (widget-value (car (cddr widget))) - tag-list (split-string tag-string ",")) - (dolist (tag tag-list) - (push (cons file tag) lst)))))) - - -;;; bookmark.el support - -(declare-function bookmark-make-record-default - "bookmark" (&optional no-file no-context posn)) -(declare-function bookmark-prop-get "bookmark" (bookmark prop)) - -(defun image-dired-bookmark-name () - "Create a default bookmark name for the current EWW buffer." - (file-name-nondirectory - (directory-file-name - (file-name-directory (image-dired-original-file-name))))) - -(defun image-dired-bookmark-make-record () - "Create a bookmark for the current EWW buffer." - `(,(image-dired-bookmark-name) - ,@(bookmark-make-record-default t) - (location . ,(file-name-directory (image-dired-original-file-name))) - (image-dired-file . ,(file-name-nondirectory (image-dired-original-file-name))) - (handler . image-dired-bookmark-jump))) - -;;;###autoload -(defun image-dired-bookmark-jump (bookmark) - "Default bookmark handler for Image-Dired buffers." - ;; User already cached thumbnails, so disable any checking. - (let ((image-dired-show-all-from-dir-max-files nil)) - (image-dired (bookmark-prop-get bookmark 'location)) - ;; TODO: Go to the bookmarked file, if it exists. - ;; (bookmark-prop-get bookmark 'image-dired-file) - (goto-char (point-min)))) - -(put 'image-dired-bookmark-jump 'bookmark-handler-type "Image-Dired") - -;;; Obsolete - -;;;###autoload -(define-obsolete-function-alias 'tumme #'image-dired "24.4") - -;;;###autoload -(define-obsolete-function-alias 'image-dired-setup-dired-keybindings - #'image-dired-minor-mode "26.1") - -(defcustom image-dired-temp-image-file - (expand-file-name ".image-dired_temp" image-dired-dir) - "Name of temporary image file used by various commands." - :type 'file) -(make-obsolete-variable 'image-dired-temp-image-file - "no longer used." "29.1") - -(defcustom image-dired-cmd-create-temp-image-program - (if (executable-find "gm") "gm" "convert") - "Executable used to create temporary image. -Used together with `image-dired-cmd-create-temp-image-options'." - :type 'file - :version "29.1") -(make-obsolete-variable 'image-dired-cmd-create-temp-image-program - "no longer used." "29.1") - -(defcustom image-dired-cmd-create-temp-image-options - (let ((opts '("-size" "%wx%h" "%f[0]" - "-resize" "%wx%h>" - "-strip" "jpeg:%t"))) - (if (executable-find "gm") (cons "convert" opts) opts)) - "Options of command used to create temporary image for display window. -Used together with `image-dired-cmd-create-temp-image-program', -Available format specifiers are: %w and %h which are replaced by -the calculated max size for width and height in the image display window, -%f which is replaced by the file name of the original image and %t which -is replaced by the file name of the temporary file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-create-temp-image-options - "no longer used." "29.1") - -(defcustom image-dired-display-window-width-correction 1 - "Number to be used to correct image display window width. -Change if the default (1) does not work (i.e. if the image does not -completely fit)." - :type 'integer) -(make-obsolete-variable 'image-dired-display-window-width-correction - "no longer used." "29.1") - -(defcustom image-dired-display-window-height-correction 0 - "Number to be used to correct image display window height. -Change if the default (0) does not work (i.e. if the image does not -completely fit)." - :type 'integer) -(make-obsolete-variable 'image-dired-display-window-height-correction - "no longer used." "29.1") - -(defun image-dired-display-window-width (window) - "Return width, in pixels, of WINDOW." - (declare (obsolete nil "29.1")) - (- (image-dired-window-width-pixels window) - image-dired-display-window-width-correction)) - -(defun image-dired-display-window-height (window) - "Return height, in pixels, of WINDOW." - (declare (obsolete nil "29.1")) - (- (image-dired-window-height-pixels window) - image-dired-display-window-height-correction)) - -(defun image-dired-window-height-pixels (window) - "Calculate WINDOW height in pixels." - (declare (obsolete nil "29.1")) - ;; Note: The mode-line consumes one line - (* (- (window-height window) 1) (frame-char-height))) - -(defcustom image-dired-cmd-read-exif-data-program "exiftool" - "Program used to read EXIF data to image. -Used together with `image-dired-cmd-read-exif-data-options'." - :type 'file) -(make-obsolete-variable 'image-dired-cmd-read-exif-data-program - "use `exif-parse-file' and `exif-field' instead." "29.1") - -(defcustom image-dired-cmd-read-exif-data-options '("-s" "-s" "-s" "-%t" "%f") - "Arguments of command used to read EXIF data. -Used with `image-dired-cmd-read-exif-data-program'. -Available format specifiers are: %f which is replaced -by the image file name and %t which is replaced by the tag name." - :version "26.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-read-exif-data-options - "use `exif-parse-file' and `exif-field' instead." "29.1") - -(defun image-dired-get-exif-data (file tag-name) - "From FILE, return EXIF tag TAG-NAME." - (declare (obsolete "use `exif-parse-file' and `exif-field' instead." "29.1")) - (image-dired--check-executable-exists - 'image-dired-cmd-read-exif-data-program) - (let ((buf (get-buffer-create "*image-dired-get-exif-data*")) - (spec (list (cons ?f file) (cons ?t tag-name))) - tag-value) - (with-current-buffer buf - (delete-region (point-min) (point-max)) - (if (not (eq (apply #'call-process image-dired-cmd-read-exif-data-program - nil t nil - (mapcar - (lambda (arg) (format-spec arg spec)) - image-dired-cmd-read-exif-data-options)) - 0)) - (error "Could not get EXIF tag") - (goto-char (point-min)) - ;; Clean buffer from newlines and carriage returns before - ;; getting final info - (while (search-forward-regexp "[\n\r]" nil t) - (replace-match "" nil t)) - (setq tag-value (buffer-substring (point-min) (point-max))))) - tag-value)) - -(defcustom image-dired-cmd-rotate-thumbnail-program - (if (executable-find "gm") "gm" "mogrify") - "Executable used to rotate thumbnail. -Used together with `image-dired-cmd-rotate-thumbnail-options'." - :type 'file - :version "29.1") -(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-program nil "29.1") - -(defcustom image-dired-cmd-rotate-thumbnail-options - (let ((opts '("-rotate" "%d" "%t"))) - (if (executable-find "gm") (cons "mogrify" opts) opts)) - "Arguments of command used to rotate thumbnail image. -Used with `image-dired-cmd-rotate-thumbnail-program'. -Available format specifiers are: %d which is replaced by the -number of (positive) degrees to rotate the image, normally 90 or 270 -\(for 90 degrees right and left), %t which is replaced by the file name -of the thumbnail file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-options nil "29.1") - -(defun image-dired-rotate-thumbnail (degrees) - "Rotate thumbnail DEGREES degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (image-dired--check-executable-exists - 'image-dired-cmd-rotate-thumbnail-program) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (let* ((file (image-dired-thumb-name (image-dired-original-file-name))) - (thumb (expand-file-name file)) - (spec (list (cons ?d degrees) (cons ?t thumb)))) - (apply #'call-process image-dired-cmd-rotate-thumbnail-program nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-rotate-thumbnail-options)) - (clear-image-cache thumb)))) - -(defun image-dired-rotate-thumbnail-left () - "Rotate thumbnail left (counter clockwise) 90 degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (interactive) - (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) - (image-dired-rotate-thumbnail "270"))) - -(defun image-dired-rotate-thumbnail-right () - "Rotate thumbnail counter right (clockwise) 90 degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (interactive) - (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) - (image-dired-rotate-thumbnail "90"))) - -(defun image-dired-modify-mark-on-thumb-original-file (command) - "Modify mark in Dired buffer. -COMMAND is one of `mark' for marking file in Dired, `unmark' for -unmarking file in Dired or `flag' for flagging file for delete in -Dired." - (declare (obsolete image-dired--on-file-in-dired-buffer "29.1")) - (let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (if (not (and dired-buf file-name)) - (message "No image, or image with correct properties, at point") - (with-current-buffer dired-buf - (message "%s" file-name) - (when (dired-goto-file file-name) - (cond ((eq command 'mark) (dired-mark 1)) - ((eq command 'unmark) (dired-unmark 1)) - ((eq command 'toggle) - (if (image-dired-dired-file-marked-p) - (dired-unmark 1) - (dired-mark 1))) - ((eq command 'flag) (dired-flag-file-deletion 1))) - (image-dired-thumb-update-marks)))))) - -(defun image-dired-display-current-image-full () - "Display current image in full size." - (declare (obsolete image-transform-original "29.1")) - (interactive nil image-dired-thumbnail-mode) - (let ((file (image-dired-original-file-name))) - (if file - (progn - (image-dired-display-image file) - (with-current-buffer image-dired-display-image-buffer - (image-transform-original))) - (error "No original file name at point")))) - -(defun image-dired-display-current-image-sized () - "Display current image in sized to fit window dimensions." - (declare (obsolete image-mode-fit-frame "29.1")) - (interactive nil image-dired-thumbnail-mode) - (let ((file (image-dired-original-file-name))) - (if file - (progn - (image-dired-display-image file)) - (error "No original file name at point")))) - -(defun image-dired-add-to-tag-file-list (tag file) - "Add relation between TAG and FILE." - (declare (obsolete nil "29.1")) - (let (curr) - (if image-dired-tag-file-list - (if (setq curr (assoc tag image-dired-tag-file-list)) - (if (not (member file curr)) - (setcdr curr (cons file (cdr curr)))) - (setcdr image-dired-tag-file-list - (cons (list tag file) (cdr image-dired-tag-file-list)))) - (setq image-dired-tag-file-list (list (list tag file)))))) - -(defun image-dired-display-thumb-properties () - "Display thumbnail properties in the echo area." - (declare (obsolete image-dired-update-header-line "29.1")) - (image-dired-update-header-line)) - -(defvar image-dired-slideshow-count 0 - "Keeping track on number of images in slideshow.") -(make-obsolete-variable 'image-dired-slideshow-count "no longer used." "29.1") - -(defvar image-dired-slideshow-times 0 - "Number of pictures to display in slideshow.") -(make-obsolete-variable 'image-dired-slideshow-times "no longer used." "29.1") - -(define-obsolete-function-alias 'image-dired-create-display-image-buffer - #'ignore "29.1") -(define-obsolete-function-alias 'image-dired-create-gallery-lists - #'image-dired--create-gallery-lists "29.1") -(define-obsolete-function-alias 'image-dired-add-to-file-comment-list - #'image-dired--add-to-file-comment-list "29.1") -(define-obsolete-function-alias 'image-dired-add-to-tag-file-lists - #'image-dired--add-to-tag-file-lists "29.1") -(define-obsolete-function-alias 'image-dired-hidden-p - #'image-dired--hidden-p "29.1") - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;;;;;;; TEST-SECTION ;;;;;;;;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; (defvar image-dired-dir-max-size 12300000) - -;; (defun image-dired-test-clean-old-files () -;; "Clean `image-dired-dir' from old thumbnail files. -;; \"Oldness\" measured using last access time. If the total size of all -;; thumbnail files in `image-dired-dir' is larger than 'image-dired-dir-max-size', -;; old files are deleted until the max size is reached." -;; (let* ((files -;; (sort -;; (mapcar -;; (lambda (f) -;; (let ((fattribs (file-attributes f))) -;; `(,(file-attribute-access-time fattribs) -;; ,(file-attribute-size fattribs) ,f))) -;; (directory-files (image-dired-dir) t ".+\\.thumb\\..+$")) -;; ;; Sort function. Compare time between two files. -;; (lambda (l1 l2) -;; (time-less-p (car l1) (car l2))))) -;; (dirsize (apply '+ (mapcar (lambda (x) (cadr x)) files)))) -;; (while (> dirsize image-dired-dir-max-size) -;; (y-or-n-p -;; (format "Size of thumbnail directory: %d, delete old file %s? " -;; dirsize (cadr (cdar files)))) -;; (delete-file (cadr (cdar files))) -;; (setq dirsize (- dirsize (car (cdar files)))) -;; (setq files (cdr files))))) +(provide 'image-dired-external) -(provide 'image-dired) +;; Local Variables: +;; nameless-current-name: "image-dired" +;; End: -;;; image-dired.el ends here +;;; image-dired-external.el ends here diff --git a/lisp/image/image-dired-tags.el b/lisp/image/image-dired-tags.el index 9f12354111..97003851e0 100644 --- a/lisp/image/image-dired-tags.el +++ b/lisp/image/image-dired-tags.el @@ -1,10 +1,9 @@ -;;; image-dired.el --- use dired to browse and manipulate your images -*- lexical-binding: t -*- +;;; image-dired-tags.el --- Tag support for Image-Dired -*- lexical-binding: t -*- ;; Copyright (C) 2005-2022 Free Software Foundation, Inc. -;; Version: 0.4.11 -;; Keywords: multimedia ;; Author: Mathias Dahl +;; Keywords: multimedia ;; This file is part of GNU Emacs. @@ -23,494 +22,17 @@ ;;; Commentary: -;; BACKGROUND -;; ========== -;; -;; I needed a program to browse, organize and tag my pictures. I got -;; tired of the old gallery program I used as it did not allow -;; multi-file operations easily. Also, it put things out of my -;; control. Image viewing programs I tested did not allow multi-file -;; operations or did not do what I wanted it to. -;; -;; So, I got the idea to use the wonderful functionality of Emacs and -;; `dired' to do it. It would allow me to do almost anything I wanted, -;; which is basically just to browse all my pictures in an easy way, -;; letting me manipulate and tag them in various ways. `dired' already -;; provide all the file handling and navigation facilities; I only -;; needed to add some functions to display the images. -;; -;; I briefly tried out thumbs.el, and although it seemed more -;; powerful than this package, it did not work the way I wanted to. It -;; was too slow to create thumbnails of all files in a directory (I -;; currently keep all my 2000+ images in the same directory) and -;; browsing the thumbnail buffer was slow too. image-dired.el will not -;; create thumbnails until they are needed and the browsing is done -;; quickly and easily in Dired. I copied a great deal of ideas and -;; code from there though... :) -;; -;; `image-dired' stores the thumbnail files in `image-dired-dir' -;; using the file name format ORIGNAME.thumb.ORIGEXT. For example -;; ~/.emacs.d/image-dired/myimage01.thumb.jpg. The "database" is for -;; now just a plain text file with the following format: -;; -;; file-name-non-directory;comment:comment-text;tag1;tag2;tag3;...;tagN -;; -;; -;; PREREQUISITES -;; ============= -;; -;; * The GraphicsMagick or ImageMagick package; Image-Dired uses -;; whichever is available. -;; -;; A) For GraphicsMagick, `gm' is used. -;; Find it here: http://www.graphicsmagick.org/ -;; -;; B) For ImageMagick, `convert' and `mogrify' are used. -;; Find it here: https://www.imagemagick.org. -;; -;; * For non-lossy rotation of JPEG images, the JpegTRAN program is -;; needed. -;; -;; * For `image-dired-set-exif-data' to work, the command line tool `exiftool' is -;; needed. It can be found here: https://exiftool.org/. This -;; function is, among other things, used for writing comments to -;; image files using `image-dired-thumbnail-set-image-description'. -;; -;; -;; USAGE -;; ===== -;; -;; This information has been moved to the manual. Type `C-h r' to open -;; the Emacs manual and go to the node Thumbnails by typing `g -;; Image-Dired RET'. -;; -;; Quickstart: M-x image-dired RET DIRNAME RET -;; -;; where DIRNAME is a directory containing image files. -;; -;; LIMITATIONS -;; =========== -;; -;; * Supports all image formats that Emacs and convert supports, but -;; the thumbnails are hard-coded to JPEG or PNG format. It uses -;; JPEG by default, but can optionally follow the Thumbnail Managing -;; Standard (v0.9.0, Dec 2020), which mandates PNG. See the user -;; option `image-dired-thumbnail-storage'. -;; -;; * WARNING: The "database" format used might be changed so keep a -;; backup of `image-dired-db-file' when testing new versions. -;; -;; TODO -;; ==== -;; -;; * Investigate if it is possible to also write the tags to the image -;; files. -;; -;; * From thumbs.el: Add an option for clean-up/max-size functionality -;; for thumbnail directory. -;; -;; * From thumbs.el: Add setroot function. -;; -;; * Add `image-dired-display-thumbs-ring' and functions to cycle that. Find out -;; which is best, saving old batch just before inserting new, or -;; saving the current batch in the ring when inserting it. Adding -;; it probably needs rewriting `image-dired-display-thumbs' to be more general. -;; -;; * Find some way of toggling on and off really nice keybindings in -;; Dired (for example, using C-n or instead of C-S-n). -;; Richard suggested that we could keep C-t as prefix for -;; image-dired commands as it is currently not used in Dired. He -;; also suggested that `dired-next-line' and `dired-previous-line' -;; figure out if image-dired is enabled in the current buffer and, -;; if it is, call `image-dired-dired-next-line' and `image-dired-dired-previous-line', -;; respectively. Update: This is partly done; some bindings have -;; now been added to Dired. -;; -;; * In some way keep track of buffers and windows and stuff so that -;; it works as the user expects. -;; -;; * More/better documentation. - ;;; Code: (require 'dired) -(require 'exif) -(require 'image-mode) -(require 'widget) -(require 'xdg) - -(eval-when-compile - (require 'cl-lib) - (require 'wid-edit)) - - -;;; Customizable variables - -(defgroup image-dired nil - "Use Dired to browse your images as thumbnails, and more." - :prefix "image-dired-" - :link '(info-link "(emacs) Image-Dired") - :group 'multimedia) - -(defcustom image-dired-dir (locate-user-emacs-file "image-dired/") - "Directory where thumbnail images are stored. - -The value of this option will be ignored if Image-Dired is -customized to use the Thumbnail Managing Standard; they will be -saved in \"$XDG_CACHE_HOME/thumbnails/\" instead. See -`image-dired-thumbnail-storage'." - :type 'directory) - -(defcustom image-dired-thumbnail-storage 'use-image-dired-dir - "How `image-dired' stores thumbnail files. -There are two ways that Image-Dired can store and generate -thumbnails. If you set this variable to one of the two following -values, they will be stored in the JPEG format: - -- `use-image-dired-dir' means that the thumbnails are stored in a - central directory. - -- `per-directory' means that each thumbnail is stored in a - subdirectory called \".image-dired\" in the same directory - where the image file is. - -It can also use the \"Thumbnail Managing Standard\", which allows -sharing of thumbnails across different programs. Thumbnails will -be stored in \"$XDG_CACHE_HOME/thumbnails/\" instead of in -`image-dired-dir'. Thumbnails are saved in the PNG format, and -can be one of the following sizes: - -- `standard' means use thumbnails sized 128x128. -- `standard-large' means use thumbnails sized 256x256. -- `standard-x-large' means use thumbnails sized 512x512. -- `standard-xx-large' means use thumbnails sized 1024x1024. - -For more information on the Thumbnail Managing Standard, see: -https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html" - :type '(choice :tag "How to store thumbnail files" - (const :tag "Use image-dired-dir" use-image-dired-dir) - (const :tag "Thumbnail Managing Standard (normal 128x128)" - standard) - (const :tag "Thumbnail Managing Standard (large 256x256)" - standard-large) - (const :tag "Thumbnail Managing Standard (larger 512x512)" - standard-x-large) - (const :tag "Thumbnail Managing Standard (extra large 1024x1024)" - standard-xx-large) - (const :tag "Per-directory" per-directory)) - :version "29.1") - -(defconst image-dired--thumbnail-standard-sizes - '( standard standard-large - standard-x-large standard-xx-large) - "List of symbols representing thumbnail sizes in Thumbnail Managing Standard.") - -(defcustom image-dired-db-file - (expand-file-name ".image-dired_db" image-dired-dir) - "Database file where file names and their associated tags are stored." - :type 'file) - -(defcustom image-dired-cmd-create-thumbnail-program - (if (executable-find "gm") "gm" "convert") - "Executable used to create thumbnail. -Used together with `image-dired-cmd-create-thumbnail-options'." - :type 'file - :version "29.1") - -(defcustom image-dired-cmd-create-thumbnail-options - (let ((opts '("-size" "%wx%h" "%f[0]" - "-resize" "%wx%h>" - "-strip" "jpeg:%t"))) - (if (executable-find "gm") (cons "convert" opts) opts)) - "Options of command used to create thumbnail image. -Used with `image-dired-cmd-create-thumbnail-program'. -Available format specifiers are: %w which is replaced by -`image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', -%f which is replaced by the file name of the original image and %t -which is replaced by the file name of the thumbnail file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-pngnq-program - ;; Prefer pngquant to pngnq-s9 as it is faster on my machine. - ;; The project also seems more active than the alternatives. - ;; Prefer pngnq-s9 to pngnq as it fixes bugs in pngnq. - ;; The pngnq project seems dead (?) since 2011 or so. - (or (executable-find "pngquant") - (executable-find "pngnq-s9") - (executable-find "pngnq")) - "The file name of the `pngquant' or `pngnq' program. -It quantizes colors of PNG images down to 256 colors or fewer -using the NeuQuant algorithm." - :version "29.1" - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-pngnq-options - (if (executable-find "pngquant") - '("--ext" "-nq8.png" "%t") ; same extension as "pngnq" - '("-f" "%t")) - "Arguments to pass `image-dired-cmd-pngnq-program'. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options'." - :type '(repeat (string :tag "Argument")) - :version "29.1") - -(defcustom image-dired-cmd-pngcrush-program (executable-find "pngcrush") - "The file name of the `pngcrush' program. -It optimizes the compression of PNG images. Also it adds PNG textual chunks -with the information required by the Thumbnail Managing Standard." - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-pngcrush-options - `("-q" - "-text" "b" "Description" "Thumbnail of file://%f" - "-text" "b" "Software" ,(emacs-version) - ;; "-text b \"Thumb::Image::Height\" \"%oh\" " - ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" " - ;; "-text b \"Thumb::Image::Width\" \"%ow\" " - "-text" "b" "Thumb::MTime" "%m" - ;; "-text b \"Thumb::Size\" \"%b\" " - "-text" "b" "Thumb::URI" "file://%f" - "%q" "%t") - "Arguments for `image-dired-cmd-pngcrush-program'. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options', with %q for a -temporary file name (typically generated by pnqnq)." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-optipng-program (executable-find "optipng") - "The file name of the `optipng' program." - :version "26.1" - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-optipng-options '("-o5" "%t") - "Arguments passed to `image-dired-cmd-optipng-program'. -Available format specifiers are described in -`image-dired-cmd-create-thumbnail-options'." - :version "26.1" - :type '(repeat (string :tag "Argument")) - :link '(url-link "man:optipng(1)")) - -(defcustom image-dired-cmd-create-standard-thumbnail-options - (append '("-size" "%wx%h" "%f[0]") - (unless (or image-dired-cmd-pngcrush-program - image-dired-cmd-pngnq-program) - (list - "-set" "Thumb::MTime" "%m" - "-set" "Thumb::URI" "file://%f" - "-set" "Description" "Thumbnail of file://%f" - "-set" "Software" (emacs-version))) - '("-thumbnail" "%wx%h>" "png:%t")) - "Options for creating thumbnails according to the Thumbnail Managing Standard. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options', with %m for file modification time." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-rotate-original-program - "jpegtran" - "Executable used to rotate original image. -Used together with `image-dired-cmd-rotate-original-options'." - :type 'file) - -(defcustom image-dired-cmd-rotate-original-options - '("-rotate" "%d" "-copy" "all" "-outfile" "%t" "%o") - "Arguments of command used to rotate original image. -Used with `image-dired-cmd-rotate-original-program'. -Available format specifiers are: %d which is replaced by the -number of (positive) degrees to rotate the image, normally 90 or -270 \(for 90 degrees right and left), %o which is replaced by the -original image file name and %t which is replaced by -`image-dired-temp-image-file'." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-temp-rotate-image-file - (expand-file-name ".image-dired_rotate_temp" image-dired-dir) - "Temporary file for rotate operations." - :type 'file) - -(defcustom image-dired-rotate-original-ask-before-overwrite t - "Confirm overwrite of original file after rotate operation. -If non-nil, ask user for confirmation before overwriting the -original file with `image-dired-temp-rotate-image-file'." - :type 'boolean) - -(defcustom image-dired-cmd-write-exif-data-program - "exiftool" - "Program used to write EXIF data to image. -Used together with `image-dired-cmd-write-exif-data-options'." - :type 'file) - -(defcustom image-dired-cmd-write-exif-data-options - '("-%t=%v" "%f") - "Arguments of command used to write EXIF data. -Used with `image-dired-cmd-write-exif-data-program'. -Available format specifiers are: %f which is replaced by -the image file name, %t which is replaced by the tag name and %v -which is replaced by the tag value." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-thumb-size - (cond - ((eq 'standard image-dired-thumbnail-storage) 128) - ((eq 'standard-large image-dired-thumbnail-storage) 256) - ((eq 'standard-x-large image-dired-thumbnail-storage) 512) - ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) - (t 100)) - "Size of thumbnails, in pixels. -This is the default size for both `image-dired-thumb-width' -and `image-dired-thumb-height'. - -The value of this option will be ignored if Image-Dired is -customized to use the Thumbnail Managing Standard; the standard -sizes will be used instead. See `image-dired-thumbnail-storage'." - :type 'integer) - -(defcustom image-dired-thumb-width image-dired-thumb-size - "Width of thumbnails, in pixels." - :type 'integer) - -(defcustom image-dired-thumb-height image-dired-thumb-size - "Height of thumbnails, in pixels." - :type 'integer) - -(defcustom image-dired-thumb-relief 2 - "Size of button-like border around thumbnails." - :type 'integer) - -(defcustom image-dired-thumb-margin 2 - "Size of the margin around thumbnails. -This is where you see the cursor." - :type 'integer) - -(defcustom image-dired-thumb-visible-marks t - "Make marks and flags visible in thumbnail buffer. -If non-nil, apply the `image-dired-thumb-mark' face to marked -images and `image-dired-thumb-flagged' to images flagged for -deletion." - :type 'boolean - :version "28.1") - -(defface image-dired-thumb-mark - '((((class color) (min-colors 16)) :background "DarkOrange") - (((class color)) :foreground "yellow")) - "Face for marked images in thumbnail buffer." - :version "29.1") - -(defface image-dired-thumb-flagged - '((((class color) (min-colors 88) (background light)) :background "Red3") - (((class color) (min-colors 88) (background dark)) :background "Pink") - (((class color) (min-colors 16) (background light)) :background "Red3") - (((class color) (min-colors 16) (background dark)) :background "Pink") - (((class color) (min-colors 8)) :background "red") - (t :inverse-video t)) - "Face for images flagged for deletion in thumbnail buffer." - :version "29.1") - -(defcustom image-dired-line-up-method 'dynamic - "Default method for line-up of thumbnails in thumbnail buffer. -Used by `image-dired-display-thumbs' and other functions that needs -to line-up thumbnails. Dynamic means to use the available width of -the window containing the thumbnail buffer, Fixed means to use -`image-dired-thumbs-per-row', Interactive is for asking the user, -and No line-up means that no automatic line-up will be done." - :type '(choice :tag "Default line-up method" - (const :tag "Dynamic" dynamic) - (const :tag "Fixed" fixed) - (const :tag "Interactive" interactive) - (const :tag "No line-up" none))) - -(defcustom image-dired-thumbs-per-row 3 - "Number of thumbnails to display per row in thumb buffer." - :type 'integer) - -(defcustom image-dired-track-movement t - "The current state of the tracking and mirroring. -For more information, see the documentation for -`image-dired-toggle-movement-tracking'." - :type 'boolean) -(defcustom image-dired-append-when-browsing nil - "Append thumbnails in thumbnail buffer when browsing. -If non-nil, using `image-dired-next-line-and-display' and -`image-dired-previous-line-and-display' will leave a trail of thumbnail -images in the thumbnail buffer. If you enable this and want to clean -the thumbnail buffer because it is filled with too many thumbnails, -just call `image-dired-display-thumb' to display only the image at point. -This value can be toggled using `image-dired-toggle-append-browsing'." - :type 'boolean) +(require 'image-dired-util) -(defcustom image-dired-dired-disp-props t - "If non-nil, display properties for Dired file when browsing. -Used by `image-dired-next-line-and-display', -`image-dired-previous-line-and-display' and `image-dired-mark-and-display-next'. -If the database file is large, this can slow down image browsing in -Dired and you might want to turn it off." - :type 'boolean) +(declare-function image-dired--with-marked "image-dired") -(defcustom image-dired-display-properties-format "%b: %f (%t): %c" - "Display format for thumbnail properties. -%b is replaced with associated Dired buffer name, %f with file -name (without path) of original image file, %t with the list of -tags and %c with the comment." - :type 'string) - -(defcustom image-dired-external-viewer - ;; TODO: Use mailcap, dired-guess-shell-alist-default, - ;; dired-view-command-alist. - (cond ((executable-find "display")) - ((executable-find "xli")) - ((executable-find "qiv") "qiv -t") - ((executable-find "feh") "feh")) - "Name of external viewer. -Including parameters. Used when displaying original image from -`image-dired-thumbnail-mode'." - :version "28.1" - :type '(choice string - (const :tag "Not Set" nil))) - -(defcustom image-dired-main-image-directory - (or (xdg-user-dir "PICTURES") "~/pics/") - "Name of main image directory, if any. -Used by `image-dired-copy-with-exif-file-name'." - :type 'string - :version "29.1") - -(defcustom image-dired-show-all-from-dir-max-files 500 - "Maximum number of files in directory before prompting. - -If there are more image files than this in a selected directory, -the `image-dired-show-all-from-dir' command will ask for -confirmation before creating the thumbnail buffer. If this -variable is nil, it will never ask." - :type '(choice integer - (const :tag "Disable warning" nil)) - :version "29.1") - -(defcustom image-dired-marking-shows-next t - "If non-nil, marking, unmarking or flagging an image shows the next image. - -This affects the following commands: -\\ - `image-dired-flag-thumb-original-file' (bound to \\[image-dired-flag-thumb-original-file]) - `image-dired-mark-thumb-original-file' (bound to \\[image-dired-mark-thumb-original-file]) - `image-dired-unmark-thumb-original-file' (bound to \\[image-dired-unmark-thumb-original-file])" - :type 'boolean - :version "29.1") - - -;;; Util functions - -(defvar image-dired-debug nil - "Non-nil means enable debug messages.") - -(defun image-dired-debug-message (&rest args) - "Display debug message ARGS when `image-dired-debug' is non-nil." - (when image-dired-debug - (apply #'message args))) +(defvar image-dired-dir) +(defvar image-dired-thumbnail-storage) +(defvar image-dired-db-file) (defmacro image-dired--with-db-file (&rest body) "Run BODY in a temp buffer containing `image-dired-db-file'. @@ -521,557 +43,6 @@ Return the last form in BODY." (insert-file-contents image-dired-db-file)) ,@body)) -(defun image-dired-dir () - "Return the current thumbnail directory (from variable `image-dired-dir'). -Create the thumbnail directory if it does not exist." - (let ((image-dired-dir (file-name-as-directory - (expand-file-name image-dired-dir)))) - (unless (file-directory-p image-dired-dir) - (with-file-modes #o700 - (make-directory image-dired-dir t)) - (message "Thumbnail directory created: %s" image-dired-dir)) - image-dired-dir)) - -(defun image-dired-insert-image (file type relief margin) - "Insert image FILE of image TYPE, using RELIEF and MARGIN, at point." - (let ((i `(image :type ,type - :file ,file - :relief ,relief - :margin ,margin))) - (insert-image i))) - -(defun image-dired-get-thumbnail-image (file) - "Return the image descriptor for a thumbnail of image file FILE." - (unless (string-match-p (image-file-name-regexp) file) - (error "%s is not a valid image file" file)) - (let* ((thumb-file (image-dired-thumb-name file)) - (thumb-attr (file-attributes thumb-file))) - (when (or (not thumb-attr) - (time-less-p (file-attribute-modification-time thumb-attr) - (file-attribute-modification-time - (file-attributes file)))) - (image-dired-create-thumb file thumb-file)) - (create-image thumb-file))) - -(defun image-dired-insert-thumbnail (file original-file-name - associated-dired-buffer) - "Insert thumbnail image FILE. -Add text properties ORIGINAL-FILE-NAME and ASSOCIATED-DIRED-BUFFER." - (let (beg end) - (setq beg (point)) - (image-dired-insert-image - file - ;; Thumbnails are created asynchronously, so we might not yet - ;; have a file. But if it exists, it might have been cached from - ;; before and we should use it instead of our current settings. - (or (and (file-exists-p file) - (image-type-from-file-header file)) - (and (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - 'png) - 'jpeg) - image-dired-thumb-relief - image-dired-thumb-margin) - (setq end (point)) - (add-text-properties - beg end - (list 'image-dired-thumbnail t - 'original-file-name original-file-name - 'associated-dired-buffer associated-dired-buffer - 'tags (image-dired-list-tags original-file-name) - 'mouse-face 'highlight - 'comment (image-dired-get-comment original-file-name))))) - -(defun image-dired-thumb-name (file) - "Return absolute file name for thumbnail FILE. -Depending on the value of `image-dired-thumbnail-storage', the -file name of the thumbnail will vary: -- For `use-image-dired-dir', make a SHA1-hash of the image file's - directory name and add that to make the thumbnail file name - unique. -- For `per-directory' storage, just add a subdirectory. -- For `standard' storage, produce the file name according to the - Thumbnail Managing Standard. Among other things, an MD5-hash - of the image file's directory name will be added to the - filename. -See also `image-dired-thumbnail-storage'." - (cond ((memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - (let ((thumbdir (cl-case image-dired-thumbnail-storage - (standard "thumbnails/normal") - (standard-large "thumbnails/large") - (standard-x-large "thumbnails/x-large") - (standard-xx-large "thumbnails/xx-large")))) - (expand-file-name - ;; MD5 is mandated by the Thumbnail Managing Standard. - (concat (md5 (concat "file://" (expand-file-name file))) ".png") - (expand-file-name thumbdir (xdg-cache-home))))) - ((eq 'use-image-dired-dir image-dired-thumbnail-storage) - (let* ((f (expand-file-name file)) - (hash - (md5 (file-name-as-directory (file-name-directory f))))) - (format "%s%s%s.thumb.%s" - (file-name-as-directory (expand-file-name (image-dired-dir))) - (file-name-base f) - (if hash (concat "_" hash) "") - (file-name-extension f)))) - ((eq 'per-directory image-dired-thumbnail-storage) - (let ((f (expand-file-name file))) - (format "%s.image-dired/%s.thumb.%s" - (file-name-directory f) - (file-name-base f) - (file-name-extension f)))))) - -(defun image-dired--check-executable-exists (executable) - (unless (executable-find (symbol-value executable)) - (error "Executable %S not found" executable))) - - -;;; Creating thumbnails - -(defun image-dired-thumb-size (dimension) - "Return thumb size depending on `image-dired-thumbnail-storage'. -DIMENSION should be either the symbol `width' or `height'." - (cond - ((eq 'standard image-dired-thumbnail-storage) 128) - ((eq 'standard-large image-dired-thumbnail-storage) 256) - ((eq 'standard-x-large image-dired-thumbnail-storage) 512) - ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) - (t (cl-ecase dimension - (width image-dired-thumb-width) - (height image-dired-thumb-height))))) - -(defvar image-dired--generate-thumbs-start nil - "Time when `display-thumbs' was called.") - -(defvar image-dired-queue nil - "List of items in the queue. -Each item has the form (ORIGINAL-FILE TARGET-FILE).") - -(defvar image-dired-queue-active-jobs 0 - "Number of active jobs in `image-dired-queue'.") - -(defvar image-dired-queue-active-limit (min 4 (max 2 (/ (num-processors) 2))) - "Maximum number of concurrent jobs permitted for generating images. -Increase at own risk. If you want to experiment with this, -consider setting `image-dired-debug' to a non-nil value to see -the time spent on generating thumbnails. Run `image-clear-cache' -and remove the cached thumbnail files between each trial run.") - -(defun image-dired-pngnq-thumb (spec) - "Quantize thumbnail described by format SPEC with pngnq(1)." - (let ((process - (apply #'start-process "image-dired-pngnq" nil - image-dired-cmd-pngnq-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-pngnq-options)))) - (setf (process-sentinel process) - (lambda (process status) - (if (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - ;; Pass off to pngcrush, or just rename the - ;; THUMB-nq8.png file back to THUMB.png - (if (and image-dired-cmd-pngcrush-program - (executable-find image-dired-cmd-pngcrush-program)) - (image-dired-pngcrush-thumb spec) - (let ((nq8 (cdr (assq ?q spec))) - (thumb (cdr (assq ?t spec)))) - (rename-file nq8 thumb t))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))))) - process)) - -(defun image-dired-pngcrush-thumb (spec) - "Optimize thumbnail described by format SPEC with pngcrush(1)." - ;; If pngnq wasn't run, then the THUMB-nq8.png file does not exist. - ;; pngcrush needs an infile and outfile, so we just copy THUMB to - ;; THUMB-nq8.png and use the latter as a temp file. - (when (not image-dired-cmd-pngnq-program) - (let ((temp (cdr (assq ?q spec))) - (thumb (cdr (assq ?t spec)))) - (copy-file thumb temp))) - (let ((process - (apply #'start-process "image-dired-pngcrush" nil - image-dired-cmd-pngcrush-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-pngcrush-options)))) - (setf (process-sentinel process) - (lambda (process status) - (unless (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))) - (when (memq (process-status process) '(exit signal)) - (let ((temp (cdr (assq ?q spec)))) - (delete-file temp))))) - process)) - -(defun image-dired-optipng-thumb (spec) - "Optimize thumbnail described by format SPEC with optipng(1)." - (let ((process - (apply #'start-process "image-dired-optipng" nil - image-dired-cmd-optipng-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-optipng-options)))) - (setf (process-sentinel process) - (lambda (process status) - (unless (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))))) - process)) - -(defun image-dired-create-thumb-1 (original-file thumbnail-file) - "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE." - (image-dired--check-executable-exists - 'image-dired-cmd-create-thumbnail-program) - (let* ((width (int-to-string (image-dired-thumb-size 'width))) - (height (int-to-string (image-dired-thumb-size 'height))) - (modif-time (format-time-string - "%s" (file-attribute-modification-time - (file-attributes original-file)))) - (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png" - thumbnail-file)) - (spec - (list - (cons ?w width) - (cons ?h height) - (cons ?m modif-time) - (cons ?f original-file) - (cons ?q thumbnail-nq8-file) - (cons ?t thumbnail-file))) - (thumbnail-dir (file-name-directory thumbnail-file)) - process) - (when (not (file-exists-p thumbnail-dir)) - (with-file-modes #o700 - (make-directory thumbnail-dir t)) - (message "Thumbnail directory created: %s" thumbnail-dir)) - - ;; Thumbnail file creation processes begin here and are marshaled - ;; in a queue by `image-dired-create-thumb'. - (setq process - (apply #'start-process "image-dired-create-thumbnail" nil - image-dired-cmd-create-thumbnail-program - (mapcar - (lambda (arg) (format-spec arg spec)) - (if (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - image-dired-cmd-create-standard-thumbnail-options - image-dired-cmd-create-thumbnail-options)))) - - (setf (process-sentinel process) - (lambda (process status) - ;; Trigger next in queue once a thumbnail has been created - (cl-decf image-dired-queue-active-jobs) - (image-dired-thumb-queue-run) - (when (= image-dired-queue-active-jobs 0) - (image-dired-debug-message - (format-time-string - "Generated thumbnails in %s.%3N seconds" - (time-subtract nil - image-dired--generate-thumbs-start)))) - (if (not (and (eq (process-status process) 'exit) - (zerop (process-exit-status process)))) - (message "Thumb could not be created for %s: %s" - (abbreviate-file-name original-file) - (string-replace "\n" "" status)) - (set-file-modes thumbnail-file #o600) - (clear-image-cache thumbnail-file) - ;; PNG thumbnail has been created since we are - ;; following the XDG thumbnail spec, so try to optimize - (when (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - (cond - ((and image-dired-cmd-pngnq-program - (executable-find image-dired-cmd-pngnq-program)) - (image-dired-pngnq-thumb spec)) - ((and image-dired-cmd-pngcrush-program - (executable-find image-dired-cmd-pngcrush-program)) - (image-dired-pngcrush-thumb spec)) - ((and image-dired-cmd-optipng-program - (executable-find image-dired-cmd-optipng-program)) - (image-dired-optipng-thumb spec))))))) - process)) - -(defun image-dired-thumb-queue-run () - "Run a queued job if one exists and not too many jobs are running. -Queued items live in `image-dired-queue'." - (while (and image-dired-queue - (< image-dired-queue-active-jobs - image-dired-queue-active-limit)) - (cl-incf image-dired-queue-active-jobs) - (apply #'image-dired-create-thumb-1 (pop image-dired-queue)))) - -(defun image-dired-create-thumb (original-file thumbnail-file) - "Add a job for generating ORIGINAL-FILE thumbnail to `image-dired-queue'. -The new file will be named THUMBNAIL-FILE." - (setq image-dired-queue - (nconc image-dired-queue - (list (list original-file thumbnail-file)))) - (run-at-time 0 nil #'image-dired-thumb-queue-run)) - -(defmacro image-dired--with-marked (&rest body) - "Eval BODY with point on each marked thumbnail. -If no marked file could be found, execute BODY on the current -thumbnail." - `(with-current-buffer image-dired-thumbnail-buffer - (let (found) - (save-mark-and-excursion - (goto-char (point-min)) - (while (not (eobp)) - (when (image-dired-thumb-file-marked-p) - (setq found t) - ,@body) - (forward-char))) - (unless found - ,@body)))) - -;;;###autoload -(defun image-dired-dired-toggle-marked-thumbs (&optional arg) - "Toggle thumbnails in front of file names in the Dired buffer. -If no marked file could be found, insert or hide thumbnails on the -current line. ARG, if non-nil, specifies the files to use instead -of the marked files. If ARG is an integer, use the next ARG (or -previous -ARG, if ARG<0) files." - (interactive "P") - (dired-map-over-marks - (let ((image-pos (dired-move-to-filename)) - (image-file (dired-get-filename nil t)) - thumb-file - overlay) - (when (and image-file - (string-match-p (image-file-name-regexp) image-file)) - (setq thumb-file (image-dired-get-thumbnail-image image-file)) - ;; If image is not already added, then add it. - (let ((thumb-ov (cl-loop for ov in (overlays-in (point) (1+ (point))) - if (overlay-get ov 'thumb-file) return ov))) - (if thumb-ov - (delete-overlay thumb-ov) - (put-image thumb-file image-pos) - (setq overlay - (cl-loop for ov in (overlays-in (point) (1+ (point))) - if (overlay-get ov 'put-image) return ov)) - (overlay-put overlay 'image-file image-file) - (overlay-put overlay 'thumb-file thumb-file))))) - arg ; Show or hide image on ARG next files. - 'show-progress) ; Update dired display after each image is updated. - (add-hook 'dired-after-readin-hook - 'image-dired-dired-after-readin-hook nil t)) - -(defun image-dired-dired-after-readin-hook () - "Relocate existing thumbnail overlays in Dired buffer after reverting. -Move them to their corresponding files if they still exist. -Otherwise, delete overlays." - (mapc (lambda (overlay) - (when (overlay-get overlay 'put-image) - (let* ((image-file (overlay-get overlay 'image-file)) - (image-pos (dired-goto-file image-file))) - (if image-pos - (move-overlay overlay image-pos image-pos) - (delete-overlay overlay))))) - (overlays-in (point-min) (point-max)))) - -(defun image-dired-next-line-and-display () - "Move to next Dired line and display thumbnail image." - (interactive) - (dired-next-line 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-previous-line-and-display () - "Move to previous Dired line and display thumbnail image." - (interactive) - (dired-previous-line 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-toggle-append-browsing () - "Toggle `image-dired-append-when-browsing'." - (interactive) - (setq image-dired-append-when-browsing - (not image-dired-append-when-browsing)) - (message "Append browsing %s" - (if image-dired-append-when-browsing - "on" - "off"))) - -(defun image-dired-mark-and-display-next () - "Mark current file in Dired and display next thumbnail image." - (interactive) - (dired-mark 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-toggle-dired-display-properties () - "Toggle `image-dired-dired-disp-props'." - (interactive) - (setq image-dired-dired-disp-props - (not image-dired-dired-disp-props)) - (message "Dired display properties %s" - (if image-dired-dired-disp-props - "on" - "off"))) - -(defvar image-dired-thumbnail-buffer "*image-dired*" - "Image-Dired's thumbnail buffer.") - -(defun image-dired-create-thumbnail-buffer () - "Create thumb buffer and set `image-dired-thumbnail-mode'." - (let ((buf (get-buffer-create image-dired-thumbnail-buffer))) - (with-current-buffer buf - (setq buffer-read-only t) - (if (not (eq major-mode 'image-dired-thumbnail-mode)) - (image-dired-thumbnail-mode))) - buf)) - -(defvar image-dired-display-image-buffer "*image-dired-display-image*" - "Where larger versions of the images are display.") - -(defvar image-dired-saved-window-configuration nil - "Saved window configuration.") - -;;;###autoload -(defun image-dired-dired-with-window-configuration (dir &optional arg) - "Open directory DIR and create a default window configuration. - -Convenience command that: - - - Opens Dired in folder DIR - - Splits windows in most useful (?) way - - Sets `truncate-lines' to t - -After the command has finished, you would typically mark some -image files in Dired and type -\\[image-dired-display-thumbs] (`image-dired-display-thumbs'). - -If called with prefix argument ARG, skip splitting of windows. - -The current window configuration is saved and can be restored by -calling `image-dired-restore-window-configuration'." - (interactive "DDirectory: \nP") - (let ((buf (image-dired-create-thumbnail-buffer)) - (buf2 (get-buffer-create image-dired-display-image-buffer))) - (setq image-dired-saved-window-configuration - (current-window-configuration)) - (dired dir) - (delete-other-windows) - (when (not arg) - (split-window-right) - (setq truncate-lines t) - (save-excursion - (other-window 1) - (pop-to-buffer-same-window buf) - (select-window (split-window-below)) - (pop-to-buffer-same-window buf2) - (other-window -2))))) - -(defun image-dired-restore-window-configuration () - "Restore window configuration. -Restore any changes to the window configuration made by calling -`image-dired-dired-with-window-configuration'." - (interactive nil image-dired-thumbnail-mode) - (if image-dired-saved-window-configuration - (set-window-configuration image-dired-saved-window-configuration) - (message "No saved window configuration"))) - -(defun image-dired--line-up-with-method () - "Line up thumbnails according to `image-dired-line-up-method'." - (cond ((eq 'dynamic image-dired-line-up-method) - (image-dired-line-up-dynamic)) - ((eq 'fixed image-dired-line-up-method) - (image-dired-line-up)) - ((eq 'interactive image-dired-line-up-method) - (image-dired-line-up-interactive)) - ((eq 'none image-dired-line-up-method) - nil) - (t - (image-dired-line-up-dynamic)))) - -;;;###autoload -(defun image-dired-display-thumbs (&optional arg append do-not-pop) - "Display thumbnails of all marked files, in `image-dired-thumbnail-buffer'. -If a thumbnail image does not exist for a file, it is created on the -fly. With prefix argument ARG, display only thumbnail for file at -point (this is useful if you have marked some files but want to show -another one). - -Recommended usage is to split the current frame horizontally so that -you have the Dired buffer in the left window and the -`image-dired-thumbnail-buffer' buffer in the right window. - -With optional argument APPEND, append thumbnail to thumbnail buffer -instead of erasing it first. - -Optional argument DO-NOT-POP controls if `pop-to-buffer' should be -used or not. If non-nil, use `display-buffer' instead of -`pop-to-buffer'. This is used from functions like -`image-dired-next-line-and-display' and -`image-dired-previous-line-and-display' where we do not want the -thumbnail buffer to be selected." - (interactive "P") - (setq image-dired--generate-thumbs-start (current-time)) - (let ((buf (image-dired-create-thumbnail-buffer)) - thumb-name files dired-buf) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (setq dired-buf (current-buffer)) - (with-current-buffer buf - (let ((inhibit-read-only t)) - (if (not append) - (erase-buffer) - (goto-char (point-max))) - (dolist (curr-file files) - (setq thumb-name (image-dired-thumb-name curr-file)) - (when (not (file-exists-p thumb-name)) - (image-dired-create-thumb curr-file thumb-name)) - (image-dired-insert-thumbnail thumb-name curr-file dired-buf))) - (if do-not-pop - (display-buffer buf) - (pop-to-buffer buf)) - (image-dired--line-up-with-method)))) - -;;;###autoload -(defun image-dired-show-all-from-dir (dir) - "Make a thumbnail buffer for all images in DIR and display it. -Any file matching `image-file-name-regexp' is considered an image -file. - -If the number of image files in DIR exceeds -`image-dired-show-all-from-dir-max-files', ask for confirmation -before creating the thumbnail buffer. If that variable is nil, -never ask for confirmation." - (interactive "DImage-Dired: ") - (dired dir) - (dired-mark-files-regexp (image-file-name-regexp)) - (let ((files (dired-get-marked-files nil nil nil t))) - (cond ((and (null (cdr files))) - (message "No image files in directory")) - ((or (not image-dired-show-all-from-dir-max-files) - (<= (length (cdr files)) image-dired-show-all-from-dir-max-files) - (and (> (length (cdr files)) image-dired-show-all-from-dir-max-files) - (y-or-n-p - (format - "Directory contains more than %d image files. Proceed?" - image-dired-show-all-from-dir-max-files)))) - (image-dired-display-thumbs) - (pop-to-buffer image-dired-thumbnail-buffer) - (setq default-directory dir) - (image-dired-unmark-all-marks)) - (t (message "Image-Dired canceled"))))) - -;;;###autoload -(defalias 'image-dired 'image-dired-show-all-from-dir) - - -;;; Tags - (defun image-dired-sane-db-file () "Check if `image-dired-db-file' exists. If not, try to create it (including any parent directories). @@ -1212,962 +183,6 @@ With prefix argument ARG, remove tag from file at point." (image-dired-update-property 'tags (image-dired-list-tags (image-dired-original-file-name)))))) - -;;; Thumbnail mode (cont.) - -(defun image-dired-original-file-name () - "Get original file name for thumbnail or display image at point." - (get-text-property (point) 'original-file-name)) - -(defun image-dired-file-name-at-point () - "Get abbreviated file name for thumbnail or display image at point." - (let ((f (image-dired-original-file-name))) - (when f - (abbreviate-file-name f)))) - -(defun image-dired-associated-dired-buffer () - "Get associated Dired buffer at point." - (get-text-property (point) 'associated-dired-buffer)) - -(defun image-dired-get-buffer-window (buf) - "Return window where buffer BUF is." - (get-window-with-predicate - (lambda (window) - (equal (window-buffer window) buf)) - nil t)) - -(defun image-dired-track-original-file () - "Track the original file in the associated Dired buffer. -See documentation for `image-dired-toggle-movement-tracking'. -Interactive use only useful if `image-dired-track-movement' is nil." - (interactive) - (let* ((dired-buf (image-dired-associated-dired-buffer)) - (file-name (image-dired-original-file-name)) - (window (image-dired-get-buffer-window dired-buf))) - (and (buffer-live-p dired-buf) file-name - (with-current-buffer dired-buf - (if (not (dired-goto-file file-name)) - (message "Could not track file") - (if window (set-window-point window (point)))))))) - -(defun image-dired-toggle-movement-tracking () - "Turn on and off `image-dired-track-movement'. -Tracking of the movements between thumbnail and Dired buffer so that -they are \"mirrored\" in the dired buffer. When this is on, moving -around in the thumbnail or dired buffer will find the matching -position in the other buffer." - (interactive) - (setq image-dired-track-movement (not image-dired-track-movement)) - (message "Movement tracking %s" (if image-dired-track-movement "on" "off"))) - -(defun image-dired-track-thumbnail () - "Track current Dired file's thumb in `image-dired-thumbnail-buffer'. -This is almost the same as what `image-dired-track-original-file' does, -but the other way around." - (let ((file (dired-get-filename)) - prop-val found window) - (when (get-buffer image-dired-thumbnail-buffer) - (with-current-buffer image-dired-thumbnail-buffer - (goto-char (point-min)) - (while (and (not (eobp)) - (not found)) - (if (and (setq prop-val - (get-text-property (point) 'original-file-name)) - (string= prop-val file)) - (setq found t)) - (if (not found) - (forward-char 1))) - (when found - (if (setq window (image-dired-thumbnail-window)) - (set-window-point window (point))) - (image-dired-update-header-line)))))) - -(defun image-dired-dired-next-line (&optional arg) - "Call `dired-next-line', then track thumbnail. -This can safely replace `dired-next-line'. -With prefix argument, move ARG lines." - (interactive "P") - (dired-next-line (or arg 1)) - (if image-dired-track-movement - (image-dired-track-thumbnail))) - -(defun image-dired-dired-previous-line (&optional arg) - "Call `dired-previous-line', then track thumbnail. -This can safely replace `dired-previous-line'. -With prefix argument, move ARG lines." - (interactive "P") - (dired-previous-line (or arg 1)) - (if image-dired-track-movement - (image-dired-track-thumbnail))) - -(defun image-dired--display-thumb-properties-fun () - (let ((old-buf (current-buffer)) - (old-point (point))) - (lambda () - (when (and (equal (current-buffer) old-buf) - (= (point) old-point)) - (ignore-errors - (image-dired-update-header-line)))))) - -(defun image-dired-forward-image (&optional arg wrap-around) - "Move to next image and display properties. -Optional prefix ARG says how many images to move; the default is -one image. Negative means move backwards. -On reaching end or beginning of buffer, stop and show a message. - -If optional argument WRAP-AROUND is non-nil, wrap around: if -point is on the last image, move to the last one and vice versa." - (interactive "p") - (setq arg (or arg 1)) - (let (pos) - (dotimes (_ (abs arg)) - (if (and (not (if (> arg 0) (eobp) (bobp))) - (save-excursion - (forward-char (if (> arg 0) 1 -1)) - (while (and (not (if (> arg 0) (eobp) (bobp))) - (not (image-dired-image-at-point-p))) - (forward-char (if (> arg 0) 1 -1))) - (setq pos (point)) - (image-dired-image-at-point-p))) - (progn (goto-char pos) - (image-dired-update-header-line)) - (if wrap-around - (progn (goto-char (if (> arg 0) - (point-min) - ;; There are two spaces after the last image. - (- (point-max) 2))) - (image-dired-update-header-line)) - (message "At %s image" (if (> arg 0) "last" "first")) - (run-at-time 1 nil (image-dired--display-thumb-properties-fun)))))) - (when image-dired-track-movement - (image-dired-track-original-file))) - -(defun image-dired-backward-image (&optional arg) - "Move to previous image and display properties. -Optional prefix ARG says how many images to move; the default is -one image. Negative means move forward. -On reaching end or beginning of buffer, stop and show a message." - (interactive "p") - (image-dired-forward-image (- (or arg 1)))) - -(defun image-dired-next-line () - "Move to next line and display properties." - (interactive nil image-dired-thumbnail-mode) - (let ((goal-column (current-column))) - (forward-line 1) - (move-to-column goal-column)) - ;; If we end up in an empty spot, back up to the next thumbnail. - (if (not (image-dired-image-at-point-p)) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - - -(defun image-dired-previous-line () - "Move to previous line and display properties." - (interactive nil image-dired-thumbnail-mode) - (let ((goal-column (current-column))) - (forward-line -1) - (move-to-column goal-column)) - ;; If we end up in an empty spot, back up to the next - ;; thumbnail. This should only happen if the user deleted a - ;; thumbnail and did not refresh, so it is not very common. But we - ;; can handle it in a good manner, so why not? - (if (not (image-dired-image-at-point-p)) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-beginning-of-buffer () - "Move to the first image in the buffer and display properties." - (interactive nil image-dired-thumbnail-mode) - (goto-char (point-min)) - (while (and (not (image-at-point-p)) - (not (eobp))) - (forward-char 1)) - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-end-of-buffer () - "Move to the last image in the buffer and display properties." - (interactive nil image-dired-thumbnail-mode) - (goto-char (point-max)) - (while (and (not (image-at-point-p)) - (not (bobp))) - (forward-char -1)) - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-format-properties-string (buf file props comment) - "Format display properties. -BUF is the associated Dired buffer, FILE is the original image file -name, PROPS is a stringified list of tags and COMMENT is the image file's -comment." - (format-spec - image-dired-display-properties-format - (list - (cons ?b (or buf "")) - (cons ?f file) - (cons ?t (or props "")) - (cons ?c (or comment ""))))) - -(defun image-dired-update-header-line () - "Update image information in the header line." - (when (and (not (eobp)) - (memq major-mode '(image-dired-thumbnail-mode - image-dired-display-image-mode))) - (let ((file-name (file-name-nondirectory (image-dired-original-file-name))) - (dired-buf (buffer-name (image-dired-associated-dired-buffer))) - (props (mapconcat #'identity (get-text-property (point) 'tags) ", ")) - (comment (get-text-property (point) 'comment)) - (message-log-max nil)) - (if file-name - (setq header-line-format - (image-dired-format-properties-string - dired-buf - file-name - props - comment)))))) - -(defun image-dired-dired-file-marked-p (&optional marker) - "In Dired, return t if file on current line is marked. -If optional argument MARKER is non-nil, it is a character to look -for. The default is to look for `dired-marker-char'." - (setq marker (or marker dired-marker-char)) - (save-excursion - (beginning-of-line) - (and (looking-at dired-re-mark) - (= (aref (match-string 0) 0) marker)))) - -(defun image-dired-dired-file-flagged-p () - "In Dired, return t if file on current line is flagged for deletion." - (image-dired-dired-file-marked-p dired-del-marker)) - -(defmacro image-dired--with-thumbnail-buffer (&rest body) - (declare (indent defun) (debug t)) - `(if-let ((buf (get-buffer image-dired-thumbnail-buffer))) - (with-current-buffer buf - (if-let ((win (get-buffer-window buf))) - (with-selected-window win - ,@body) - ,@body)) - (user-error "No such buffer: %s" image-dired-thumbnail-buffer))) - -(defmacro image-dired--on-file-in-dired-buffer (&rest body) - "Run BODY with point on file at point in Dired buffer. -Should be called from commands in `image-dired-thumbnail-mode'." - (declare (indent defun) (debug t)) - `(let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (if (not (and dired-buf file-name)) - (message "No image, or image with correct properties, at point") - (with-current-buffer dired-buf - (when (dired-goto-file file-name) - ,@body - (image-dired-thumb-update-marks)))))) - -(defmacro image-dired--do-mark-command (maybe-next &rest body) - "Helper macro for the mark, unmark and flag commands. -Run BODY in Dired buffer. -If optional argument MAYBE-NEXT is non-nil, show next image -according to `image-dired-marking-shows-next'." - (declare (indent defun) (debug t)) - `(image-dired--with-thumbnail-buffer - (image-dired--on-file-in-dired-buffer - ,@body) - ,(when maybe-next - '(if image-dired-marking-shows-next - (image-dired-display-next-thumbnail-original) - (image-dired-next-line))))) - -(defun image-dired-mark-thumb-original-file () - "Mark original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-mark 1))) - -(defun image-dired-unmark-thumb-original-file () - "Unmark original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-unmark 1))) - -(defun image-dired-flag-thumb-original-file () - "Flag original image file for deletion in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-flag-file-deletion 1))) - -(defun image-dired-toggle-mark-thumb-original-file () - "Toggle mark on original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command nil - (if (image-dired-dired-file-marked-p) - (dired-unmark 1) - (dired-mark 1)))) - -(defun image-dired-unmark-all-marks () - "Remove all marks from all files in associated Dired buffer. -Also update the marks in the thumbnail buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command nil - (dired-unmark-all-marks)) - (image-dired--with-thumbnail-buffer - (image-dired-thumb-update-marks))) - -(defun image-dired-jump-original-dired-buffer () - "Jump to the Dired buffer associated with the current image file. -You probably want to use this together with -`image-dired-track-original-file'." - (interactive nil image-dired-thumbnail-mode) - (let ((buf (image-dired-associated-dired-buffer)) - window frame) - (setq window (image-dired-get-buffer-window buf)) - (if window - (progn - (if (not (equal (selected-frame) (setq frame (window-frame window)))) - (select-frame-set-input-focus frame)) - (select-window window)) - (message "Associated dired buffer not visible")))) - -;;;###autoload -(defun image-dired-jump-thumbnail-buffer () - "Jump to thumbnail buffer." - (interactive) - (let ((window (image-dired-thumbnail-window)) - frame) - (if window - (progn - (if (not (equal (selected-frame) (setq frame (window-frame window)))) - (select-frame-set-input-focus frame)) - (select-window window)) - (message "Thumbnail buffer not visible")))) - -(defvar image-dired-thumbnail-mode-line-up-map - (let ((map (make-sparse-keymap))) - ;; map it to "g" so that the user can press it more quickly - (define-key map "g" #'image-dired-line-up-dynamic) - ;; "f" for "fixed" number of thumbs per row - (define-key map "f" #'image-dired-line-up) - ;; "i" for "interactive" - (define-key map "i" #'image-dired-line-up-interactive) - map) - "Keymap for line-up commands in `image-dired-thumbnail-mode'.") - -(defvar image-dired-thumbnail-mode-tag-map - (let ((map (make-sparse-keymap))) - ;; map it to "t" so that the user can press it more quickly - (define-key map "t" #'image-dired-tag-thumbnail) - ;; "r" for "remove" - (define-key map "r" #'image-dired-tag-thumbnail-remove) - map) - "Keymap for tag commands in `image-dired-thumbnail-mode'.") - -(defvar image-dired-thumbnail-mode-map - (let ((map (make-sparse-keymap))) - (define-key map [right] #'image-dired-forward-image) - (define-key map [left] #'image-dired-backward-image) - (define-key map [up] #'image-dired-previous-line) - (define-key map [down] #'image-dired-next-line) - (define-key map "\C-f" #'image-dired-forward-image) - (define-key map "\C-b" #'image-dired-backward-image) - (define-key map "\C-p" #'image-dired-previous-line) - (define-key map "\C-n" #'image-dired-next-line) - - (define-key map "<" #'image-dired-beginning-of-buffer) - (define-key map ">" #'image-dired-end-of-buffer) - (define-key map (kbd "M-<") #'image-dired-beginning-of-buffer) - (define-key map (kbd "M->") #'image-dired-end-of-buffer) - - (define-key map "d" #'image-dired-flag-thumb-original-file) - (define-key map [delete] #'image-dired-flag-thumb-original-file) - (define-key map "m" #'image-dired-mark-thumb-original-file) - (define-key map "u" #'image-dired-unmark-thumb-original-file) - (define-key map "U" #'image-dired-unmark-all-marks) - (define-key map "." #'image-dired-track-original-file) - (define-key map [tab] #'image-dired-jump-original-dired-buffer) - - ;; add line-up map - (define-key map "g" image-dired-thumbnail-mode-line-up-map) - ;; add tag map - (define-key map "t" image-dired-thumbnail-mode-tag-map) - - (define-key map "\C-m" #'image-dired-display-thumbnail-original-image) - (define-key map [C-return] #'image-dired-thumbnail-display-external) - - (define-key map "L" #'image-dired-rotate-original-left) - (define-key map "R" #'image-dired-rotate-original-right) - - (define-key map "D" #'image-dired-thumbnail-set-image-description) - (define-key map "S" #'image-dired-slideshow-start) - (define-key map "\C-d" #'image-dired-delete-char) - (define-key map " " #'image-dired-display-next-thumbnail-original) - (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) - (define-key map "c" #'image-dired-comment-thumbnail) - - ;; Mouse - (define-key map [mouse-2] #'image-dired-mouse-display-image) - (define-key map [mouse-1] #'image-dired-mouse-select-thumbnail) - (define-key map [mouse-3] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-1] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-2] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-3] #'image-dired-mouse-select-thumbnail) - ;; Seems I must first set C-down-mouse-1 to undefined, or else it - ;; will trigger the buffer menu. If I try to instead bind - ;; C-down-mouse-1 to `image-dired-mouse-toggle-mark', I get a message - ;; about C-mouse-1 not being defined afterwards. Annoying, but I - ;; probably do not completely understand mouse events. - (define-key map [C-down-mouse-1] #'undefined) - (define-key map [C-mouse-1] #'image-dired-mouse-toggle-mark) - map) - "Keymap for `image-dired-thumbnail-mode'.") - -(easy-menu-define image-dired-thumbnail-mode-menu image-dired-thumbnail-mode-map - "Menu for `image-dired-thumbnail-mode'." - '("Image-Dired" - ["Display image" image-dired-display-thumbnail-original-image] - ["Display in external viewer" image-dired-thumbnail-display-external] - ["Jump to Dired buffer" image-dired-jump-original-dired-buffer] - "---" - ["Mark image" image-dired-mark-thumb-original-file] - ["Unmark image" image-dired-unmark-thumb-original-file] - ["Unmark all images" image-dired-unmark-all-marks] - ["Flag for deletion" image-dired-flag-thumb-original-file] - ["Delete marked images" image-dired-delete-marked] - "---" - ["Rotate original right" image-dired-rotate-original-right] - ["Rotate original left" image-dired-rotate-original-left] - "---" - ["Comment thumbnail" image-dired-comment-thumbnail] - ["Tag current or marked thumbnails" image-dired-tag-thumbnail] - ["Remove tag from current or marked thumbnails" - image-dired-tag-thumbnail-remove] - ["Start slideshow" image-dired-slideshow-start] - "---" - ("View Options" - ["Toggle movement tracking" image-dired-toggle-movement-tracking - :style toggle - :selected image-dired-track-movement] - "---" - ["Line up thumbnails" image-dired-line-up] - ["Dynamic line up" image-dired-line-up-dynamic] - ["Refresh thumb" image-dired-refresh-thumb]) - ["Quit" quit-window])) - -(defvar image-dired-display-image-mode-map - (let ((map (make-sparse-keymap))) - (define-key map "S" #'image-dired-slideshow-start) - (define-key map (kbd "SPC") #'image-dired-display-next-thumbnail-original) - (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) - (define-key map "n" #'image-dired-display-next-thumbnail-original) - (define-key map "p" #'image-dired-display-previous-thumbnail-original) - (define-key map "m" #'image-dired-mark-thumb-original-file) - (define-key map "d" #'image-dired-flag-thumb-original-file) - (define-key map "u" #'image-dired-unmark-thumb-original-file) - (define-key map "U" #'image-dired-unmark-all-marks) - ;; Disable keybindings from `image-mode-map' that doesn't make sense here. - (define-key map "o" nil) ; image-save - map) - "Keymap for `image-dired-display-image-mode'.") - -(define-derived-mode image-dired-thumbnail-mode - special-mode "image-dired-thumbnail" - "Browse and manipulate thumbnail images using Dired. -Use `image-dired-minor-mode' to get a nice setup." - :interactive nil - (buffer-disable-undo) - (add-hook 'file-name-at-point-functions 'image-dired-file-name-at-point nil t) - (setq-local window-resize-pixelwise t) - (setq-local bookmark-make-record-function #'image-dired-bookmark-make-record) - ;; Use approximately as much vertical spacing as horizontal. - (setq-local line-spacing (frame-char-width))) - - -;;; Display image mode - -(define-derived-mode image-dired-display-image-mode - image-mode "image-dired-image-display" - "Mode for displaying and manipulating original image. -Resized or in full-size." - :interactive nil - (add-hook 'file-name-at-point-functions #'image-dired-file-name-at-point nil t)) - -(defvar image-dired-minor-mode-map - (let ((map (make-sparse-keymap))) - ;; (set-keymap-parent map dired-mode-map) - ;; Hijack previous and next line movement. Let C-p and C-b be - ;; though... - (define-key map "p" #'image-dired-dired-previous-line) - (define-key map "n" #'image-dired-dired-next-line) - (define-key map [up] #'image-dired-dired-previous-line) - (define-key map [down] #'image-dired-dired-next-line) - - (define-key map (kbd "C-S-n") #'image-dired-next-line-and-display) - (define-key map (kbd "C-S-p") #'image-dired-previous-line-and-display) - (define-key map (kbd "C-S-m") #'image-dired-mark-and-display-next) - - (define-key map "\C-td" #'image-dired-display-thumbs) - (define-key map [tab] #'image-dired-jump-thumbnail-buffer) - (define-key map "\C-ti" #'image-dired-dired-display-image) - (define-key map "\C-tx" #'image-dired-dired-display-external) - (define-key map "\C-ta" #'image-dired-display-thumbs-append) - (define-key map "\C-t." #'image-dired-display-thumb) - (define-key map "\C-tc" #'image-dired-dired-comment-files) - (define-key map "\C-tf" #'image-dired-mark-tagged-files) - map) - "Keymap for `image-dired-minor-mode'.") - -(easy-menu-define image-dired-minor-mode-menu image-dired-minor-mode-map - "Menu for `image-dired-minor-mode'." - '("Image-dired" - ["Display thumb for next file" image-dired-next-line-and-display] - ["Display thumb for previous file" image-dired-previous-line-and-display] - ["Mark and display next" image-dired-mark-and-display-next] - "---" - ["Create thumbnails for marked files" image-dired-create-thumbs] - "---" - ["Display thumbnails append" image-dired-display-thumbs-append] - ["Display this thumbnail" image-dired-display-thumb] - ["Display image" image-dired-dired-display-image] - ["Display in external viewer" image-dired-dired-display-external] - "---" - ["Toggle display properties" image-dired-toggle-dired-display-properties - :style toggle - :selected image-dired-dired-disp-props] - ["Toggle append browsing" image-dired-toggle-append-browsing - :style toggle - :selected image-dired-append-when-browsing] - ["Toggle movement tracking" image-dired-toggle-movement-tracking - :style toggle - :selected image-dired-track-movement] - "---" - ["Jump to thumbnail buffer" image-dired-jump-thumbnail-buffer] - ["Mark tagged files" image-dired-mark-tagged-files] - ["Comment files" image-dired-dired-comment-files] - ["Copy with EXIF file name" image-dired-copy-with-exif-file-name])) - -;;;###autoload -(define-minor-mode image-dired-minor-mode - "Setup easy-to-use keybindings for the commands to be used in Dired mode. -Note that n, p and and will be hijacked and bound to -`image-dired-dired-next-line' and `image-dired-dired-previous-line'." - :keymap image-dired-minor-mode-map) - -(declare-function clear-image-cache "image.c" (&optional filter)) - -(defun image-dired-create-thumbs (&optional arg) - "Create thumbnail images for all marked files in Dired. -With prefix argument ARG, create thumbnails even if they already exist -\(i.e. use this to refresh your thumbnails)." - (interactive "P") - (let (thumb-name) - (dolist (curr-file (dired-get-marked-files)) - (setq thumb-name (image-dired-thumb-name curr-file)) - ;; If the user overrides the exist check, we must clear the - ;; image cache so that if the user wants to display the - ;; thumbnail, it is not fetched from cache. - (when arg - (clear-image-cache (expand-file-name thumb-name))) - (when (or (not (file-exists-p thumb-name)) - arg) - (image-dired-create-thumb curr-file thumb-name))))) - - -;;; Slideshow - -(defcustom image-dired-slideshow-delay 5.0 - "Seconds to wait before showing the next image in a slideshow. -This is used by `image-dired-slideshow-start'." - :type 'float - :version "29.1") - -(define-obsolete-variable-alias 'image-dired-slideshow-timer - 'image-dired--slideshow-timer "29.1") -(defvar image-dired--slideshow-timer nil - "Slideshow timer.") - -(defvar image-dired--slideshow-initial nil) - -(defun image-dired-slideshow-step () - "Step to next image in a slideshow." - (if-let ((buf (get-buffer image-dired-thumbnail-buffer))) - (with-current-buffer buf - (image-dired-display-next-thumbnail-original)) - (image-dired-slideshow-stop))) - -(defun image-dired-slideshow-start (&optional arg) - "Start a slideshow, waiting `image-dired-slideshow-delay' between images. - -With prefix argument ARG, wait that many seconds before going to -the next image. - -With a negative prefix argument, prompt user for the delay." - (interactive "P" image-dired-thumbnail-mode image-dired-display-image-mode) - (let ((delay (if (not arg) - image-dired-slideshow-delay - (if (> arg 0) - arg - (string-to-number - (let ((delay (number-to-string image-dired-slideshow-delay))) - (read-string - (format-prompt "Delay, in seconds. Decimals are accepted" delay)) - delay)))))) - (setq image-dired--slideshow-timer - (run-with-timer - 0 delay - 'image-dired-slideshow-step)) - (add-hook 'post-command-hook 'image-dired-slideshow-stop) - (setq image-dired--slideshow-initial t) - (message "Running slideshow; use any command to stop"))) - -(defun image-dired-slideshow-stop () - "Cancel slideshow." - ;; Make sure we don't immediately stop after - ;; `image-dired-slideshow-start'. - (unless image-dired--slideshow-initial - (remove-hook 'post-command-hook 'image-dired-slideshow-stop) - (cancel-timer image-dired--slideshow-timer)) - (setq image-dired--slideshow-initial nil)) - - -;;; Thumbnail mode (cont. 3) - -(defun image-dired-delete-char () - "Remove current thumbnail from thumbnail buffer and line up." - (interactive nil image-dired-thumbnail-mode) - (let ((inhibit-read-only t)) - (delete-char 1) - (when (= (following-char) ?\s) - (delete-char 1)))) - -;;;###autoload -(defun image-dired-display-thumbs-append () - "Append thumbnails to `image-dired-thumbnail-buffer'." - (interactive) - (image-dired-display-thumbs nil t t)) - -;;;###autoload -(defun image-dired-display-thumb () - "Shorthand for `image-dired-display-thumbs' with prefix argument." - (interactive) - (image-dired-display-thumbs t nil t)) - -(defun image-dired-line-up () - "Line up thumbnails according to `image-dired-thumbs-per-row'. -See also `image-dired-line-up-dynamic'." - (interactive) - (let ((inhibit-read-only t)) - (goto-char (point-min)) - (while (and (not (image-dired-image-at-point-p)) - (not (eobp))) - (delete-char 1)) - (while (not (eobp)) - (forward-char) - (while (and (not (image-dired-image-at-point-p)) - (not (eobp))) - (delete-char 1))) - (goto-char (point-min)) - (let ((seen 0) - (thumb-prev-pos 0) - (thumb-width-chars - (ceiling (/ (+ (* 2 image-dired-thumb-relief) - (* 2 image-dired-thumb-margin) - (image-dired-thumb-size 'width)) - (float (frame-char-width)))))) - (while (not (eobp)) - (forward-char) - (if (= image-dired-thumbs-per-row 1) - (insert "\n") - (cl-incf thumb-prev-pos thumb-width-chars) - (insert (propertize " " 'display `(space :align-to ,thumb-prev-pos))) - (cl-incf seen) - (when (and (= seen (- image-dired-thumbs-per-row 1)) - (not (eobp))) - (forward-char) - (insert "\n") - (setq seen 0) - (setq thumb-prev-pos 0))))) - (goto-char (point-min)))) - -(defun image-dired-line-up-dynamic () - "Line up thumbnails images dynamically. -Calculate how many thumbnails fit." - (interactive) - (let* ((char-width (frame-char-width)) - (width (image-dired-window-width-pixels (image-dired-thumbnail-window))) - (image-dired-thumbs-per-row - (/ width - (+ (* 2 image-dired-thumb-relief) - (* 2 image-dired-thumb-margin) - (image-dired-thumb-size 'width) - char-width)))) - (image-dired-line-up))) - -(defun image-dired-line-up-interactive () - "Line up thumbnails interactively. -Ask user how many thumbnails should be displayed per row." - (interactive) - (let ((image-dired-thumbs-per-row - (string-to-number (read-string "How many thumbs per row: ")))) - (if (not (> image-dired-thumbs-per-row 0)) - (message "Number must be greater than 0") - (image-dired-line-up)))) - -(defun image-dired-thumbnail-display-external () - "Display original image for thumbnail at point using external viewer." - (interactive) - (let ((file (image-dired-original-file-name))) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (if (not file) - (message "No original file name found") - (start-process "image-dired-thumb-external" nil - image-dired-external-viewer file))))) - -;;;###autoload -(defun image-dired-dired-display-external () - "Display file at point using an external viewer." - (interactive) - (let ((file (dired-get-filename))) - (start-process "image-dired-external" nil - image-dired-external-viewer file))) - -(defun image-dired-window-width-pixels (window) - "Calculate WINDOW width in pixels." - (* (window-width window) (frame-char-width))) - -(defun image-dired-display-window () - "Return window where `image-dired-display-image-buffer' is visible." - (get-window-with-predicate - (lambda (window) - (equal (buffer-name (window-buffer window)) image-dired-display-image-buffer)) - nil t)) - -(defun image-dired-thumbnail-window () - "Return window where `image-dired-thumbnail-buffer' is visible." - (get-window-with-predicate - (lambda (window) - (equal (buffer-name (window-buffer window)) image-dired-thumbnail-buffer)) - nil t)) - -(defun image-dired-associated-dired-buffer-window () - "Return window where associated Dired buffer is visible." - (let (buf) - (if (image-dired-image-at-point-p) - (progn - (setq buf (image-dired-associated-dired-buffer)) - (get-window-with-predicate - (lambda (window) - (equal (window-buffer window) buf)))) - (error "No thumbnail image at point")))) - -(defun image-dired-display-image (file &optional _ignored) - "Display image FILE in image buffer. -Use this when you want to display the image, in a new window. -The window will use `image-dired-display-image-mode' which is -based on `image-mode'." - (declare (advertised-calling-convention (file) "29.1")) - (setq file (expand-file-name file)) - (when (not (file-exists-p file)) - (error "No such file: %s" file)) - (let ((buf (get-buffer image-dired-display-image-buffer)) - (cur-win (selected-window))) - (when buf - (kill-buffer buf)) - (when-let ((buf (find-file-noselect file nil t))) - (pop-to-buffer buf) - (rename-buffer image-dired-display-image-buffer) - (image-dired-display-image-mode) - (select-window cur-win)))) - -(defun image-dired-display-thumbnail-original-image (&optional arg) - "Display current thumbnail's original image in display buffer. -See documentation for `image-dired-display-image' for more information. -With prefix argument ARG, display image in its original size." - (interactive "P") - (let ((file (image-dired-original-file-name))) - (if (not (string-equal major-mode "image-dired-thumbnail-mode")) - (message "Not in image-dired-thumbnail-mode") - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (if (not file) - (message "No original file name found") - (image-dired-display-image file arg)))))) - - -;;;###autoload -(defun image-dired-dired-display-image (&optional arg) - "Display current image file. -See documentation for `image-dired-display-image' for more information. -With prefix argument ARG, display image in its original size." - (interactive "P") - (image-dired-display-image (dired-get-filename) arg)) - -(defun image-dired-image-at-point-p () - "Return non-nil if there is an `image-dired' thumbnail at point." - (get-text-property (point) 'image-dired-thumbnail)) - -(defun image-dired-refresh-thumb () - "Force creation of new image for current thumbnail." - (interactive nil image-dired-thumbnail-mode) - (let* ((file (image-dired-original-file-name)) - (thumb (expand-file-name (image-dired-thumb-name file)))) - (clear-image-cache (expand-file-name thumb)) - (image-dired-create-thumb file thumb))) - -(defun image-dired-rotate-original (degrees) - "Rotate original image DEGREES degrees." - (image-dired--check-executable-exists - 'image-dired-cmd-rotate-original-program) - (if (not (image-dired-image-at-point-p)) - (message "No image at point") - (let* ((file (image-dired-original-file-name)) - (spec - (list - (cons ?d degrees) - (cons ?o (expand-file-name file)) - (cons ?t image-dired-temp-rotate-image-file)))) - (unless (eq 'jpeg (image-type file)) - (user-error "Only JPEG images can be rotated")) - (if (not (= 0 (apply #'call-process image-dired-cmd-rotate-original-program - nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-rotate-original-options)))) - (error "Could not rotate image") - (image-dired-display-image image-dired-temp-rotate-image-file) - (if (or (and image-dired-rotate-original-ask-before-overwrite - (y-or-n-p - "Rotate to temp file OK. Overwrite original image? ")) - (not image-dired-rotate-original-ask-before-overwrite)) - (progn - (copy-file image-dired-temp-rotate-image-file file t) - (image-dired-refresh-thumb)) - (image-dired-display-image file)))))) - -(defun image-dired-rotate-original-left () - "Rotate original image left (counter clockwise) 90 degrees. -The result of the rotation is displayed in the image display area -and a confirmation is needed before the original image files is -overwritten. This confirmation can be turned off using -`image-dired-rotate-original-ask-before-overwrite'." - (interactive) - (image-dired-rotate-original "270")) - -(defun image-dired-rotate-original-right () - "Rotate original image right (clockwise) 90 degrees. -The result of the rotation is displayed in the image display area -and a confirmation is needed before the original image files is -overwritten. This confirmation can be turned off using -`image-dired-rotate-original-ask-before-overwrite'." - (interactive) - (image-dired-rotate-original "90")) - - -;;; EXIF support - -(defun image-dired-get-exif-file-name (file) - "Use the image's EXIF information to return a unique file name. -The file name should be unique as long as you do not take more than -one picture per second. The original file name is suffixed at the end -for traceability. The format of the returned file name is -YYYY_MM_DD_HH_MM_DD_ORIG_FILE_NAME.jpg. Used from -`image-dired-copy-with-exif-file-name'." - (let (data no-exif-data-found) - (if (not (eq 'jpeg (image-type (expand-file-name file)))) - (setq no-exif-data-found t - data (format-time-string - "%Y:%m:%d %H:%M:%S" - (file-attribute-modification-time - (file-attributes (expand-file-name file))))) - (setq data (exif-field 'date-time (exif-parse-file - (expand-file-name file))))) - (while (string-match "[ :]" data) - (setq data (replace-match "_" nil nil data))) - (format "%s%s%s" data - (if no-exif-data-found - "_noexif_" - "_") - (file-name-nondirectory file)))) - -(defun image-dired-thumbnail-set-image-description () - "Set the ImageDescription EXIF tag for the original image. -If the image already has a value for this tag, it is used as the -default value at the prompt." - (interactive) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (let* ((file (image-dired-original-file-name)) - (old-value (or (exif-field 'description (exif-parse-file file)) ""))) - (if (eq 0 - (image-dired-set-exif-data file "ImageDescription" - (read-string "Value of ImageDescription: " - old-value))) - (message "Successfully wrote ImageDescription tag") - (error "Could not write ImageDescription tag"))))) - -(defun image-dired-set-exif-data (file tag-name tag-value) - "In FILE, set EXIF tag TAG-NAME to value TAG-VALUE." - (image-dired--check-executable-exists - 'image-dired-cmd-write-exif-data-program) - (let ((spec - (list - (cons ?f (expand-file-name file)) - (cons ?t tag-name) - (cons ?v tag-value)))) - (apply #'call-process image-dired-cmd-write-exif-data-program nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-write-exif-data-options)))) - -(defun image-dired-copy-with-exif-file-name () - "Copy file with unique name to main image directory. -Copy current or all marked files in Dired to a new file in your -main image directory, using a file name generated by -`image-dired-get-exif-file-name'. A typical usage for this if when -copying images from a digital camera into the image directory. - - Typically, you would open up the folder with the incoming -digital images, mark the files to be copied, and execute this -function. The result is a couple of new files in -`image-dired-main-image-directory' called -2005_05_08_12_52_00_dscn0319.jpg, -2005_05_08_14_27_45_dscn0320.jpg etc." - (interactive) - (let (new-name - (files (dired-get-marked-files))) - (mapc - (lambda (curr-file) - (setq new-name - (format "%s/%s" - (file-name-as-directory - (expand-file-name image-dired-main-image-directory)) - (image-dired-get-exif-file-name curr-file))) - (message "Copying %s to %s" curr-file new-name) - (copy-file curr-file new-name)) - files))) - -;;; Thumbnail mode (cont.) - -(defun image-dired-display-next-thumbnail-original (&optional arg) - "Move to the next image in the thumbnail buffer and display it. -With prefix ARG, move that many thumbnails." - (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--with-thumbnail-buffer - (image-dired-forward-image arg t) - (image-dired-display-thumbnail-original-image))) - -(defun image-dired-display-previous-thumbnail-original (arg) - "Move to the previous image in the thumbnail buffer and display it. -With prefix ARG, move that many thumbnails." - (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired-display-next-thumbnail-original (- arg))) - - -;;; Image Comments - (defun image-dired-write-comments (file-comments) "Write file comments to database. Write file comments to one or more files. @@ -2224,15 +239,6 @@ FILE-COMMENTS is an alist on the following form: (cons curr-file comment)) (dired-get-marked-files))))) -(defun image-dired-comment-thumbnail () - "Add comment to current thumbnail in thumbnail buffer." - (interactive) - (let* ((file (image-dired-original-file-name)) - (comment (image-dired-read-comment file))) - (image-dired-write-comments (list (cons file comment))) - (image-dired-update-property 'comment comment)) - (image-dired-update-header-line)) - (defun image-dired-read-comment (&optional file) "Read comment for an image. Optionally use old comment from FILE as initial value." @@ -2260,406 +266,6 @@ Optionally use old comment from FILE as initial value." comment-beg-pos comment-end-pos)))) comment))) -;;;###autoload -(defun image-dired-mark-tagged-files (regexp) - "Use REGEXP to mark files with matching tag. -A `tag' is a keyword, a piece of meta data, associated with an -image file and stored in image-dired's database file. This command -lets you input a regexp and this will be matched against all tags -on all image files in the database file. The files that have a -matching tag will be marked in the Dired buffer." - (interactive "sMark tagged files (regexp): ") - (image-dired-sane-db-file) - (let ((hits 0) - files) - (image-dired--with-db-file - ;; Collect matches - (while (search-forward-regexp "\\(^[^;\n]+\\);\\(.*\\)" nil t) - (let ((file (match-string 1)) - (tags (split-string (match-string 2) ";"))) - (when (seq-find (lambda (tag) - (string-match-p regexp tag)) - tags) - (push file files))))) - ;; Mark files - (dolist (curr-file files) - ;; I tried using `dired-mark-files-regexp' but it was waaaay to - ;; slow. Don't bother about hits found in other directories - ;; than the current one. - (when (string= (file-name-as-directory - (expand-file-name default-directory)) - (file-name-as-directory - (file-name-directory curr-file))) - (setq curr-file (file-name-nondirectory curr-file)) - (goto-char (point-min)) - (when (search-forward-regexp (format "\\s %s$" curr-file) nil t) - (setq hits (+ hits 1)) - (dired-mark 1)))) - (message "%d files with matching tag marked" hits))) - - - -;;; Mouse support - -(defun image-dired-mouse-display-image (event) - "Use mouse EVENT, call `image-dired-display-image' to display image. -Track this in associated Dired buffer if `image-dired-track-movement' is -non-nil." - (interactive "e") - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (unless (image-at-point-p) - (image-dired-backward-image)) - (let ((file (image-dired-original-file-name))) - (when file - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-display-image file)))) - -(defun image-dired-mouse-select-thumbnail (event) - "Use mouse EVENT to select thumbnail image. -Track this in associated Dired buffer if `image-dired-track-movement' is -non-nil." - (interactive "e") - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (unless (image-at-point-p) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - - - -;;; Dired marks and tags - -(defun image-dired-thumb-file-marked-p (&optional flagged) - "Check if file is marked in associated Dired buffer. -If optional argument FLAGGED is non-nil, check if file is flagged -for deletion instead." - (let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (when (and dired-buf file-name) - (with-current-buffer dired-buf - (save-excursion - (when (dired-goto-file file-name) - (if flagged - (image-dired-dired-file-flagged-p) - (image-dired-dired-file-marked-p)))))))) - -(defun image-dired-thumb-file-flagged-p () - "Check if file is flagged for deletion in associated Dired buffer." - (image-dired-thumb-file-marked-p t)) - -(defun image-dired-delete-marked () - "Delete current or marked thumbnails and associated images." - (interactive) - (image-dired--with-marked - (image-dired-delete-char) - (unless (bobp) - (backward-char))) - (image-dired--line-up-with-method) - (with-current-buffer (image-dired-associated-dired-buffer) - (dired-do-delete))) - -(defun image-dired-thumb-update-marks () - "Update the marks in the thumbnail buffer." - (when image-dired-thumb-visible-marks - (with-current-buffer image-dired-thumbnail-buffer - (save-mark-and-excursion - (goto-char (point-min)) - (let ((inhibit-read-only t)) - (while (not (eobp)) - (with-silent-modifications - (cond ((image-dired-thumb-file-marked-p) - (add-face-text-property (point) (1+ (point)) - 'image-dired-thumb-mark)) - ((image-dired-thumb-file-flagged-p) - (add-face-text-property (point) (1+ (point)) - 'image-dired-thumb-flagged)) - (t (remove-text-properties (point) (1+ (point)) - '(face image-dired-thumb-mark))))) - (forward-char))))))) - -(defun image-dired-mouse-toggle-mark-1 () - "Toggle Dired mark for current thumbnail. -Track this in associated Dired buffer if -`image-dired-track-movement' is non-nil." - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-toggle-mark-thumb-original-file)) - -(defun image-dired-mouse-toggle-mark (event) - "Use mouse EVENT to toggle Dired mark for thumbnail. -Toggle marks of all thumbnails in region, if it's active. -Track this in associated Dired buffer if -`image-dired-track-movement' is non-nil." - (interactive "e") - (if (use-region-p) - (let ((end (region-end))) - (save-excursion - (goto-char (region-beginning)) - (while (<= (point) end) - (when (image-dired-image-at-point-p) - (image-dired-mouse-toggle-mark-1)) - (forward-char)))) - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (image-dired-mouse-toggle-mark-1)) - (image-dired-thumb-update-marks)) - -(defun image-dired-dired-display-properties () - "Display properties for Dired file in the echo area." - (interactive) - (let* ((file (dired-get-filename)) - (file-name (file-name-nondirectory file)) - (dired-buf (buffer-name (current-buffer))) - (props (mapconcat #'identity (image-dired-list-tags file) ", ")) - (comment (image-dired-get-comment file)) - (message-log-max nil)) - (if file-name - (message "%s" - (image-dired-format-properties-string - dired-buf - file-name - props - comment))))) - - - -;;; Gallery support - -;; TODO: -;; * Support gallery creation when using per-directory thumbnail -;; storage. -;; * Enhanced gallery creation with basic CSS-support and pagination -;; of tag pages with many pictures. - -(defgroup image-dired-gallery nil - "Image-Dired support for generating a HTML gallery." - :prefix "image-dired-" - :group 'image-dired - :version "29.1") - -(defcustom image-dired-gallery-dir - (expand-file-name ".image-dired_gallery" image-dired-dir) - "Directory to store generated gallery html pages. -The name of this directory needs to be \"shared\" to the public -so that it can access the index.html page that image-dired creates." - :type 'directory) - -(defcustom image-dired-gallery-image-root-url - "https://example.org/image-diredpics" - "URL where the full size images are to be found on your web server. -Note that this URL has to be configured on your web server. -Image-Dired expects to find pictures in this directory. -This is used by `image-dired-gallery-generate'." - :type 'string - :version "29.1") - -(defcustom image-dired-gallery-thumb-image-root-url - "https://example.org/image-diredthumbs" - "URL where the thumbnail images are to be found on your web server. -Note that URL path has to be configured on your web server. -Image-Dired expects to find pictures in this directory. -This is used by `image-dired-gallery-generate'." - :type 'string - :version "29.1") - -(defcustom image-dired-gallery-hidden-tags - (list "private" "hidden" "pending") - "List of \"hidden\" tags. -Used by `image-dired-gallery-generate' to leave out \"hidden\" images." - :type '(repeat string)) - -(defvar image-dired-tag-file-list nil - "List to store tag-file structure.") - -(defvar image-dired-file-tag-list nil - "List to store file-tag structure.") - -(defvar image-dired-file-comment-list nil - "List to store file comments.") - -(defun image-dired--add-to-tag-file-lists (tag file) - "Helper function used from `image-dired--create-gallery-lists'. - -Add TAG to FILE in one list and FILE to TAG in the other. - -Lisp structures look like the following: - -image-dired-file-tag-list: - - ((\"filename1\" \"tag1\" \"tag2\" \"tag3\" ...) - (\"filename2\" \"tag1\" \"tag2\" \"tag3\" ...) - ...) - -image-dired-tag-file-list: - - ((\"tag1\" \"filename1\" \"filename2\" \"filename3\" ...) - (\"tag2\" \"filename1\" \"filename2\" \"filename3\" ...) - ...)" - ;; Add tag to file list - (let (curr) - (if image-dired-file-tag-list - (if (setq curr (assoc file image-dired-file-tag-list)) - (setcdr curr (cons tag (cdr curr))) - (setcdr image-dired-file-tag-list - (cons (list file tag) (cdr image-dired-file-tag-list)))) - (setq image-dired-file-tag-list (list (list file tag)))) - ;; Add file to tag list - (if image-dired-tag-file-list - (if (setq curr (assoc tag image-dired-tag-file-list)) - (if (not (member file curr)) - (setcdr curr (cons file (cdr curr)))) - (setcdr image-dired-tag-file-list - (cons (list tag file) (cdr image-dired-tag-file-list)))) - (setq image-dired-tag-file-list (list (list tag file)))))) - -(defun image-dired--add-to-file-comment-list (file comment) - "Helper function used from `image-dired--create-gallery-lists'. - -For FILE, add COMMENT to list. - -Lisp structure looks like the following: - -image-dired-file-comment-list: - - ((\"filename1\" . \"comment1\") - (\"filename2\" . \"comment2\") - ...)" - (if image-dired-file-comment-list - (if (not (assoc file image-dired-file-comment-list)) - (setcdr image-dired-file-comment-list - (cons (cons file comment) - (cdr image-dired-file-comment-list)))) - (setq image-dired-file-comment-list (list (cons file comment))))) - -(defun image-dired--create-gallery-lists () - "Create temporary lists used by `image-dired-gallery-generate'." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end beg file row-tags) - (setq image-dired-tag-file-list nil) - (setq image-dired-file-tag-list nil) - (setq image-dired-file-comment-list nil) - (goto-char (point-min)) - (while (search-forward-regexp "^." nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (setq beg (point)) - (unless (search-forward ";" end nil) - (error "Something is really wrong, check format of database")) - (setq row-tags (split-string - (buffer-substring beg end) ";")) - (setq file (car row-tags)) - (dolist (x (cdr row-tags)) - (if (not (string-match "^comment:\\(.*\\)" x)) - (image-dired--add-to-tag-file-lists x file) - (image-dired--add-to-file-comment-list file (match-string 1 x))))))) - ;; Sort tag-file list - (setq image-dired-tag-file-list - (sort image-dired-tag-file-list - (lambda (x y) - (string< (car x) (car y)))))) - -(defun image-dired--hidden-p (file) - "Return t if image FILE has a \"hidden\" tag." - (cl-loop for tag in (cdr (assoc file image-dired-file-tag-list)) - if (member tag image-dired-gallery-hidden-tags) return t)) - -(defun image-dired-gallery-generate () - "Generate gallery pages. -First we create a couple of Lisp structures from the database to make -it easier to generate, then HTML-files are created in -`image-dired-gallery-dir'." - (interactive) - (if (eq 'per-directory image-dired-thumbnail-storage) - (error "Currently, gallery generation is not supported \ -when using per-directory thumbnail file storage")) - (image-dired--create-gallery-lists) - (let ((tags image-dired-tag-file-list) - (index-file (format "%s/index.html" image-dired-gallery-dir)) - count tag tag-file - comment file-tags tag-link tag-link-list) - ;; Make sure gallery root exist - (if (file-exists-p image-dired-gallery-dir) - (if (not (file-directory-p image-dired-gallery-dir)) - (error "Variable image-dired-gallery-dir is not a directory")) - ;; FIXME: Should we set umask to 077 here, as we do for thumbnails? - (make-directory image-dired-gallery-dir)) - ;; Open index file - (with-temp-file index-file - (if (file-exists-p index-file) - (insert-file-contents index-file)) - (insert "\n") - (insert " \n") - (insert "

Image-Dired Gallery

\n") - (insert (format "

\n Gallery generated %s\n

\n" - (current-time-string))) - (insert "

Tag index

\n") - (setq count 1) - ;; Pre-generate list of all tag links - (dolist (curr tags) - (setq tag (car curr)) - (when (not (member tag image-dired-gallery-hidden-tags)) - (setq tag-link (format "%s" count tag)) - (if tag-link-list - (setq tag-link-list - (append tag-link-list (list (cons tag tag-link)))) - (setq tag-link-list (list (cons tag tag-link)))) - (setq count (1+ count)))) - (setq count 1) - ;; Main loop where we generated thumbnail pages per tag - (dolist (curr tags) - (setq tag (car curr)) - ;; Don't display hidden tags - (when (not (member tag image-dired-gallery-hidden-tags)) - ;; Insert link to tag page in index - (insert (format " %s
\n" (cdr (assoc tag tag-link-list)))) - ;; Open per-tag file - (setq tag-file (format "%s/%s.html" image-dired-gallery-dir count)) - (with-temp-file tag-file - (if (file-exists-p tag-file) - (insert-file-contents tag-file)) - (erase-buffer) - (insert "\n") - (insert " \n") - (insert "

Index

\n") - (insert (format "

Images with tag "%s"

" tag)) - ;; Main loop for files per tag page - (dolist (file (cdr curr)) - (unless (image-dired-hidden-p file) - ;; Insert thumbnail with link to full image - (insert - (format "\n" - image-dired-gallery-image-root-url - (file-name-nondirectory file) - image-dired-gallery-thumb-image-root-url - (file-name-nondirectory (image-dired-thumb-name file)) file)) - ;; Insert comment, if any - (if (setq comment (cdr (assoc file image-dired-file-comment-list))) - (insert (format "
\n%s
\n" comment)) - (insert "
\n")) - ;; Insert links to other tags, if any - (when (> (length - (setq file-tags (assoc file image-dired-file-tag-list))) 2) - (insert "[ ") - (dolist (extra-tag file-tags) - ;; Only insert if not file name or the main tag - (if (and (not (equal extra-tag tag)) - (not (equal extra-tag file))) - (insert - (format "%s " (cdr (assoc extra-tag tag-link-list)))))) - (insert "]
\n")))) - (insert "

Index

\n") - (insert " \n") - (insert "\n")) - (setq count (1+ count)))) - (insert " \n") - (insert "")))) - ;;; Tag support @@ -2764,317 +370,10 @@ tags to their respective image file. Internal function used by (dolist (tag tag-list) (push (cons file tag) lst)))))) - -;;; bookmark.el support - -(declare-function bookmark-make-record-default - "bookmark" (&optional no-file no-context posn)) -(declare-function bookmark-prop-get "bookmark" (bookmark prop)) - -(defun image-dired-bookmark-name () - "Create a default bookmark name for the current EWW buffer." - (file-name-nondirectory - (directory-file-name - (file-name-directory (image-dired-original-file-name))))) - -(defun image-dired-bookmark-make-record () - "Create a bookmark for the current EWW buffer." - `(,(image-dired-bookmark-name) - ,@(bookmark-make-record-default t) - (location . ,(file-name-directory (image-dired-original-file-name))) - (image-dired-file . ,(file-name-nondirectory (image-dired-original-file-name))) - (handler . image-dired-bookmark-jump))) - -;;;###autoload -(defun image-dired-bookmark-jump (bookmark) - "Default bookmark handler for Image-Dired buffers." - ;; User already cached thumbnails, so disable any checking. - (let ((image-dired-show-all-from-dir-max-files nil)) - (image-dired (bookmark-prop-get bookmark 'location)) - ;; TODO: Go to the bookmarked file, if it exists. - ;; (bookmark-prop-get bookmark 'image-dired-file) - (goto-char (point-min)))) - -(put 'image-dired-bookmark-jump 'bookmark-handler-type "Image-Dired") - -;;; Obsolete - -;;;###autoload -(define-obsolete-function-alias 'tumme #'image-dired "24.4") - -;;;###autoload -(define-obsolete-function-alias 'image-dired-setup-dired-keybindings - #'image-dired-minor-mode "26.1") - -(defcustom image-dired-temp-image-file - (expand-file-name ".image-dired_temp" image-dired-dir) - "Name of temporary image file used by various commands." - :type 'file) -(make-obsolete-variable 'image-dired-temp-image-file - "no longer used." "29.1") - -(defcustom image-dired-cmd-create-temp-image-program - (if (executable-find "gm") "gm" "convert") - "Executable used to create temporary image. -Used together with `image-dired-cmd-create-temp-image-options'." - :type 'file - :version "29.1") -(make-obsolete-variable 'image-dired-cmd-create-temp-image-program - "no longer used." "29.1") - -(defcustom image-dired-cmd-create-temp-image-options - (let ((opts '("-size" "%wx%h" "%f[0]" - "-resize" "%wx%h>" - "-strip" "jpeg:%t"))) - (if (executable-find "gm") (cons "convert" opts) opts)) - "Options of command used to create temporary image for display window. -Used together with `image-dired-cmd-create-temp-image-program', -Available format specifiers are: %w and %h which are replaced by -the calculated max size for width and height in the image display window, -%f which is replaced by the file name of the original image and %t which -is replaced by the file name of the temporary file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-create-temp-image-options - "no longer used." "29.1") - -(defcustom image-dired-display-window-width-correction 1 - "Number to be used to correct image display window width. -Change if the default (1) does not work (i.e. if the image does not -completely fit)." - :type 'integer) -(make-obsolete-variable 'image-dired-display-window-width-correction - "no longer used." "29.1") - -(defcustom image-dired-display-window-height-correction 0 - "Number to be used to correct image display window height. -Change if the default (0) does not work (i.e. if the image does not -completely fit)." - :type 'integer) -(make-obsolete-variable 'image-dired-display-window-height-correction - "no longer used." "29.1") - -(defun image-dired-display-window-width (window) - "Return width, in pixels, of WINDOW." - (declare (obsolete nil "29.1")) - (- (image-dired-window-width-pixels window) - image-dired-display-window-width-correction)) - -(defun image-dired-display-window-height (window) - "Return height, in pixels, of WINDOW." - (declare (obsolete nil "29.1")) - (- (image-dired-window-height-pixels window) - image-dired-display-window-height-correction)) - -(defun image-dired-window-height-pixels (window) - "Calculate WINDOW height in pixels." - (declare (obsolete nil "29.1")) - ;; Note: The mode-line consumes one line - (* (- (window-height window) 1) (frame-char-height))) - -(defcustom image-dired-cmd-read-exif-data-program "exiftool" - "Program used to read EXIF data to image. -Used together with `image-dired-cmd-read-exif-data-options'." - :type 'file) -(make-obsolete-variable 'image-dired-cmd-read-exif-data-program - "use `exif-parse-file' and `exif-field' instead." "29.1") - -(defcustom image-dired-cmd-read-exif-data-options '("-s" "-s" "-s" "-%t" "%f") - "Arguments of command used to read EXIF data. -Used with `image-dired-cmd-read-exif-data-program'. -Available format specifiers are: %f which is replaced -by the image file name and %t which is replaced by the tag name." - :version "26.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-read-exif-data-options - "use `exif-parse-file' and `exif-field' instead." "29.1") - -(defun image-dired-get-exif-data (file tag-name) - "From FILE, return EXIF tag TAG-NAME." - (declare (obsolete "use `exif-parse-file' and `exif-field' instead." "29.1")) - (image-dired--check-executable-exists - 'image-dired-cmd-read-exif-data-program) - (let ((buf (get-buffer-create "*image-dired-get-exif-data*")) - (spec (list (cons ?f file) (cons ?t tag-name))) - tag-value) - (with-current-buffer buf - (delete-region (point-min) (point-max)) - (if (not (eq (apply #'call-process image-dired-cmd-read-exif-data-program - nil t nil - (mapcar - (lambda (arg) (format-spec arg spec)) - image-dired-cmd-read-exif-data-options)) - 0)) - (error "Could not get EXIF tag") - (goto-char (point-min)) - ;; Clean buffer from newlines and carriage returns before - ;; getting final info - (while (search-forward-regexp "[\n\r]" nil t) - (replace-match "" nil t)) - (setq tag-value (buffer-substring (point-min) (point-max))))) - tag-value)) - -(defcustom image-dired-cmd-rotate-thumbnail-program - (if (executable-find "gm") "gm" "mogrify") - "Executable used to rotate thumbnail. -Used together with `image-dired-cmd-rotate-thumbnail-options'." - :type 'file - :version "29.1") -(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-program nil "29.1") - -(defcustom image-dired-cmd-rotate-thumbnail-options - (let ((opts '("-rotate" "%d" "%t"))) - (if (executable-find "gm") (cons "mogrify" opts) opts)) - "Arguments of command used to rotate thumbnail image. -Used with `image-dired-cmd-rotate-thumbnail-program'. -Available format specifiers are: %d which is replaced by the -number of (positive) degrees to rotate the image, normally 90 or 270 -\(for 90 degrees right and left), %t which is replaced by the file name -of the thumbnail file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-options nil "29.1") - -(defun image-dired-rotate-thumbnail (degrees) - "Rotate thumbnail DEGREES degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (image-dired--check-executable-exists - 'image-dired-cmd-rotate-thumbnail-program) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (let* ((file (image-dired-thumb-name (image-dired-original-file-name))) - (thumb (expand-file-name file)) - (spec (list (cons ?d degrees) (cons ?t thumb)))) - (apply #'call-process image-dired-cmd-rotate-thumbnail-program nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-rotate-thumbnail-options)) - (clear-image-cache thumb)))) - -(defun image-dired-rotate-thumbnail-left () - "Rotate thumbnail left (counter clockwise) 90 degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (interactive) - (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) - (image-dired-rotate-thumbnail "270"))) - -(defun image-dired-rotate-thumbnail-right () - "Rotate thumbnail counter right (clockwise) 90 degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (interactive) - (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) - (image-dired-rotate-thumbnail "90"))) - -(defun image-dired-modify-mark-on-thumb-original-file (command) - "Modify mark in Dired buffer. -COMMAND is one of `mark' for marking file in Dired, `unmark' for -unmarking file in Dired or `flag' for flagging file for delete in -Dired." - (declare (obsolete image-dired--on-file-in-dired-buffer "29.1")) - (let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (if (not (and dired-buf file-name)) - (message "No image, or image with correct properties, at point") - (with-current-buffer dired-buf - (message "%s" file-name) - (when (dired-goto-file file-name) - (cond ((eq command 'mark) (dired-mark 1)) - ((eq command 'unmark) (dired-unmark 1)) - ((eq command 'toggle) - (if (image-dired-dired-file-marked-p) - (dired-unmark 1) - (dired-mark 1))) - ((eq command 'flag) (dired-flag-file-deletion 1))) - (image-dired-thumb-update-marks)))))) - -(defun image-dired-display-current-image-full () - "Display current image in full size." - (declare (obsolete image-transform-original "29.1")) - (interactive nil image-dired-thumbnail-mode) - (let ((file (image-dired-original-file-name))) - (if file - (progn - (image-dired-display-image file) - (with-current-buffer image-dired-display-image-buffer - (image-transform-original))) - (error "No original file name at point")))) - -(defun image-dired-display-current-image-sized () - "Display current image in sized to fit window dimensions." - (declare (obsolete image-mode-fit-frame "29.1")) - (interactive nil image-dired-thumbnail-mode) - (let ((file (image-dired-original-file-name))) - (if file - (progn - (image-dired-display-image file)) - (error "No original file name at point")))) - -(defun image-dired-add-to-tag-file-list (tag file) - "Add relation between TAG and FILE." - (declare (obsolete nil "29.1")) - (let (curr) - (if image-dired-tag-file-list - (if (setq curr (assoc tag image-dired-tag-file-list)) - (if (not (member file curr)) - (setcdr curr (cons file (cdr curr)))) - (setcdr image-dired-tag-file-list - (cons (list tag file) (cdr image-dired-tag-file-list)))) - (setq image-dired-tag-file-list (list (list tag file)))))) - -(defun image-dired-display-thumb-properties () - "Display thumbnail properties in the echo area." - (declare (obsolete image-dired-update-header-line "29.1")) - (image-dired-update-header-line)) - -(defvar image-dired-slideshow-count 0 - "Keeping track on number of images in slideshow.") -(make-obsolete-variable 'image-dired-slideshow-count "no longer used." "29.1") - -(defvar image-dired-slideshow-times 0 - "Number of pictures to display in slideshow.") -(make-obsolete-variable 'image-dired-slideshow-times "no longer used." "29.1") - -(define-obsolete-function-alias 'image-dired-create-display-image-buffer - #'ignore "29.1") -(define-obsolete-function-alias 'image-dired-create-gallery-lists - #'image-dired--create-gallery-lists "29.1") -(define-obsolete-function-alias 'image-dired-add-to-file-comment-list - #'image-dired--add-to-file-comment-list "29.1") -(define-obsolete-function-alias 'image-dired-add-to-tag-file-lists - #'image-dired--add-to-tag-file-lists "29.1") -(define-obsolete-function-alias 'image-dired-hidden-p - #'image-dired--hidden-p "29.1") - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;;;;;;; TEST-SECTION ;;;;;;;;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; (defvar image-dired-dir-max-size 12300000) - -;; (defun image-dired-test-clean-old-files () -;; "Clean `image-dired-dir' from old thumbnail files. -;; \"Oldness\" measured using last access time. If the total size of all -;; thumbnail files in `image-dired-dir' is larger than 'image-dired-dir-max-size', -;; old files are deleted until the max size is reached." -;; (let* ((files -;; (sort -;; (mapcar -;; (lambda (f) -;; (let ((fattribs (file-attributes f))) -;; `(,(file-attribute-access-time fattribs) -;; ,(file-attribute-size fattribs) ,f))) -;; (directory-files (image-dired-dir) t ".+\\.thumb\\..+$")) -;; ;; Sort function. Compare time between two files. -;; (lambda (l1 l2) -;; (time-less-p (car l1) (car l2))))) -;; (dirsize (apply '+ (mapcar (lambda (x) (cadr x)) files)))) -;; (while (> dirsize image-dired-dir-max-size) -;; (y-or-n-p -;; (format "Size of thumbnail directory: %d, delete old file %s? " -;; dirsize (cadr (cdar files)))) -;; (delete-file (cadr (cdar files))) -;; (setq dirsize (- dirsize (car (cdar files)))) -;; (setq files (cdr files))))) +(provide 'image-dired-tags) -(provide 'image-dired) +;; Local Variables: +;; nameless-current-name: "image-dired" +;; End: -;;; image-dired.el ends here +;;; image-dired-tags.el ends here diff --git a/lisp/image/image-dired-util.el b/lisp/image/image-dired-util.el index 9f12354111..6380dbb40a 100644 --- a/lisp/image/image-dired-util.el +++ b/lisp/image/image-dired-util.el @@ -1,9 +1,7 @@ -;;; image-dired.el --- use dired to browse and manipulate your images -*- lexical-binding: t -*- +;;; image-dired-util.el --- util functions for Image-Dired -*- lexical-binding: t -*- ;; Copyright (C) 2005-2022 Free Software Foundation, Inc. -;; Version: 0.4.11 -;; Keywords: multimedia ;; Author: Mathias Dahl ;; This file is part of GNU Emacs. @@ -23,487 +21,18 @@ ;;; Commentary: -;; BACKGROUND -;; ========== -;; -;; I needed a program to browse, organize and tag my pictures. I got -;; tired of the old gallery program I used as it did not allow -;; multi-file operations easily. Also, it put things out of my -;; control. Image viewing programs I tested did not allow multi-file -;; operations or did not do what I wanted it to. -;; -;; So, I got the idea to use the wonderful functionality of Emacs and -;; `dired' to do it. It would allow me to do almost anything I wanted, -;; which is basically just to browse all my pictures in an easy way, -;; letting me manipulate and tag them in various ways. `dired' already -;; provide all the file handling and navigation facilities; I only -;; needed to add some functions to display the images. -;; -;; I briefly tried out thumbs.el, and although it seemed more -;; powerful than this package, it did not work the way I wanted to. It -;; was too slow to create thumbnails of all files in a directory (I -;; currently keep all my 2000+ images in the same directory) and -;; browsing the thumbnail buffer was slow too. image-dired.el will not -;; create thumbnails until they are needed and the browsing is done -;; quickly and easily in Dired. I copied a great deal of ideas and -;; code from there though... :) -;; -;; `image-dired' stores the thumbnail files in `image-dired-dir' -;; using the file name format ORIGNAME.thumb.ORIGEXT. For example -;; ~/.emacs.d/image-dired/myimage01.thumb.jpg. The "database" is for -;; now just a plain text file with the following format: -;; -;; file-name-non-directory;comment:comment-text;tag1;tag2;tag3;...;tagN -;; -;; -;; PREREQUISITES -;; ============= -;; -;; * The GraphicsMagick or ImageMagick package; Image-Dired uses -;; whichever is available. -;; -;; A) For GraphicsMagick, `gm' is used. -;; Find it here: http://www.graphicsmagick.org/ -;; -;; B) For ImageMagick, `convert' and `mogrify' are used. -;; Find it here: https://www.imagemagick.org. -;; -;; * For non-lossy rotation of JPEG images, the JpegTRAN program is -;; needed. -;; -;; * For `image-dired-set-exif-data' to work, the command line tool `exiftool' is -;; needed. It can be found here: https://exiftool.org/. This -;; function is, among other things, used for writing comments to -;; image files using `image-dired-thumbnail-set-image-description'. -;; -;; -;; USAGE -;; ===== -;; -;; This information has been moved to the manual. Type `C-h r' to open -;; the Emacs manual and go to the node Thumbnails by typing `g -;; Image-Dired RET'. -;; -;; Quickstart: M-x image-dired RET DIRNAME RET -;; -;; where DIRNAME is a directory containing image files. -;; -;; LIMITATIONS -;; =========== -;; -;; * Supports all image formats that Emacs and convert supports, but -;; the thumbnails are hard-coded to JPEG or PNG format. It uses -;; JPEG by default, but can optionally follow the Thumbnail Managing -;; Standard (v0.9.0, Dec 2020), which mandates PNG. See the user -;; option `image-dired-thumbnail-storage'. -;; -;; * WARNING: The "database" format used might be changed so keep a -;; backup of `image-dired-db-file' when testing new versions. -;; -;; TODO -;; ==== -;; -;; * Investigate if it is possible to also write the tags to the image -;; files. -;; -;; * From thumbs.el: Add an option for clean-up/max-size functionality -;; for thumbnail directory. -;; -;; * From thumbs.el: Add setroot function. -;; -;; * Add `image-dired-display-thumbs-ring' and functions to cycle that. Find out -;; which is best, saving old batch just before inserting new, or -;; saving the current batch in the ring when inserting it. Adding -;; it probably needs rewriting `image-dired-display-thumbs' to be more general. -;; -;; * Find some way of toggling on and off really nice keybindings in -;; Dired (for example, using C-n or instead of C-S-n). -;; Richard suggested that we could keep C-t as prefix for -;; image-dired commands as it is currently not used in Dired. He -;; also suggested that `dired-next-line' and `dired-previous-line' -;; figure out if image-dired is enabled in the current buffer and, -;; if it is, call `image-dired-dired-next-line' and `image-dired-dired-previous-line', -;; respectively. Update: This is partly done; some bindings have -;; now been added to Dired. -;; -;; * In some way keep track of buffers and windows and stuff so that -;; it works as the user expects. -;; -;; * More/better documentation. - ;;; Code: -(require 'dired) -(require 'exif) -(require 'image-mode) -(require 'widget) (require 'xdg) -(eval-when-compile - (require 'cl-lib) - (require 'wid-edit)) - - -;;; Customizable variables - -(defgroup image-dired nil - "Use Dired to browse your images as thumbnails, and more." - :prefix "image-dired-" - :link '(info-link "(emacs) Image-Dired") - :group 'multimedia) - -(defcustom image-dired-dir (locate-user-emacs-file "image-dired/") - "Directory where thumbnail images are stored. - -The value of this option will be ignored if Image-Dired is -customized to use the Thumbnail Managing Standard; they will be -saved in \"$XDG_CACHE_HOME/thumbnails/\" instead. See -`image-dired-thumbnail-storage'." - :type 'directory) - -(defcustom image-dired-thumbnail-storage 'use-image-dired-dir - "How `image-dired' stores thumbnail files. -There are two ways that Image-Dired can store and generate -thumbnails. If you set this variable to one of the two following -values, they will be stored in the JPEG format: - -- `use-image-dired-dir' means that the thumbnails are stored in a - central directory. - -- `per-directory' means that each thumbnail is stored in a - subdirectory called \".image-dired\" in the same directory - where the image file is. - -It can also use the \"Thumbnail Managing Standard\", which allows -sharing of thumbnails across different programs. Thumbnails will -be stored in \"$XDG_CACHE_HOME/thumbnails/\" instead of in -`image-dired-dir'. Thumbnails are saved in the PNG format, and -can be one of the following sizes: - -- `standard' means use thumbnails sized 128x128. -- `standard-large' means use thumbnails sized 256x256. -- `standard-x-large' means use thumbnails sized 512x512. -- `standard-xx-large' means use thumbnails sized 1024x1024. - -For more information on the Thumbnail Managing Standard, see: -https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html" - :type '(choice :tag "How to store thumbnail files" - (const :tag "Use image-dired-dir" use-image-dired-dir) - (const :tag "Thumbnail Managing Standard (normal 128x128)" - standard) - (const :tag "Thumbnail Managing Standard (large 256x256)" - standard-large) - (const :tag "Thumbnail Managing Standard (larger 512x512)" - standard-x-large) - (const :tag "Thumbnail Managing Standard (extra large 1024x1024)" - standard-xx-large) - (const :tag "Per-directory" per-directory)) - :version "29.1") +(defvar image-dired-dir) +(defvar image-dired-thumbnail-storage) (defconst image-dired--thumbnail-standard-sizes '( standard standard-large standard-x-large standard-xx-large) "List of symbols representing thumbnail sizes in Thumbnail Managing Standard.") -(defcustom image-dired-db-file - (expand-file-name ".image-dired_db" image-dired-dir) - "Database file where file names and their associated tags are stored." - :type 'file) - -(defcustom image-dired-cmd-create-thumbnail-program - (if (executable-find "gm") "gm" "convert") - "Executable used to create thumbnail. -Used together with `image-dired-cmd-create-thumbnail-options'." - :type 'file - :version "29.1") - -(defcustom image-dired-cmd-create-thumbnail-options - (let ((opts '("-size" "%wx%h" "%f[0]" - "-resize" "%wx%h>" - "-strip" "jpeg:%t"))) - (if (executable-find "gm") (cons "convert" opts) opts)) - "Options of command used to create thumbnail image. -Used with `image-dired-cmd-create-thumbnail-program'. -Available format specifiers are: %w which is replaced by -`image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', -%f which is replaced by the file name of the original image and %t -which is replaced by the file name of the thumbnail file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-pngnq-program - ;; Prefer pngquant to pngnq-s9 as it is faster on my machine. - ;; The project also seems more active than the alternatives. - ;; Prefer pngnq-s9 to pngnq as it fixes bugs in pngnq. - ;; The pngnq project seems dead (?) since 2011 or so. - (or (executable-find "pngquant") - (executable-find "pngnq-s9") - (executable-find "pngnq")) - "The file name of the `pngquant' or `pngnq' program. -It quantizes colors of PNG images down to 256 colors or fewer -using the NeuQuant algorithm." - :version "29.1" - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-pngnq-options - (if (executable-find "pngquant") - '("--ext" "-nq8.png" "%t") ; same extension as "pngnq" - '("-f" "%t")) - "Arguments to pass `image-dired-cmd-pngnq-program'. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options'." - :type '(repeat (string :tag "Argument")) - :version "29.1") - -(defcustom image-dired-cmd-pngcrush-program (executable-find "pngcrush") - "The file name of the `pngcrush' program. -It optimizes the compression of PNG images. Also it adds PNG textual chunks -with the information required by the Thumbnail Managing Standard." - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-pngcrush-options - `("-q" - "-text" "b" "Description" "Thumbnail of file://%f" - "-text" "b" "Software" ,(emacs-version) - ;; "-text b \"Thumb::Image::Height\" \"%oh\" " - ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" " - ;; "-text b \"Thumb::Image::Width\" \"%ow\" " - "-text" "b" "Thumb::MTime" "%m" - ;; "-text b \"Thumb::Size\" \"%b\" " - "-text" "b" "Thumb::URI" "file://%f" - "%q" "%t") - "Arguments for `image-dired-cmd-pngcrush-program'. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options', with %q for a -temporary file name (typically generated by pnqnq)." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-optipng-program (executable-find "optipng") - "The file name of the `optipng' program." - :version "26.1" - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-optipng-options '("-o5" "%t") - "Arguments passed to `image-dired-cmd-optipng-program'. -Available format specifiers are described in -`image-dired-cmd-create-thumbnail-options'." - :version "26.1" - :type '(repeat (string :tag "Argument")) - :link '(url-link "man:optipng(1)")) - -(defcustom image-dired-cmd-create-standard-thumbnail-options - (append '("-size" "%wx%h" "%f[0]") - (unless (or image-dired-cmd-pngcrush-program - image-dired-cmd-pngnq-program) - (list - "-set" "Thumb::MTime" "%m" - "-set" "Thumb::URI" "file://%f" - "-set" "Description" "Thumbnail of file://%f" - "-set" "Software" (emacs-version))) - '("-thumbnail" "%wx%h>" "png:%t")) - "Options for creating thumbnails according to the Thumbnail Managing Standard. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options', with %m for file modification time." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-rotate-original-program - "jpegtran" - "Executable used to rotate original image. -Used together with `image-dired-cmd-rotate-original-options'." - :type 'file) - -(defcustom image-dired-cmd-rotate-original-options - '("-rotate" "%d" "-copy" "all" "-outfile" "%t" "%o") - "Arguments of command used to rotate original image. -Used with `image-dired-cmd-rotate-original-program'. -Available format specifiers are: %d which is replaced by the -number of (positive) degrees to rotate the image, normally 90 or -270 \(for 90 degrees right and left), %o which is replaced by the -original image file name and %t which is replaced by -`image-dired-temp-image-file'." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-temp-rotate-image-file - (expand-file-name ".image-dired_rotate_temp" image-dired-dir) - "Temporary file for rotate operations." - :type 'file) - -(defcustom image-dired-rotate-original-ask-before-overwrite t - "Confirm overwrite of original file after rotate operation. -If non-nil, ask user for confirmation before overwriting the -original file with `image-dired-temp-rotate-image-file'." - :type 'boolean) - -(defcustom image-dired-cmd-write-exif-data-program - "exiftool" - "Program used to write EXIF data to image. -Used together with `image-dired-cmd-write-exif-data-options'." - :type 'file) - -(defcustom image-dired-cmd-write-exif-data-options - '("-%t=%v" "%f") - "Arguments of command used to write EXIF data. -Used with `image-dired-cmd-write-exif-data-program'. -Available format specifiers are: %f which is replaced by -the image file name, %t which is replaced by the tag name and %v -which is replaced by the tag value." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-thumb-size - (cond - ((eq 'standard image-dired-thumbnail-storage) 128) - ((eq 'standard-large image-dired-thumbnail-storage) 256) - ((eq 'standard-x-large image-dired-thumbnail-storage) 512) - ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) - (t 100)) - "Size of thumbnails, in pixels. -This is the default size for both `image-dired-thumb-width' -and `image-dired-thumb-height'. - -The value of this option will be ignored if Image-Dired is -customized to use the Thumbnail Managing Standard; the standard -sizes will be used instead. See `image-dired-thumbnail-storage'." - :type 'integer) - -(defcustom image-dired-thumb-width image-dired-thumb-size - "Width of thumbnails, in pixels." - :type 'integer) - -(defcustom image-dired-thumb-height image-dired-thumb-size - "Height of thumbnails, in pixels." - :type 'integer) - -(defcustom image-dired-thumb-relief 2 - "Size of button-like border around thumbnails." - :type 'integer) - -(defcustom image-dired-thumb-margin 2 - "Size of the margin around thumbnails. -This is where you see the cursor." - :type 'integer) - -(defcustom image-dired-thumb-visible-marks t - "Make marks and flags visible in thumbnail buffer. -If non-nil, apply the `image-dired-thumb-mark' face to marked -images and `image-dired-thumb-flagged' to images flagged for -deletion." - :type 'boolean - :version "28.1") - -(defface image-dired-thumb-mark - '((((class color) (min-colors 16)) :background "DarkOrange") - (((class color)) :foreground "yellow")) - "Face for marked images in thumbnail buffer." - :version "29.1") - -(defface image-dired-thumb-flagged - '((((class color) (min-colors 88) (background light)) :background "Red3") - (((class color) (min-colors 88) (background dark)) :background "Pink") - (((class color) (min-colors 16) (background light)) :background "Red3") - (((class color) (min-colors 16) (background dark)) :background "Pink") - (((class color) (min-colors 8)) :background "red") - (t :inverse-video t)) - "Face for images flagged for deletion in thumbnail buffer." - :version "29.1") - -(defcustom image-dired-line-up-method 'dynamic - "Default method for line-up of thumbnails in thumbnail buffer. -Used by `image-dired-display-thumbs' and other functions that needs -to line-up thumbnails. Dynamic means to use the available width of -the window containing the thumbnail buffer, Fixed means to use -`image-dired-thumbs-per-row', Interactive is for asking the user, -and No line-up means that no automatic line-up will be done." - :type '(choice :tag "Default line-up method" - (const :tag "Dynamic" dynamic) - (const :tag "Fixed" fixed) - (const :tag "Interactive" interactive) - (const :tag "No line-up" none))) - -(defcustom image-dired-thumbs-per-row 3 - "Number of thumbnails to display per row in thumb buffer." - :type 'integer) - -(defcustom image-dired-track-movement t - "The current state of the tracking and mirroring. -For more information, see the documentation for -`image-dired-toggle-movement-tracking'." - :type 'boolean) - -(defcustom image-dired-append-when-browsing nil - "Append thumbnails in thumbnail buffer when browsing. -If non-nil, using `image-dired-next-line-and-display' and -`image-dired-previous-line-and-display' will leave a trail of thumbnail -images in the thumbnail buffer. If you enable this and want to clean -the thumbnail buffer because it is filled with too many thumbnails, -just call `image-dired-display-thumb' to display only the image at point. -This value can be toggled using `image-dired-toggle-append-browsing'." - :type 'boolean) - -(defcustom image-dired-dired-disp-props t - "If non-nil, display properties for Dired file when browsing. -Used by `image-dired-next-line-and-display', -`image-dired-previous-line-and-display' and `image-dired-mark-and-display-next'. -If the database file is large, this can slow down image browsing in -Dired and you might want to turn it off." - :type 'boolean) - -(defcustom image-dired-display-properties-format "%b: %f (%t): %c" - "Display format for thumbnail properties. -%b is replaced with associated Dired buffer name, %f with file -name (without path) of original image file, %t with the list of -tags and %c with the comment." - :type 'string) - -(defcustom image-dired-external-viewer - ;; TODO: Use mailcap, dired-guess-shell-alist-default, - ;; dired-view-command-alist. - (cond ((executable-find "display")) - ((executable-find "xli")) - ((executable-find "qiv") "qiv -t") - ((executable-find "feh") "feh")) - "Name of external viewer. -Including parameters. Used when displaying original image from -`image-dired-thumbnail-mode'." - :version "28.1" - :type '(choice string - (const :tag "Not Set" nil))) - -(defcustom image-dired-main-image-directory - (or (xdg-user-dir "PICTURES") "~/pics/") - "Name of main image directory, if any. -Used by `image-dired-copy-with-exif-file-name'." - :type 'string - :version "29.1") - -(defcustom image-dired-show-all-from-dir-max-files 500 - "Maximum number of files in directory before prompting. - -If there are more image files than this in a selected directory, -the `image-dired-show-all-from-dir' command will ask for -confirmation before creating the thumbnail buffer. If this -variable is nil, it will never ask." - :type '(choice integer - (const :tag "Disable warning" nil)) - :version "29.1") - -(defcustom image-dired-marking-shows-next t - "If non-nil, marking, unmarking or flagging an image shows the next image. - -This affects the following commands: -\\ - `image-dired-flag-thumb-original-file' (bound to \\[image-dired-flag-thumb-original-file]) - `image-dired-mark-thumb-original-file' (bound to \\[image-dired-mark-thumb-original-file]) - `image-dired-unmark-thumb-original-file' (bound to \\[image-dired-unmark-thumb-original-file])" - :type 'boolean - :version "29.1") - - -;;; Util functions - (defvar image-dired-debug nil "Non-nil means enable debug messages.") @@ -512,15 +41,6 @@ This affects the following commands: (when image-dired-debug (apply #'message args))) -(defmacro image-dired--with-db-file (&rest body) - "Run BODY in a temp buffer containing `image-dired-db-file'. -Return the last form in BODY." - (declare (indent 0) (debug t)) - `(with-temp-buffer - (if (file-exists-p image-dired-db-file) - (insert-file-contents image-dired-db-file)) - ,@body)) - (defun image-dired-dir () "Return the current thumbnail directory (from variable `image-dired-dir'). Create the thumbnail directory if it does not exist." @@ -532,56 +52,6 @@ Create the thumbnail directory if it does not exist." (message "Thumbnail directory created: %s" image-dired-dir)) image-dired-dir)) -(defun image-dired-insert-image (file type relief margin) - "Insert image FILE of image TYPE, using RELIEF and MARGIN, at point." - (let ((i `(image :type ,type - :file ,file - :relief ,relief - :margin ,margin))) - (insert-image i))) - -(defun image-dired-get-thumbnail-image (file) - "Return the image descriptor for a thumbnail of image file FILE." - (unless (string-match-p (image-file-name-regexp) file) - (error "%s is not a valid image file" file)) - (let* ((thumb-file (image-dired-thumb-name file)) - (thumb-attr (file-attributes thumb-file))) - (when (or (not thumb-attr) - (time-less-p (file-attribute-modification-time thumb-attr) - (file-attribute-modification-time - (file-attributes file)))) - (image-dired-create-thumb file thumb-file)) - (create-image thumb-file))) - -(defun image-dired-insert-thumbnail (file original-file-name - associated-dired-buffer) - "Insert thumbnail image FILE. -Add text properties ORIGINAL-FILE-NAME and ASSOCIATED-DIRED-BUFFER." - (let (beg end) - (setq beg (point)) - (image-dired-insert-image - file - ;; Thumbnails are created asynchronously, so we might not yet - ;; have a file. But if it exists, it might have been cached from - ;; before and we should use it instead of our current settings. - (or (and (file-exists-p file) - (image-type-from-file-header file)) - (and (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - 'png) - 'jpeg) - image-dired-thumb-relief - image-dired-thumb-margin) - (setq end (point)) - (add-text-properties - beg end - (list 'image-dired-thumbnail t - 'original-file-name original-file-name - 'associated-dired-buffer associated-dired-buffer - 'tags (image-dired-list-tags original-file-name) - 'mouse-face 'highlight - 'comment (image-dired-get-comment original-file-name))))) - (defun image-dired-thumb-name (file) "Return absolute file name for thumbnail FILE. Depending on the value of `image-dired-thumbnail-storage', the @@ -622,599 +92,12 @@ See also `image-dired-thumbnail-storage'." (file-name-base f) (file-name-extension f)))))) -(defun image-dired--check-executable-exists (executable) - (unless (executable-find (symbol-value executable)) - (error "Executable %S not found" executable))) - - -;;; Creating thumbnails - -(defun image-dired-thumb-size (dimension) - "Return thumb size depending on `image-dired-thumbnail-storage'. -DIMENSION should be either the symbol `width' or `height'." - (cond - ((eq 'standard image-dired-thumbnail-storage) 128) - ((eq 'standard-large image-dired-thumbnail-storage) 256) - ((eq 'standard-x-large image-dired-thumbnail-storage) 512) - ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) - (t (cl-ecase dimension - (width image-dired-thumb-width) - (height image-dired-thumb-height))))) - -(defvar image-dired--generate-thumbs-start nil - "Time when `display-thumbs' was called.") - -(defvar image-dired-queue nil - "List of items in the queue. -Each item has the form (ORIGINAL-FILE TARGET-FILE).") - -(defvar image-dired-queue-active-jobs 0 - "Number of active jobs in `image-dired-queue'.") - -(defvar image-dired-queue-active-limit (min 4 (max 2 (/ (num-processors) 2))) - "Maximum number of concurrent jobs permitted for generating images. -Increase at own risk. If you want to experiment with this, -consider setting `image-dired-debug' to a non-nil value to see -the time spent on generating thumbnails. Run `image-clear-cache' -and remove the cached thumbnail files between each trial run.") - -(defun image-dired-pngnq-thumb (spec) - "Quantize thumbnail described by format SPEC with pngnq(1)." - (let ((process - (apply #'start-process "image-dired-pngnq" nil - image-dired-cmd-pngnq-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-pngnq-options)))) - (setf (process-sentinel process) - (lambda (process status) - (if (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - ;; Pass off to pngcrush, or just rename the - ;; THUMB-nq8.png file back to THUMB.png - (if (and image-dired-cmd-pngcrush-program - (executable-find image-dired-cmd-pngcrush-program)) - (image-dired-pngcrush-thumb spec) - (let ((nq8 (cdr (assq ?q spec))) - (thumb (cdr (assq ?t spec)))) - (rename-file nq8 thumb t))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))))) - process)) - -(defun image-dired-pngcrush-thumb (spec) - "Optimize thumbnail described by format SPEC with pngcrush(1)." - ;; If pngnq wasn't run, then the THUMB-nq8.png file does not exist. - ;; pngcrush needs an infile and outfile, so we just copy THUMB to - ;; THUMB-nq8.png and use the latter as a temp file. - (when (not image-dired-cmd-pngnq-program) - (let ((temp (cdr (assq ?q spec))) - (thumb (cdr (assq ?t spec)))) - (copy-file thumb temp))) - (let ((process - (apply #'start-process "image-dired-pngcrush" nil - image-dired-cmd-pngcrush-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-pngcrush-options)))) - (setf (process-sentinel process) - (lambda (process status) - (unless (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))) - (when (memq (process-status process) '(exit signal)) - (let ((temp (cdr (assq ?q spec)))) - (delete-file temp))))) - process)) - -(defun image-dired-optipng-thumb (spec) - "Optimize thumbnail described by format SPEC with optipng(1)." - (let ((process - (apply #'start-process "image-dired-optipng" nil - image-dired-cmd-optipng-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-optipng-options)))) - (setf (process-sentinel process) - (lambda (process status) - (unless (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))))) - process)) - -(defun image-dired-create-thumb-1 (original-file thumbnail-file) - "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE." - (image-dired--check-executable-exists - 'image-dired-cmd-create-thumbnail-program) - (let* ((width (int-to-string (image-dired-thumb-size 'width))) - (height (int-to-string (image-dired-thumb-size 'height))) - (modif-time (format-time-string - "%s" (file-attribute-modification-time - (file-attributes original-file)))) - (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png" - thumbnail-file)) - (spec - (list - (cons ?w width) - (cons ?h height) - (cons ?m modif-time) - (cons ?f original-file) - (cons ?q thumbnail-nq8-file) - (cons ?t thumbnail-file))) - (thumbnail-dir (file-name-directory thumbnail-file)) - process) - (when (not (file-exists-p thumbnail-dir)) - (with-file-modes #o700 - (make-directory thumbnail-dir t)) - (message "Thumbnail directory created: %s" thumbnail-dir)) - - ;; Thumbnail file creation processes begin here and are marshaled - ;; in a queue by `image-dired-create-thumb'. - (setq process - (apply #'start-process "image-dired-create-thumbnail" nil - image-dired-cmd-create-thumbnail-program - (mapcar - (lambda (arg) (format-spec arg spec)) - (if (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - image-dired-cmd-create-standard-thumbnail-options - image-dired-cmd-create-thumbnail-options)))) - - (setf (process-sentinel process) - (lambda (process status) - ;; Trigger next in queue once a thumbnail has been created - (cl-decf image-dired-queue-active-jobs) - (image-dired-thumb-queue-run) - (when (= image-dired-queue-active-jobs 0) - (image-dired-debug-message - (format-time-string - "Generated thumbnails in %s.%3N seconds" - (time-subtract nil - image-dired--generate-thumbs-start)))) - (if (not (and (eq (process-status process) 'exit) - (zerop (process-exit-status process)))) - (message "Thumb could not be created for %s: %s" - (abbreviate-file-name original-file) - (string-replace "\n" "" status)) - (set-file-modes thumbnail-file #o600) - (clear-image-cache thumbnail-file) - ;; PNG thumbnail has been created since we are - ;; following the XDG thumbnail spec, so try to optimize - (when (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - (cond - ((and image-dired-cmd-pngnq-program - (executable-find image-dired-cmd-pngnq-program)) - (image-dired-pngnq-thumb spec)) - ((and image-dired-cmd-pngcrush-program - (executable-find image-dired-cmd-pngcrush-program)) - (image-dired-pngcrush-thumb spec)) - ((and image-dired-cmd-optipng-program - (executable-find image-dired-cmd-optipng-program)) - (image-dired-optipng-thumb spec))))))) - process)) - -(defun image-dired-thumb-queue-run () - "Run a queued job if one exists and not too many jobs are running. -Queued items live in `image-dired-queue'." - (while (and image-dired-queue - (< image-dired-queue-active-jobs - image-dired-queue-active-limit)) - (cl-incf image-dired-queue-active-jobs) - (apply #'image-dired-create-thumb-1 (pop image-dired-queue)))) - -(defun image-dired-create-thumb (original-file thumbnail-file) - "Add a job for generating ORIGINAL-FILE thumbnail to `image-dired-queue'. -The new file will be named THUMBNAIL-FILE." - (setq image-dired-queue - (nconc image-dired-queue - (list (list original-file thumbnail-file)))) - (run-at-time 0 nil #'image-dired-thumb-queue-run)) - -(defmacro image-dired--with-marked (&rest body) - "Eval BODY with point on each marked thumbnail. -If no marked file could be found, execute BODY on the current -thumbnail." - `(with-current-buffer image-dired-thumbnail-buffer - (let (found) - (save-mark-and-excursion - (goto-char (point-min)) - (while (not (eobp)) - (when (image-dired-thumb-file-marked-p) - (setq found t) - ,@body) - (forward-char))) - (unless found - ,@body)))) - -;;;###autoload -(defun image-dired-dired-toggle-marked-thumbs (&optional arg) - "Toggle thumbnails in front of file names in the Dired buffer. -If no marked file could be found, insert or hide thumbnails on the -current line. ARG, if non-nil, specifies the files to use instead -of the marked files. If ARG is an integer, use the next ARG (or -previous -ARG, if ARG<0) files." - (interactive "P") - (dired-map-over-marks - (let ((image-pos (dired-move-to-filename)) - (image-file (dired-get-filename nil t)) - thumb-file - overlay) - (when (and image-file - (string-match-p (image-file-name-regexp) image-file)) - (setq thumb-file (image-dired-get-thumbnail-image image-file)) - ;; If image is not already added, then add it. - (let ((thumb-ov (cl-loop for ov in (overlays-in (point) (1+ (point))) - if (overlay-get ov 'thumb-file) return ov))) - (if thumb-ov - (delete-overlay thumb-ov) - (put-image thumb-file image-pos) - (setq overlay - (cl-loop for ov in (overlays-in (point) (1+ (point))) - if (overlay-get ov 'put-image) return ov)) - (overlay-put overlay 'image-file image-file) - (overlay-put overlay 'thumb-file thumb-file))))) - arg ; Show or hide image on ARG next files. - 'show-progress) ; Update dired display after each image is updated. - (add-hook 'dired-after-readin-hook - 'image-dired-dired-after-readin-hook nil t)) - -(defun image-dired-dired-after-readin-hook () - "Relocate existing thumbnail overlays in Dired buffer after reverting. -Move them to their corresponding files if they still exist. -Otherwise, delete overlays." - (mapc (lambda (overlay) - (when (overlay-get overlay 'put-image) - (let* ((image-file (overlay-get overlay 'image-file)) - (image-pos (dired-goto-file image-file))) - (if image-pos - (move-overlay overlay image-pos image-pos) - (delete-overlay overlay))))) - (overlays-in (point-min) (point-max)))) - -(defun image-dired-next-line-and-display () - "Move to next Dired line and display thumbnail image." - (interactive) - (dired-next-line 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-previous-line-and-display () - "Move to previous Dired line and display thumbnail image." - (interactive) - (dired-previous-line 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-toggle-append-browsing () - "Toggle `image-dired-append-when-browsing'." - (interactive) - (setq image-dired-append-when-browsing - (not image-dired-append-when-browsing)) - (message "Append browsing %s" - (if image-dired-append-when-browsing - "on" - "off"))) - -(defun image-dired-mark-and-display-next () - "Mark current file in Dired and display next thumbnail image." - (interactive) - (dired-mark 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-toggle-dired-display-properties () - "Toggle `image-dired-dired-disp-props'." - (interactive) - (setq image-dired-dired-disp-props - (not image-dired-dired-disp-props)) - (message "Dired display properties %s" - (if image-dired-dired-disp-props - "on" - "off"))) - (defvar image-dired-thumbnail-buffer "*image-dired*" "Image-Dired's thumbnail buffer.") -(defun image-dired-create-thumbnail-buffer () - "Create thumb buffer and set `image-dired-thumbnail-mode'." - (let ((buf (get-buffer-create image-dired-thumbnail-buffer))) - (with-current-buffer buf - (setq buffer-read-only t) - (if (not (eq major-mode 'image-dired-thumbnail-mode)) - (image-dired-thumbnail-mode))) - buf)) - (defvar image-dired-display-image-buffer "*image-dired-display-image*" "Where larger versions of the images are display.") -(defvar image-dired-saved-window-configuration nil - "Saved window configuration.") - -;;;###autoload -(defun image-dired-dired-with-window-configuration (dir &optional arg) - "Open directory DIR and create a default window configuration. - -Convenience command that: - - - Opens Dired in folder DIR - - Splits windows in most useful (?) way - - Sets `truncate-lines' to t - -After the command has finished, you would typically mark some -image files in Dired and type -\\[image-dired-display-thumbs] (`image-dired-display-thumbs'). - -If called with prefix argument ARG, skip splitting of windows. - -The current window configuration is saved and can be restored by -calling `image-dired-restore-window-configuration'." - (interactive "DDirectory: \nP") - (let ((buf (image-dired-create-thumbnail-buffer)) - (buf2 (get-buffer-create image-dired-display-image-buffer))) - (setq image-dired-saved-window-configuration - (current-window-configuration)) - (dired dir) - (delete-other-windows) - (when (not arg) - (split-window-right) - (setq truncate-lines t) - (save-excursion - (other-window 1) - (pop-to-buffer-same-window buf) - (select-window (split-window-below)) - (pop-to-buffer-same-window buf2) - (other-window -2))))) - -(defun image-dired-restore-window-configuration () - "Restore window configuration. -Restore any changes to the window configuration made by calling -`image-dired-dired-with-window-configuration'." - (interactive nil image-dired-thumbnail-mode) - (if image-dired-saved-window-configuration - (set-window-configuration image-dired-saved-window-configuration) - (message "No saved window configuration"))) - -(defun image-dired--line-up-with-method () - "Line up thumbnails according to `image-dired-line-up-method'." - (cond ((eq 'dynamic image-dired-line-up-method) - (image-dired-line-up-dynamic)) - ((eq 'fixed image-dired-line-up-method) - (image-dired-line-up)) - ((eq 'interactive image-dired-line-up-method) - (image-dired-line-up-interactive)) - ((eq 'none image-dired-line-up-method) - nil) - (t - (image-dired-line-up-dynamic)))) - -;;;###autoload -(defun image-dired-display-thumbs (&optional arg append do-not-pop) - "Display thumbnails of all marked files, in `image-dired-thumbnail-buffer'. -If a thumbnail image does not exist for a file, it is created on the -fly. With prefix argument ARG, display only thumbnail for file at -point (this is useful if you have marked some files but want to show -another one). - -Recommended usage is to split the current frame horizontally so that -you have the Dired buffer in the left window and the -`image-dired-thumbnail-buffer' buffer in the right window. - -With optional argument APPEND, append thumbnail to thumbnail buffer -instead of erasing it first. - -Optional argument DO-NOT-POP controls if `pop-to-buffer' should be -used or not. If non-nil, use `display-buffer' instead of -`pop-to-buffer'. This is used from functions like -`image-dired-next-line-and-display' and -`image-dired-previous-line-and-display' where we do not want the -thumbnail buffer to be selected." - (interactive "P") - (setq image-dired--generate-thumbs-start (current-time)) - (let ((buf (image-dired-create-thumbnail-buffer)) - thumb-name files dired-buf) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (setq dired-buf (current-buffer)) - (with-current-buffer buf - (let ((inhibit-read-only t)) - (if (not append) - (erase-buffer) - (goto-char (point-max))) - (dolist (curr-file files) - (setq thumb-name (image-dired-thumb-name curr-file)) - (when (not (file-exists-p thumb-name)) - (image-dired-create-thumb curr-file thumb-name)) - (image-dired-insert-thumbnail thumb-name curr-file dired-buf))) - (if do-not-pop - (display-buffer buf) - (pop-to-buffer buf)) - (image-dired--line-up-with-method)))) - -;;;###autoload -(defun image-dired-show-all-from-dir (dir) - "Make a thumbnail buffer for all images in DIR and display it. -Any file matching `image-file-name-regexp' is considered an image -file. - -If the number of image files in DIR exceeds -`image-dired-show-all-from-dir-max-files', ask for confirmation -before creating the thumbnail buffer. If that variable is nil, -never ask for confirmation." - (interactive "DImage-Dired: ") - (dired dir) - (dired-mark-files-regexp (image-file-name-regexp)) - (let ((files (dired-get-marked-files nil nil nil t))) - (cond ((and (null (cdr files))) - (message "No image files in directory")) - ((or (not image-dired-show-all-from-dir-max-files) - (<= (length (cdr files)) image-dired-show-all-from-dir-max-files) - (and (> (length (cdr files)) image-dired-show-all-from-dir-max-files) - (y-or-n-p - (format - "Directory contains more than %d image files. Proceed?" - image-dired-show-all-from-dir-max-files)))) - (image-dired-display-thumbs) - (pop-to-buffer image-dired-thumbnail-buffer) - (setq default-directory dir) - (image-dired-unmark-all-marks)) - (t (message "Image-Dired canceled"))))) - -;;;###autoload -(defalias 'image-dired 'image-dired-show-all-from-dir) - - -;;; Tags - -(defun image-dired-sane-db-file () - "Check if `image-dired-db-file' exists. -If not, try to create it (including any parent directories). -Signal error if there are problems creating it." - (or (file-exists-p image-dired-db-file) - (let (dir buf) - (unless (file-directory-p (setq dir (file-name-directory - image-dired-db-file))) - (with-file-modes #o700 - (make-directory dir t))) - (with-current-buffer (setq buf (create-file-buffer - image-dired-db-file)) - (with-file-modes #o600 - (write-file image-dired-db-file))) - (kill-buffer buf) - (file-exists-p image-dired-db-file)) - (error "Could not create %s" image-dired-db-file))) - -(defvar image-dired-tag-history nil "Variable holding the tag history.") - -(defun image-dired-write-tags (file-tags) - "Write file tags to database. -Write each file and tag in FILE-TAGS to the database. -FILE-TAGS is an alist in the following form: - ((FILE . TAG) ... )" - (image-dired-sane-db-file) - (let (end file tag) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (dolist (elt file-tags) - (setq file (car elt) - tag (cdr elt)) - (goto-char (point-min)) - (if (search-forward-regexp (format "^%s.*$" file) nil t) - (progn - (setq end (point)) - (beginning-of-line) - (when (not (search-forward (format ";%s" tag) end t)) - (end-of-line) - (insert (format ";%s" tag)))) - (goto-char (point-max)) - (insert (format "%s;%s\n" file tag)))) - (save-buffer)))) - -(defun image-dired-remove-tag (files tag) - "For all FILES, remove TAG from the image database." - (image-dired-sane-db-file) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (let (end) - (unless (listp files) - (if (stringp files) - (setq files (list files)) - (error "Files must be a string or a list of strings!"))) - (dolist (file files) - (goto-char (point-min)) - (when (search-forward-regexp (format "^%s;" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (search-forward-regexp - (format "\\(;%s\\)\\($\\|;\\)" tag) end t) - (delete-region (match-beginning 1) (match-end 1)) - ;; Check if file should still be in the database. If - ;; it has no tags or comments, it will be removed. - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (not (search-forward ";" end t)) - (kill-line 1)))))) - (save-buffer))) - -(defun image-dired-list-tags (file) - "Read all tags for image FILE from the image database." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end (tags "")) - (when (search-forward-regexp (format "^%s" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (if (search-forward ";" end t) - (if (search-forward "comment:" end t) - (if (search-forward ";" end t) - (setq tags (buffer-substring (point) end))) - (setq tags (buffer-substring (point) end))))) - (split-string tags ";")))) - -;;;###autoload -(defun image-dired-tag-files (arg) - "Tag marked file(s) in Dired. With prefix ARG, tag file at point." - (interactive "P") - (let ((tag (completing-read - "Tags to add (separate tags with a semicolon): " - image-dired-tag-history nil nil nil 'image-dired-tag-history)) - files) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (image-dired-write-tags - (mapcar - (lambda (x) - (cons x tag)) - files)))) - -(defun image-dired-tag-thumbnail () - "Tag current or marked thumbnails." - (interactive) - (let ((tag (completing-read - "Tags to add (separate tags with a semicolon): " - image-dired-tag-history nil nil nil 'image-dired-tag-history))) - (image-dired--with-marked - (image-dired-write-tags - (list (cons (image-dired-original-file-name) tag))) - (image-dired-update-property - 'tags (image-dired-list-tags (image-dired-original-file-name)))))) - -;;;###autoload -(defun image-dired-delete-tag (arg) - "Remove tag for selected file(s). -With prefix argument ARG, remove tag from file at point." - (interactive "P") - (let ((tag (completing-read "Tag to remove: " image-dired-tag-history - nil nil nil 'image-dired-tag-history)) - files) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (image-dired-remove-tag files tag))) - -(defun image-dired-tag-thumbnail-remove () - "Remove tag from current or marked thumbnails." - (interactive) - (let ((tag (completing-read "Tag to remove: " image-dired-tag-history - nil nil nil 'image-dired-tag-history))) - (image-dired--with-marked - (image-dired-remove-tag (image-dired-original-file-name) tag) - (image-dired-update-property - 'tags (image-dired-list-tags (image-dired-original-file-name)))))) - - -;;; Thumbnail mode (cont.) - (defun image-dired-original-file-name () "Get original file name for thumbnail or display image at point." (get-text-property (point) 'original-file-name)) @@ -1236,706 +119,6 @@ With prefix argument ARG, remove tag from file at point." (equal (window-buffer window) buf)) nil t)) -(defun image-dired-track-original-file () - "Track the original file in the associated Dired buffer. -See documentation for `image-dired-toggle-movement-tracking'. -Interactive use only useful if `image-dired-track-movement' is nil." - (interactive) - (let* ((dired-buf (image-dired-associated-dired-buffer)) - (file-name (image-dired-original-file-name)) - (window (image-dired-get-buffer-window dired-buf))) - (and (buffer-live-p dired-buf) file-name - (with-current-buffer dired-buf - (if (not (dired-goto-file file-name)) - (message "Could not track file") - (if window (set-window-point window (point)))))))) - -(defun image-dired-toggle-movement-tracking () - "Turn on and off `image-dired-track-movement'. -Tracking of the movements between thumbnail and Dired buffer so that -they are \"mirrored\" in the dired buffer. When this is on, moving -around in the thumbnail or dired buffer will find the matching -position in the other buffer." - (interactive) - (setq image-dired-track-movement (not image-dired-track-movement)) - (message "Movement tracking %s" (if image-dired-track-movement "on" "off"))) - -(defun image-dired-track-thumbnail () - "Track current Dired file's thumb in `image-dired-thumbnail-buffer'. -This is almost the same as what `image-dired-track-original-file' does, -but the other way around." - (let ((file (dired-get-filename)) - prop-val found window) - (when (get-buffer image-dired-thumbnail-buffer) - (with-current-buffer image-dired-thumbnail-buffer - (goto-char (point-min)) - (while (and (not (eobp)) - (not found)) - (if (and (setq prop-val - (get-text-property (point) 'original-file-name)) - (string= prop-val file)) - (setq found t)) - (if (not found) - (forward-char 1))) - (when found - (if (setq window (image-dired-thumbnail-window)) - (set-window-point window (point))) - (image-dired-update-header-line)))))) - -(defun image-dired-dired-next-line (&optional arg) - "Call `dired-next-line', then track thumbnail. -This can safely replace `dired-next-line'. -With prefix argument, move ARG lines." - (interactive "P") - (dired-next-line (or arg 1)) - (if image-dired-track-movement - (image-dired-track-thumbnail))) - -(defun image-dired-dired-previous-line (&optional arg) - "Call `dired-previous-line', then track thumbnail. -This can safely replace `dired-previous-line'. -With prefix argument, move ARG lines." - (interactive "P") - (dired-previous-line (or arg 1)) - (if image-dired-track-movement - (image-dired-track-thumbnail))) - -(defun image-dired--display-thumb-properties-fun () - (let ((old-buf (current-buffer)) - (old-point (point))) - (lambda () - (when (and (equal (current-buffer) old-buf) - (= (point) old-point)) - (ignore-errors - (image-dired-update-header-line)))))) - -(defun image-dired-forward-image (&optional arg wrap-around) - "Move to next image and display properties. -Optional prefix ARG says how many images to move; the default is -one image. Negative means move backwards. -On reaching end or beginning of buffer, stop and show a message. - -If optional argument WRAP-AROUND is non-nil, wrap around: if -point is on the last image, move to the last one and vice versa." - (interactive "p") - (setq arg (or arg 1)) - (let (pos) - (dotimes (_ (abs arg)) - (if (and (not (if (> arg 0) (eobp) (bobp))) - (save-excursion - (forward-char (if (> arg 0) 1 -1)) - (while (and (not (if (> arg 0) (eobp) (bobp))) - (not (image-dired-image-at-point-p))) - (forward-char (if (> arg 0) 1 -1))) - (setq pos (point)) - (image-dired-image-at-point-p))) - (progn (goto-char pos) - (image-dired-update-header-line)) - (if wrap-around - (progn (goto-char (if (> arg 0) - (point-min) - ;; There are two spaces after the last image. - (- (point-max) 2))) - (image-dired-update-header-line)) - (message "At %s image" (if (> arg 0) "last" "first")) - (run-at-time 1 nil (image-dired--display-thumb-properties-fun)))))) - (when image-dired-track-movement - (image-dired-track-original-file))) - -(defun image-dired-backward-image (&optional arg) - "Move to previous image and display properties. -Optional prefix ARG says how many images to move; the default is -one image. Negative means move forward. -On reaching end or beginning of buffer, stop and show a message." - (interactive "p") - (image-dired-forward-image (- (or arg 1)))) - -(defun image-dired-next-line () - "Move to next line and display properties." - (interactive nil image-dired-thumbnail-mode) - (let ((goal-column (current-column))) - (forward-line 1) - (move-to-column goal-column)) - ;; If we end up in an empty spot, back up to the next thumbnail. - (if (not (image-dired-image-at-point-p)) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - - -(defun image-dired-previous-line () - "Move to previous line and display properties." - (interactive nil image-dired-thumbnail-mode) - (let ((goal-column (current-column))) - (forward-line -1) - (move-to-column goal-column)) - ;; If we end up in an empty spot, back up to the next - ;; thumbnail. This should only happen if the user deleted a - ;; thumbnail and did not refresh, so it is not very common. But we - ;; can handle it in a good manner, so why not? - (if (not (image-dired-image-at-point-p)) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-beginning-of-buffer () - "Move to the first image in the buffer and display properties." - (interactive nil image-dired-thumbnail-mode) - (goto-char (point-min)) - (while (and (not (image-at-point-p)) - (not (eobp))) - (forward-char 1)) - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-end-of-buffer () - "Move to the last image in the buffer and display properties." - (interactive nil image-dired-thumbnail-mode) - (goto-char (point-max)) - (while (and (not (image-at-point-p)) - (not (bobp))) - (forward-char -1)) - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - -(defun image-dired-format-properties-string (buf file props comment) - "Format display properties. -BUF is the associated Dired buffer, FILE is the original image file -name, PROPS is a stringified list of tags and COMMENT is the image file's -comment." - (format-spec - image-dired-display-properties-format - (list - (cons ?b (or buf "")) - (cons ?f file) - (cons ?t (or props "")) - (cons ?c (or comment ""))))) - -(defun image-dired-update-header-line () - "Update image information in the header line." - (when (and (not (eobp)) - (memq major-mode '(image-dired-thumbnail-mode - image-dired-display-image-mode))) - (let ((file-name (file-name-nondirectory (image-dired-original-file-name))) - (dired-buf (buffer-name (image-dired-associated-dired-buffer))) - (props (mapconcat #'identity (get-text-property (point) 'tags) ", ")) - (comment (get-text-property (point) 'comment)) - (message-log-max nil)) - (if file-name - (setq header-line-format - (image-dired-format-properties-string - dired-buf - file-name - props - comment)))))) - -(defun image-dired-dired-file-marked-p (&optional marker) - "In Dired, return t if file on current line is marked. -If optional argument MARKER is non-nil, it is a character to look -for. The default is to look for `dired-marker-char'." - (setq marker (or marker dired-marker-char)) - (save-excursion - (beginning-of-line) - (and (looking-at dired-re-mark) - (= (aref (match-string 0) 0) marker)))) - -(defun image-dired-dired-file-flagged-p () - "In Dired, return t if file on current line is flagged for deletion." - (image-dired-dired-file-marked-p dired-del-marker)) - -(defmacro image-dired--with-thumbnail-buffer (&rest body) - (declare (indent defun) (debug t)) - `(if-let ((buf (get-buffer image-dired-thumbnail-buffer))) - (with-current-buffer buf - (if-let ((win (get-buffer-window buf))) - (with-selected-window win - ,@body) - ,@body)) - (user-error "No such buffer: %s" image-dired-thumbnail-buffer))) - -(defmacro image-dired--on-file-in-dired-buffer (&rest body) - "Run BODY with point on file at point in Dired buffer. -Should be called from commands in `image-dired-thumbnail-mode'." - (declare (indent defun) (debug t)) - `(let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (if (not (and dired-buf file-name)) - (message "No image, or image with correct properties, at point") - (with-current-buffer dired-buf - (when (dired-goto-file file-name) - ,@body - (image-dired-thumb-update-marks)))))) - -(defmacro image-dired--do-mark-command (maybe-next &rest body) - "Helper macro for the mark, unmark and flag commands. -Run BODY in Dired buffer. -If optional argument MAYBE-NEXT is non-nil, show next image -according to `image-dired-marking-shows-next'." - (declare (indent defun) (debug t)) - `(image-dired--with-thumbnail-buffer - (image-dired--on-file-in-dired-buffer - ,@body) - ,(when maybe-next - '(if image-dired-marking-shows-next - (image-dired-display-next-thumbnail-original) - (image-dired-next-line))))) - -(defun image-dired-mark-thumb-original-file () - "Mark original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-mark 1))) - -(defun image-dired-unmark-thumb-original-file () - "Unmark original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-unmark 1))) - -(defun image-dired-flag-thumb-original-file () - "Flag original image file for deletion in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command t - (dired-flag-file-deletion 1))) - -(defun image-dired-toggle-mark-thumb-original-file () - "Toggle mark on original image file in associated Dired buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command nil - (if (image-dired-dired-file-marked-p) - (dired-unmark 1) - (dired-mark 1)))) - -(defun image-dired-unmark-all-marks () - "Remove all marks from all files in associated Dired buffer. -Also update the marks in the thumbnail buffer." - (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--do-mark-command nil - (dired-unmark-all-marks)) - (image-dired--with-thumbnail-buffer - (image-dired-thumb-update-marks))) - -(defun image-dired-jump-original-dired-buffer () - "Jump to the Dired buffer associated with the current image file. -You probably want to use this together with -`image-dired-track-original-file'." - (interactive nil image-dired-thumbnail-mode) - (let ((buf (image-dired-associated-dired-buffer)) - window frame) - (setq window (image-dired-get-buffer-window buf)) - (if window - (progn - (if (not (equal (selected-frame) (setq frame (window-frame window)))) - (select-frame-set-input-focus frame)) - (select-window window)) - (message "Associated dired buffer not visible")))) - -;;;###autoload -(defun image-dired-jump-thumbnail-buffer () - "Jump to thumbnail buffer." - (interactive) - (let ((window (image-dired-thumbnail-window)) - frame) - (if window - (progn - (if (not (equal (selected-frame) (setq frame (window-frame window)))) - (select-frame-set-input-focus frame)) - (select-window window)) - (message "Thumbnail buffer not visible")))) - -(defvar image-dired-thumbnail-mode-line-up-map - (let ((map (make-sparse-keymap))) - ;; map it to "g" so that the user can press it more quickly - (define-key map "g" #'image-dired-line-up-dynamic) - ;; "f" for "fixed" number of thumbs per row - (define-key map "f" #'image-dired-line-up) - ;; "i" for "interactive" - (define-key map "i" #'image-dired-line-up-interactive) - map) - "Keymap for line-up commands in `image-dired-thumbnail-mode'.") - -(defvar image-dired-thumbnail-mode-tag-map - (let ((map (make-sparse-keymap))) - ;; map it to "t" so that the user can press it more quickly - (define-key map "t" #'image-dired-tag-thumbnail) - ;; "r" for "remove" - (define-key map "r" #'image-dired-tag-thumbnail-remove) - map) - "Keymap for tag commands in `image-dired-thumbnail-mode'.") - -(defvar image-dired-thumbnail-mode-map - (let ((map (make-sparse-keymap))) - (define-key map [right] #'image-dired-forward-image) - (define-key map [left] #'image-dired-backward-image) - (define-key map [up] #'image-dired-previous-line) - (define-key map [down] #'image-dired-next-line) - (define-key map "\C-f" #'image-dired-forward-image) - (define-key map "\C-b" #'image-dired-backward-image) - (define-key map "\C-p" #'image-dired-previous-line) - (define-key map "\C-n" #'image-dired-next-line) - - (define-key map "<" #'image-dired-beginning-of-buffer) - (define-key map ">" #'image-dired-end-of-buffer) - (define-key map (kbd "M-<") #'image-dired-beginning-of-buffer) - (define-key map (kbd "M->") #'image-dired-end-of-buffer) - - (define-key map "d" #'image-dired-flag-thumb-original-file) - (define-key map [delete] #'image-dired-flag-thumb-original-file) - (define-key map "m" #'image-dired-mark-thumb-original-file) - (define-key map "u" #'image-dired-unmark-thumb-original-file) - (define-key map "U" #'image-dired-unmark-all-marks) - (define-key map "." #'image-dired-track-original-file) - (define-key map [tab] #'image-dired-jump-original-dired-buffer) - - ;; add line-up map - (define-key map "g" image-dired-thumbnail-mode-line-up-map) - ;; add tag map - (define-key map "t" image-dired-thumbnail-mode-tag-map) - - (define-key map "\C-m" #'image-dired-display-thumbnail-original-image) - (define-key map [C-return] #'image-dired-thumbnail-display-external) - - (define-key map "L" #'image-dired-rotate-original-left) - (define-key map "R" #'image-dired-rotate-original-right) - - (define-key map "D" #'image-dired-thumbnail-set-image-description) - (define-key map "S" #'image-dired-slideshow-start) - (define-key map "\C-d" #'image-dired-delete-char) - (define-key map " " #'image-dired-display-next-thumbnail-original) - (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) - (define-key map "c" #'image-dired-comment-thumbnail) - - ;; Mouse - (define-key map [mouse-2] #'image-dired-mouse-display-image) - (define-key map [mouse-1] #'image-dired-mouse-select-thumbnail) - (define-key map [mouse-3] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-1] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-2] #'image-dired-mouse-select-thumbnail) - (define-key map [down-mouse-3] #'image-dired-mouse-select-thumbnail) - ;; Seems I must first set C-down-mouse-1 to undefined, or else it - ;; will trigger the buffer menu. If I try to instead bind - ;; C-down-mouse-1 to `image-dired-mouse-toggle-mark', I get a message - ;; about C-mouse-1 not being defined afterwards. Annoying, but I - ;; probably do not completely understand mouse events. - (define-key map [C-down-mouse-1] #'undefined) - (define-key map [C-mouse-1] #'image-dired-mouse-toggle-mark) - map) - "Keymap for `image-dired-thumbnail-mode'.") - -(easy-menu-define image-dired-thumbnail-mode-menu image-dired-thumbnail-mode-map - "Menu for `image-dired-thumbnail-mode'." - '("Image-Dired" - ["Display image" image-dired-display-thumbnail-original-image] - ["Display in external viewer" image-dired-thumbnail-display-external] - ["Jump to Dired buffer" image-dired-jump-original-dired-buffer] - "---" - ["Mark image" image-dired-mark-thumb-original-file] - ["Unmark image" image-dired-unmark-thumb-original-file] - ["Unmark all images" image-dired-unmark-all-marks] - ["Flag for deletion" image-dired-flag-thumb-original-file] - ["Delete marked images" image-dired-delete-marked] - "---" - ["Rotate original right" image-dired-rotate-original-right] - ["Rotate original left" image-dired-rotate-original-left] - "---" - ["Comment thumbnail" image-dired-comment-thumbnail] - ["Tag current or marked thumbnails" image-dired-tag-thumbnail] - ["Remove tag from current or marked thumbnails" - image-dired-tag-thumbnail-remove] - ["Start slideshow" image-dired-slideshow-start] - "---" - ("View Options" - ["Toggle movement tracking" image-dired-toggle-movement-tracking - :style toggle - :selected image-dired-track-movement] - "---" - ["Line up thumbnails" image-dired-line-up] - ["Dynamic line up" image-dired-line-up-dynamic] - ["Refresh thumb" image-dired-refresh-thumb]) - ["Quit" quit-window])) - -(defvar image-dired-display-image-mode-map - (let ((map (make-sparse-keymap))) - (define-key map "S" #'image-dired-slideshow-start) - (define-key map (kbd "SPC") #'image-dired-display-next-thumbnail-original) - (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) - (define-key map "n" #'image-dired-display-next-thumbnail-original) - (define-key map "p" #'image-dired-display-previous-thumbnail-original) - (define-key map "m" #'image-dired-mark-thumb-original-file) - (define-key map "d" #'image-dired-flag-thumb-original-file) - (define-key map "u" #'image-dired-unmark-thumb-original-file) - (define-key map "U" #'image-dired-unmark-all-marks) - ;; Disable keybindings from `image-mode-map' that doesn't make sense here. - (define-key map "o" nil) ; image-save - map) - "Keymap for `image-dired-display-image-mode'.") - -(define-derived-mode image-dired-thumbnail-mode - special-mode "image-dired-thumbnail" - "Browse and manipulate thumbnail images using Dired. -Use `image-dired-minor-mode' to get a nice setup." - :interactive nil - (buffer-disable-undo) - (add-hook 'file-name-at-point-functions 'image-dired-file-name-at-point nil t) - (setq-local window-resize-pixelwise t) - (setq-local bookmark-make-record-function #'image-dired-bookmark-make-record) - ;; Use approximately as much vertical spacing as horizontal. - (setq-local line-spacing (frame-char-width))) - - -;;; Display image mode - -(define-derived-mode image-dired-display-image-mode - image-mode "image-dired-image-display" - "Mode for displaying and manipulating original image. -Resized or in full-size." - :interactive nil - (add-hook 'file-name-at-point-functions #'image-dired-file-name-at-point nil t)) - -(defvar image-dired-minor-mode-map - (let ((map (make-sparse-keymap))) - ;; (set-keymap-parent map dired-mode-map) - ;; Hijack previous and next line movement. Let C-p and C-b be - ;; though... - (define-key map "p" #'image-dired-dired-previous-line) - (define-key map "n" #'image-dired-dired-next-line) - (define-key map [up] #'image-dired-dired-previous-line) - (define-key map [down] #'image-dired-dired-next-line) - - (define-key map (kbd "C-S-n") #'image-dired-next-line-and-display) - (define-key map (kbd "C-S-p") #'image-dired-previous-line-and-display) - (define-key map (kbd "C-S-m") #'image-dired-mark-and-display-next) - - (define-key map "\C-td" #'image-dired-display-thumbs) - (define-key map [tab] #'image-dired-jump-thumbnail-buffer) - (define-key map "\C-ti" #'image-dired-dired-display-image) - (define-key map "\C-tx" #'image-dired-dired-display-external) - (define-key map "\C-ta" #'image-dired-display-thumbs-append) - (define-key map "\C-t." #'image-dired-display-thumb) - (define-key map "\C-tc" #'image-dired-dired-comment-files) - (define-key map "\C-tf" #'image-dired-mark-tagged-files) - map) - "Keymap for `image-dired-minor-mode'.") - -(easy-menu-define image-dired-minor-mode-menu image-dired-minor-mode-map - "Menu for `image-dired-minor-mode'." - '("Image-dired" - ["Display thumb for next file" image-dired-next-line-and-display] - ["Display thumb for previous file" image-dired-previous-line-and-display] - ["Mark and display next" image-dired-mark-and-display-next] - "---" - ["Create thumbnails for marked files" image-dired-create-thumbs] - "---" - ["Display thumbnails append" image-dired-display-thumbs-append] - ["Display this thumbnail" image-dired-display-thumb] - ["Display image" image-dired-dired-display-image] - ["Display in external viewer" image-dired-dired-display-external] - "---" - ["Toggle display properties" image-dired-toggle-dired-display-properties - :style toggle - :selected image-dired-dired-disp-props] - ["Toggle append browsing" image-dired-toggle-append-browsing - :style toggle - :selected image-dired-append-when-browsing] - ["Toggle movement tracking" image-dired-toggle-movement-tracking - :style toggle - :selected image-dired-track-movement] - "---" - ["Jump to thumbnail buffer" image-dired-jump-thumbnail-buffer] - ["Mark tagged files" image-dired-mark-tagged-files] - ["Comment files" image-dired-dired-comment-files] - ["Copy with EXIF file name" image-dired-copy-with-exif-file-name])) - -;;;###autoload -(define-minor-mode image-dired-minor-mode - "Setup easy-to-use keybindings for the commands to be used in Dired mode. -Note that n, p and and will be hijacked and bound to -`image-dired-dired-next-line' and `image-dired-dired-previous-line'." - :keymap image-dired-minor-mode-map) - -(declare-function clear-image-cache "image.c" (&optional filter)) - -(defun image-dired-create-thumbs (&optional arg) - "Create thumbnail images for all marked files in Dired. -With prefix argument ARG, create thumbnails even if they already exist -\(i.e. use this to refresh your thumbnails)." - (interactive "P") - (let (thumb-name) - (dolist (curr-file (dired-get-marked-files)) - (setq thumb-name (image-dired-thumb-name curr-file)) - ;; If the user overrides the exist check, we must clear the - ;; image cache so that if the user wants to display the - ;; thumbnail, it is not fetched from cache. - (when arg - (clear-image-cache (expand-file-name thumb-name))) - (when (or (not (file-exists-p thumb-name)) - arg) - (image-dired-create-thumb curr-file thumb-name))))) - - -;;; Slideshow - -(defcustom image-dired-slideshow-delay 5.0 - "Seconds to wait before showing the next image in a slideshow. -This is used by `image-dired-slideshow-start'." - :type 'float - :version "29.1") - -(define-obsolete-variable-alias 'image-dired-slideshow-timer - 'image-dired--slideshow-timer "29.1") -(defvar image-dired--slideshow-timer nil - "Slideshow timer.") - -(defvar image-dired--slideshow-initial nil) - -(defun image-dired-slideshow-step () - "Step to next image in a slideshow." - (if-let ((buf (get-buffer image-dired-thumbnail-buffer))) - (with-current-buffer buf - (image-dired-display-next-thumbnail-original)) - (image-dired-slideshow-stop))) - -(defun image-dired-slideshow-start (&optional arg) - "Start a slideshow, waiting `image-dired-slideshow-delay' between images. - -With prefix argument ARG, wait that many seconds before going to -the next image. - -With a negative prefix argument, prompt user for the delay." - (interactive "P" image-dired-thumbnail-mode image-dired-display-image-mode) - (let ((delay (if (not arg) - image-dired-slideshow-delay - (if (> arg 0) - arg - (string-to-number - (let ((delay (number-to-string image-dired-slideshow-delay))) - (read-string - (format-prompt "Delay, in seconds. Decimals are accepted" delay)) - delay)))))) - (setq image-dired--slideshow-timer - (run-with-timer - 0 delay - 'image-dired-slideshow-step)) - (add-hook 'post-command-hook 'image-dired-slideshow-stop) - (setq image-dired--slideshow-initial t) - (message "Running slideshow; use any command to stop"))) - -(defun image-dired-slideshow-stop () - "Cancel slideshow." - ;; Make sure we don't immediately stop after - ;; `image-dired-slideshow-start'. - (unless image-dired--slideshow-initial - (remove-hook 'post-command-hook 'image-dired-slideshow-stop) - (cancel-timer image-dired--slideshow-timer)) - (setq image-dired--slideshow-initial nil)) - - -;;; Thumbnail mode (cont. 3) - -(defun image-dired-delete-char () - "Remove current thumbnail from thumbnail buffer and line up." - (interactive nil image-dired-thumbnail-mode) - (let ((inhibit-read-only t)) - (delete-char 1) - (when (= (following-char) ?\s) - (delete-char 1)))) - -;;;###autoload -(defun image-dired-display-thumbs-append () - "Append thumbnails to `image-dired-thumbnail-buffer'." - (interactive) - (image-dired-display-thumbs nil t t)) - -;;;###autoload -(defun image-dired-display-thumb () - "Shorthand for `image-dired-display-thumbs' with prefix argument." - (interactive) - (image-dired-display-thumbs t nil t)) - -(defun image-dired-line-up () - "Line up thumbnails according to `image-dired-thumbs-per-row'. -See also `image-dired-line-up-dynamic'." - (interactive) - (let ((inhibit-read-only t)) - (goto-char (point-min)) - (while (and (not (image-dired-image-at-point-p)) - (not (eobp))) - (delete-char 1)) - (while (not (eobp)) - (forward-char) - (while (and (not (image-dired-image-at-point-p)) - (not (eobp))) - (delete-char 1))) - (goto-char (point-min)) - (let ((seen 0) - (thumb-prev-pos 0) - (thumb-width-chars - (ceiling (/ (+ (* 2 image-dired-thumb-relief) - (* 2 image-dired-thumb-margin) - (image-dired-thumb-size 'width)) - (float (frame-char-width)))))) - (while (not (eobp)) - (forward-char) - (if (= image-dired-thumbs-per-row 1) - (insert "\n") - (cl-incf thumb-prev-pos thumb-width-chars) - (insert (propertize " " 'display `(space :align-to ,thumb-prev-pos))) - (cl-incf seen) - (when (and (= seen (- image-dired-thumbs-per-row 1)) - (not (eobp))) - (forward-char) - (insert "\n") - (setq seen 0) - (setq thumb-prev-pos 0))))) - (goto-char (point-min)))) - -(defun image-dired-line-up-dynamic () - "Line up thumbnails images dynamically. -Calculate how many thumbnails fit." - (interactive) - (let* ((char-width (frame-char-width)) - (width (image-dired-window-width-pixels (image-dired-thumbnail-window))) - (image-dired-thumbs-per-row - (/ width - (+ (* 2 image-dired-thumb-relief) - (* 2 image-dired-thumb-margin) - (image-dired-thumb-size 'width) - char-width)))) - (image-dired-line-up))) - -(defun image-dired-line-up-interactive () - "Line up thumbnails interactively. -Ask user how many thumbnails should be displayed per row." - (interactive) - (let ((image-dired-thumbs-per-row - (string-to-number (read-string "How many thumbs per row: ")))) - (if (not (> image-dired-thumbs-per-row 0)) - (message "Number must be greater than 0") - (image-dired-line-up)))) - -(defun image-dired-thumbnail-display-external () - "Display original image for thumbnail at point using external viewer." - (interactive) - (let ((file (image-dired-original-file-name))) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (if (not file) - (message "No original file name found") - (start-process "image-dired-thumb-external" nil - image-dired-external-viewer file))))) - -;;;###autoload -(defun image-dired-dired-display-external () - "Display file at point using an external viewer." - (interactive) - (let ((file (dired-get-filename))) - (start-process "image-dired-external" nil - image-dired-external-viewer file))) - (defun image-dired-window-width-pixels (window) "Calculate WINDOW width in pixels." (* (window-width window) (frame-char-width))) @@ -1965,1116 +148,14 @@ Ask user how many thumbnails should be displayed per row." (equal (window-buffer window) buf)))) (error "No thumbnail image at point")))) -(defun image-dired-display-image (file &optional _ignored) - "Display image FILE in image buffer. -Use this when you want to display the image, in a new window. -The window will use `image-dired-display-image-mode' which is -based on `image-mode'." - (declare (advertised-calling-convention (file) "29.1")) - (setq file (expand-file-name file)) - (when (not (file-exists-p file)) - (error "No such file: %s" file)) - (let ((buf (get-buffer image-dired-display-image-buffer)) - (cur-win (selected-window))) - (when buf - (kill-buffer buf)) - (when-let ((buf (find-file-noselect file nil t))) - (pop-to-buffer buf) - (rename-buffer image-dired-display-image-buffer) - (image-dired-display-image-mode) - (select-window cur-win)))) - -(defun image-dired-display-thumbnail-original-image (&optional arg) - "Display current thumbnail's original image in display buffer. -See documentation for `image-dired-display-image' for more information. -With prefix argument ARG, display image in its original size." - (interactive "P") - (let ((file (image-dired-original-file-name))) - (if (not (string-equal major-mode "image-dired-thumbnail-mode")) - (message "Not in image-dired-thumbnail-mode") - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (if (not file) - (message "No original file name found") - (image-dired-display-image file arg)))))) - - -;;;###autoload -(defun image-dired-dired-display-image (&optional arg) - "Display current image file. -See documentation for `image-dired-display-image' for more information. -With prefix argument ARG, display image in its original size." - (interactive "P") - (image-dired-display-image (dired-get-filename) arg)) - (defun image-dired-image-at-point-p () "Return non-nil if there is an `image-dired' thumbnail at point." (get-text-property (point) 'image-dired-thumbnail)) -(defun image-dired-refresh-thumb () - "Force creation of new image for current thumbnail." - (interactive nil image-dired-thumbnail-mode) - (let* ((file (image-dired-original-file-name)) - (thumb (expand-file-name (image-dired-thumb-name file)))) - (clear-image-cache (expand-file-name thumb)) - (image-dired-create-thumb file thumb))) - -(defun image-dired-rotate-original (degrees) - "Rotate original image DEGREES degrees." - (image-dired--check-executable-exists - 'image-dired-cmd-rotate-original-program) - (if (not (image-dired-image-at-point-p)) - (message "No image at point") - (let* ((file (image-dired-original-file-name)) - (spec - (list - (cons ?d degrees) - (cons ?o (expand-file-name file)) - (cons ?t image-dired-temp-rotate-image-file)))) - (unless (eq 'jpeg (image-type file)) - (user-error "Only JPEG images can be rotated")) - (if (not (= 0 (apply #'call-process image-dired-cmd-rotate-original-program - nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-rotate-original-options)))) - (error "Could not rotate image") - (image-dired-display-image image-dired-temp-rotate-image-file) - (if (or (and image-dired-rotate-original-ask-before-overwrite - (y-or-n-p - "Rotate to temp file OK. Overwrite original image? ")) - (not image-dired-rotate-original-ask-before-overwrite)) - (progn - (copy-file image-dired-temp-rotate-image-file file t) - (image-dired-refresh-thumb)) - (image-dired-display-image file)))))) - -(defun image-dired-rotate-original-left () - "Rotate original image left (counter clockwise) 90 degrees. -The result of the rotation is displayed in the image display area -and a confirmation is needed before the original image files is -overwritten. This confirmation can be turned off using -`image-dired-rotate-original-ask-before-overwrite'." - (interactive) - (image-dired-rotate-original "270")) - -(defun image-dired-rotate-original-right () - "Rotate original image right (clockwise) 90 degrees. -The result of the rotation is displayed in the image display area -and a confirmation is needed before the original image files is -overwritten. This confirmation can be turned off using -`image-dired-rotate-original-ask-before-overwrite'." - (interactive) - (image-dired-rotate-original "90")) - - -;;; EXIF support - -(defun image-dired-get-exif-file-name (file) - "Use the image's EXIF information to return a unique file name. -The file name should be unique as long as you do not take more than -one picture per second. The original file name is suffixed at the end -for traceability. The format of the returned file name is -YYYY_MM_DD_HH_MM_DD_ORIG_FILE_NAME.jpg. Used from -`image-dired-copy-with-exif-file-name'." - (let (data no-exif-data-found) - (if (not (eq 'jpeg (image-type (expand-file-name file)))) - (setq no-exif-data-found t - data (format-time-string - "%Y:%m:%d %H:%M:%S" - (file-attribute-modification-time - (file-attributes (expand-file-name file))))) - (setq data (exif-field 'date-time (exif-parse-file - (expand-file-name file))))) - (while (string-match "[ :]" data) - (setq data (replace-match "_" nil nil data))) - (format "%s%s%s" data - (if no-exif-data-found - "_noexif_" - "_") - (file-name-nondirectory file)))) - -(defun image-dired-thumbnail-set-image-description () - "Set the ImageDescription EXIF tag for the original image. -If the image already has a value for this tag, it is used as the -default value at the prompt." - (interactive) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (let* ((file (image-dired-original-file-name)) - (old-value (or (exif-field 'description (exif-parse-file file)) ""))) - (if (eq 0 - (image-dired-set-exif-data file "ImageDescription" - (read-string "Value of ImageDescription: " - old-value))) - (message "Successfully wrote ImageDescription tag") - (error "Could not write ImageDescription tag"))))) - -(defun image-dired-set-exif-data (file tag-name tag-value) - "In FILE, set EXIF tag TAG-NAME to value TAG-VALUE." - (image-dired--check-executable-exists - 'image-dired-cmd-write-exif-data-program) - (let ((spec - (list - (cons ?f (expand-file-name file)) - (cons ?t tag-name) - (cons ?v tag-value)))) - (apply #'call-process image-dired-cmd-write-exif-data-program nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-write-exif-data-options)))) - -(defun image-dired-copy-with-exif-file-name () - "Copy file with unique name to main image directory. -Copy current or all marked files in Dired to a new file in your -main image directory, using a file name generated by -`image-dired-get-exif-file-name'. A typical usage for this if when -copying images from a digital camera into the image directory. - - Typically, you would open up the folder with the incoming -digital images, mark the files to be copied, and execute this -function. The result is a couple of new files in -`image-dired-main-image-directory' called -2005_05_08_12_52_00_dscn0319.jpg, -2005_05_08_14_27_45_dscn0320.jpg etc." - (interactive) - (let (new-name - (files (dired-get-marked-files))) - (mapc - (lambda (curr-file) - (setq new-name - (format "%s/%s" - (file-name-as-directory - (expand-file-name image-dired-main-image-directory)) - (image-dired-get-exif-file-name curr-file))) - (message "Copying %s to %s" curr-file new-name) - (copy-file curr-file new-name)) - files))) - -;;; Thumbnail mode (cont.) - -(defun image-dired-display-next-thumbnail-original (&optional arg) - "Move to the next image in the thumbnail buffer and display it. -With prefix ARG, move that many thumbnails." - (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired--with-thumbnail-buffer - (image-dired-forward-image arg t) - (image-dired-display-thumbnail-original-image))) - -(defun image-dired-display-previous-thumbnail-original (arg) - "Move to the previous image in the thumbnail buffer and display it. -With prefix ARG, move that many thumbnails." - (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) - (image-dired-display-next-thumbnail-original (- arg))) - - -;;; Image Comments - -(defun image-dired-write-comments (file-comments) - "Write file comments to database. -Write file comments to one or more files. -FILE-COMMENTS is an alist on the following form: - ((FILE . COMMENT) ... )" - (image-dired-sane-db-file) - (let (end comment-beg-pos comment-end-pos file comment) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (dolist (elt file-comments) - (setq file (car elt) - comment (cdr elt)) - (goto-char (point-min)) - (if (search-forward-regexp (format "^%s.*$" file) nil t) - (progn - (setq end (point)) - (beginning-of-line) - ;; Delete old comment, if any - (when (search-forward ";comment:" end t) - (setq comment-beg-pos (match-beginning 0)) - ;; Any tags after the comment? - (if (search-forward ";" end t) - (setq comment-end-pos (- (point) 1)) - (setq comment-end-pos end)) - ;; Delete comment tag and comment - (delete-region comment-beg-pos comment-end-pos)) - ;; Insert new comment - (beginning-of-line) - (unless (search-forward ";" end t) - (end-of-line) - (insert ";")) - (insert (format "comment:%s;" comment))) - ;; File does not exist in database - add it. - (goto-char (point-max)) - (insert (format "%s;comment:%s\n" file comment)))) - (save-buffer)))) - -(defun image-dired-update-property (prop value) - "Update text property PROP with value VALUE at point." - (let ((inhibit-read-only t)) - (put-text-property - (point) (1+ (point)) - prop - value))) - -;;;###autoload -(defun image-dired-dired-comment-files () - "Add comment to current or marked files in Dired." - (interactive) - (let ((comment (image-dired-read-comment))) - (image-dired-write-comments - (mapcar - (lambda (curr-file) - (cons curr-file comment)) - (dired-get-marked-files))))) - -(defun image-dired-comment-thumbnail () - "Add comment to current thumbnail in thumbnail buffer." - (interactive) - (let* ((file (image-dired-original-file-name)) - (comment (image-dired-read-comment file))) - (image-dired-write-comments (list (cons file comment))) - (image-dired-update-property 'comment comment)) - (image-dired-update-header-line)) - -(defun image-dired-read-comment (&optional file) - "Read comment for an image. -Optionally use old comment from FILE as initial value." - (let ((comment - (read-string - "Comment: " - (if file (image-dired-get-comment file))))) - comment)) - -(defun image-dired-get-comment (file) - "Get comment for file FILE." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end comment-beg-pos comment-end-pos comment) - (when (search-forward-regexp (format "^%s" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (search-forward ";comment:" end t) - (setq comment-beg-pos (point)) - (if (search-forward ";" end t) - (setq comment-end-pos (- (point) 1)) - (setq comment-end-pos end)) - (setq comment (buffer-substring - comment-beg-pos comment-end-pos)))) - comment))) - -;;;###autoload -(defun image-dired-mark-tagged-files (regexp) - "Use REGEXP to mark files with matching tag. -A `tag' is a keyword, a piece of meta data, associated with an -image file and stored in image-dired's database file. This command -lets you input a regexp and this will be matched against all tags -on all image files in the database file. The files that have a -matching tag will be marked in the Dired buffer." - (interactive "sMark tagged files (regexp): ") - (image-dired-sane-db-file) - (let ((hits 0) - files) - (image-dired--with-db-file - ;; Collect matches - (while (search-forward-regexp "\\(^[^;\n]+\\);\\(.*\\)" nil t) - (let ((file (match-string 1)) - (tags (split-string (match-string 2) ";"))) - (when (seq-find (lambda (tag) - (string-match-p regexp tag)) - tags) - (push file files))))) - ;; Mark files - (dolist (curr-file files) - ;; I tried using `dired-mark-files-regexp' but it was waaaay to - ;; slow. Don't bother about hits found in other directories - ;; than the current one. - (when (string= (file-name-as-directory - (expand-file-name default-directory)) - (file-name-as-directory - (file-name-directory curr-file))) - (setq curr-file (file-name-nondirectory curr-file)) - (goto-char (point-min)) - (when (search-forward-regexp (format "\\s %s$" curr-file) nil t) - (setq hits (+ hits 1)) - (dired-mark 1)))) - (message "%d files with matching tag marked" hits))) - - - -;;; Mouse support - -(defun image-dired-mouse-display-image (event) - "Use mouse EVENT, call `image-dired-display-image' to display image. -Track this in associated Dired buffer if `image-dired-track-movement' is -non-nil." - (interactive "e") - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (unless (image-at-point-p) - (image-dired-backward-image)) - (let ((file (image-dired-original-file-name))) - (when file - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-display-image file)))) - -(defun image-dired-mouse-select-thumbnail (event) - "Use mouse EVENT to select thumbnail image. -Track this in associated Dired buffer if `image-dired-track-movement' is -non-nil." - (interactive "e") - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (unless (image-at-point-p) - (image-dired-backward-image)) - (if image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-update-header-line)) - - - -;;; Dired marks and tags - -(defun image-dired-thumb-file-marked-p (&optional flagged) - "Check if file is marked in associated Dired buffer. -If optional argument FLAGGED is non-nil, check if file is flagged -for deletion instead." - (let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (when (and dired-buf file-name) - (with-current-buffer dired-buf - (save-excursion - (when (dired-goto-file file-name) - (if flagged - (image-dired-dired-file-flagged-p) - (image-dired-dired-file-marked-p)))))))) - -(defun image-dired-thumb-file-flagged-p () - "Check if file is flagged for deletion in associated Dired buffer." - (image-dired-thumb-file-marked-p t)) - -(defun image-dired-delete-marked () - "Delete current or marked thumbnails and associated images." - (interactive) - (image-dired--with-marked - (image-dired-delete-char) - (unless (bobp) - (backward-char))) - (image-dired--line-up-with-method) - (with-current-buffer (image-dired-associated-dired-buffer) - (dired-do-delete))) - -(defun image-dired-thumb-update-marks () - "Update the marks in the thumbnail buffer." - (when image-dired-thumb-visible-marks - (with-current-buffer image-dired-thumbnail-buffer - (save-mark-and-excursion - (goto-char (point-min)) - (let ((inhibit-read-only t)) - (while (not (eobp)) - (with-silent-modifications - (cond ((image-dired-thumb-file-marked-p) - (add-face-text-property (point) (1+ (point)) - 'image-dired-thumb-mark)) - ((image-dired-thumb-file-flagged-p) - (add-face-text-property (point) (1+ (point)) - 'image-dired-thumb-flagged)) - (t (remove-text-properties (point) (1+ (point)) - '(face image-dired-thumb-mark))))) - (forward-char))))))) - -(defun image-dired-mouse-toggle-mark-1 () - "Toggle Dired mark for current thumbnail. -Track this in associated Dired buffer if -`image-dired-track-movement' is non-nil." - (when image-dired-track-movement - (image-dired-track-original-file)) - (image-dired-toggle-mark-thumb-original-file)) - -(defun image-dired-mouse-toggle-mark (event) - "Use mouse EVENT to toggle Dired mark for thumbnail. -Toggle marks of all thumbnails in region, if it's active. -Track this in associated Dired buffer if -`image-dired-track-movement' is non-nil." - (interactive "e") - (if (use-region-p) - (let ((end (region-end))) - (save-excursion - (goto-char (region-beginning)) - (while (<= (point) end) - (when (image-dired-image-at-point-p) - (image-dired-mouse-toggle-mark-1)) - (forward-char)))) - (mouse-set-point event) - (goto-char (posn-point (event-end event))) - (image-dired-mouse-toggle-mark-1)) - (image-dired-thumb-update-marks)) - -(defun image-dired-dired-display-properties () - "Display properties for Dired file in the echo area." - (interactive) - (let* ((file (dired-get-filename)) - (file-name (file-name-nondirectory file)) - (dired-buf (buffer-name (current-buffer))) - (props (mapconcat #'identity (image-dired-list-tags file) ", ")) - (comment (image-dired-get-comment file)) - (message-log-max nil)) - (if file-name - (message "%s" - (image-dired-format-properties-string - dired-buf - file-name - props - comment))))) - - - -;;; Gallery support - -;; TODO: -;; * Support gallery creation when using per-directory thumbnail -;; storage. -;; * Enhanced gallery creation with basic CSS-support and pagination -;; of tag pages with many pictures. - -(defgroup image-dired-gallery nil - "Image-Dired support for generating a HTML gallery." - :prefix "image-dired-" - :group 'image-dired - :version "29.1") - -(defcustom image-dired-gallery-dir - (expand-file-name ".image-dired_gallery" image-dired-dir) - "Directory to store generated gallery html pages. -The name of this directory needs to be \"shared\" to the public -so that it can access the index.html page that image-dired creates." - :type 'directory) - -(defcustom image-dired-gallery-image-root-url - "https://example.org/image-diredpics" - "URL where the full size images are to be found on your web server. -Note that this URL has to be configured on your web server. -Image-Dired expects to find pictures in this directory. -This is used by `image-dired-gallery-generate'." - :type 'string - :version "29.1") - -(defcustom image-dired-gallery-thumb-image-root-url - "https://example.org/image-diredthumbs" - "URL where the thumbnail images are to be found on your web server. -Note that URL path has to be configured on your web server. -Image-Dired expects to find pictures in this directory. -This is used by `image-dired-gallery-generate'." - :type 'string - :version "29.1") - -(defcustom image-dired-gallery-hidden-tags - (list "private" "hidden" "pending") - "List of \"hidden\" tags. -Used by `image-dired-gallery-generate' to leave out \"hidden\" images." - :type '(repeat string)) - -(defvar image-dired-tag-file-list nil - "List to store tag-file structure.") - -(defvar image-dired-file-tag-list nil - "List to store file-tag structure.") - -(defvar image-dired-file-comment-list nil - "List to store file comments.") - -(defun image-dired--add-to-tag-file-lists (tag file) - "Helper function used from `image-dired--create-gallery-lists'. - -Add TAG to FILE in one list and FILE to TAG in the other. - -Lisp structures look like the following: - -image-dired-file-tag-list: - - ((\"filename1\" \"tag1\" \"tag2\" \"tag3\" ...) - (\"filename2\" \"tag1\" \"tag2\" \"tag3\" ...) - ...) - -image-dired-tag-file-list: - - ((\"tag1\" \"filename1\" \"filename2\" \"filename3\" ...) - (\"tag2\" \"filename1\" \"filename2\" \"filename3\" ...) - ...)" - ;; Add tag to file list - (let (curr) - (if image-dired-file-tag-list - (if (setq curr (assoc file image-dired-file-tag-list)) - (setcdr curr (cons tag (cdr curr))) - (setcdr image-dired-file-tag-list - (cons (list file tag) (cdr image-dired-file-tag-list)))) - (setq image-dired-file-tag-list (list (list file tag)))) - ;; Add file to tag list - (if image-dired-tag-file-list - (if (setq curr (assoc tag image-dired-tag-file-list)) - (if (not (member file curr)) - (setcdr curr (cons file (cdr curr)))) - (setcdr image-dired-tag-file-list - (cons (list tag file) (cdr image-dired-tag-file-list)))) - (setq image-dired-tag-file-list (list (list tag file)))))) - -(defun image-dired--add-to-file-comment-list (file comment) - "Helper function used from `image-dired--create-gallery-lists'. - -For FILE, add COMMENT to list. - -Lisp structure looks like the following: - -image-dired-file-comment-list: - - ((\"filename1\" . \"comment1\") - (\"filename2\" . \"comment2\") - ...)" - (if image-dired-file-comment-list - (if (not (assoc file image-dired-file-comment-list)) - (setcdr image-dired-file-comment-list - (cons (cons file comment) - (cdr image-dired-file-comment-list)))) - (setq image-dired-file-comment-list (list (cons file comment))))) - -(defun image-dired--create-gallery-lists () - "Create temporary lists used by `image-dired-gallery-generate'." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end beg file row-tags) - (setq image-dired-tag-file-list nil) - (setq image-dired-file-tag-list nil) - (setq image-dired-file-comment-list nil) - (goto-char (point-min)) - (while (search-forward-regexp "^." nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (setq beg (point)) - (unless (search-forward ";" end nil) - (error "Something is really wrong, check format of database")) - (setq row-tags (split-string - (buffer-substring beg end) ";")) - (setq file (car row-tags)) - (dolist (x (cdr row-tags)) - (if (not (string-match "^comment:\\(.*\\)" x)) - (image-dired--add-to-tag-file-lists x file) - (image-dired--add-to-file-comment-list file (match-string 1 x))))))) - ;; Sort tag-file list - (setq image-dired-tag-file-list - (sort image-dired-tag-file-list - (lambda (x y) - (string< (car x) (car y)))))) - -(defun image-dired--hidden-p (file) - "Return t if image FILE has a \"hidden\" tag." - (cl-loop for tag in (cdr (assoc file image-dired-file-tag-list)) - if (member tag image-dired-gallery-hidden-tags) return t)) - -(defun image-dired-gallery-generate () - "Generate gallery pages. -First we create a couple of Lisp structures from the database to make -it easier to generate, then HTML-files are created in -`image-dired-gallery-dir'." - (interactive) - (if (eq 'per-directory image-dired-thumbnail-storage) - (error "Currently, gallery generation is not supported \ -when using per-directory thumbnail file storage")) - (image-dired--create-gallery-lists) - (let ((tags image-dired-tag-file-list) - (index-file (format "%s/index.html" image-dired-gallery-dir)) - count tag tag-file - comment file-tags tag-link tag-link-list) - ;; Make sure gallery root exist - (if (file-exists-p image-dired-gallery-dir) - (if (not (file-directory-p image-dired-gallery-dir)) - (error "Variable image-dired-gallery-dir is not a directory")) - ;; FIXME: Should we set umask to 077 here, as we do for thumbnails? - (make-directory image-dired-gallery-dir)) - ;; Open index file - (with-temp-file index-file - (if (file-exists-p index-file) - (insert-file-contents index-file)) - (insert "\n") - (insert " \n") - (insert "

Image-Dired Gallery

\n") - (insert (format "

\n Gallery generated %s\n

\n" - (current-time-string))) - (insert "

Tag index

\n") - (setq count 1) - ;; Pre-generate list of all tag links - (dolist (curr tags) - (setq tag (car curr)) - (when (not (member tag image-dired-gallery-hidden-tags)) - (setq tag-link (format "%s" count tag)) - (if tag-link-list - (setq tag-link-list - (append tag-link-list (list (cons tag tag-link)))) - (setq tag-link-list (list (cons tag tag-link)))) - (setq count (1+ count)))) - (setq count 1) - ;; Main loop where we generated thumbnail pages per tag - (dolist (curr tags) - (setq tag (car curr)) - ;; Don't display hidden tags - (when (not (member tag image-dired-gallery-hidden-tags)) - ;; Insert link to tag page in index - (insert (format " %s
\n" (cdr (assoc tag tag-link-list)))) - ;; Open per-tag file - (setq tag-file (format "%s/%s.html" image-dired-gallery-dir count)) - (with-temp-file tag-file - (if (file-exists-p tag-file) - (insert-file-contents tag-file)) - (erase-buffer) - (insert "\n") - (insert " \n") - (insert "

Index

\n") - (insert (format "

Images with tag "%s"

" tag)) - ;; Main loop for files per tag page - (dolist (file (cdr curr)) - (unless (image-dired-hidden-p file) - ;; Insert thumbnail with link to full image - (insert - (format "\n" - image-dired-gallery-image-root-url - (file-name-nondirectory file) - image-dired-gallery-thumb-image-root-url - (file-name-nondirectory (image-dired-thumb-name file)) file)) - ;; Insert comment, if any - (if (setq comment (cdr (assoc file image-dired-file-comment-list))) - (insert (format "
\n%s
\n" comment)) - (insert "
\n")) - ;; Insert links to other tags, if any - (when (> (length - (setq file-tags (assoc file image-dired-file-tag-list))) 2) - (insert "[ ") - (dolist (extra-tag file-tags) - ;; Only insert if not file name or the main tag - (if (and (not (equal extra-tag tag)) - (not (equal extra-tag file))) - (insert - (format "%s " (cdr (assoc extra-tag tag-link-list)))))) - (insert "]
\n")))) - (insert "

Index

\n") - (insert " \n") - (insert "\n")) - (setq count (1+ count)))) - (insert " \n") - (insert "")))) - - -;;; Tag support - -(defvar image-dired-widget-list nil - "List to keep track of meta data in edit buffer.") - -(declare-function widget-forward "wid-edit" (arg)) - -;;;###autoload -(defun image-dired-dired-edit-comment-and-tags () - "Edit comment and tags of current or marked image files. -Edit comment and tags for all marked image files in an -easy-to-use form." - (interactive) - (setq image-dired-widget-list nil) - ;; Setup buffer. - (let ((files (dired-get-marked-files))) - (pop-to-buffer-same-window "*Image-Dired Edit Meta Data*") - (kill-all-local-variables) - (let ((inhibit-read-only t)) - (erase-buffer)) - (remove-overlays) - ;; Some help for the user. - (widget-insert -"\nEdit comments and tags for each image. Separate multiple tags -with a comma. Move forward between fields using TAB or RET. -Move to the previous field using backtab (S-TAB). Save by -activating the Save button at the bottom of the form or cancel -the operation by activating the Cancel button.\n\n") - ;; Here comes all images and a comment and tag field for each - ;; image. - (let (thumb-file img comment-widget tag-widget) - - (dolist (file files) - - (setq thumb-file (image-dired-thumb-name file) - img (create-image thumb-file)) - - (insert-image img) - (widget-insert "\n\nComment: ") - (setq comment-widget - (widget-create 'editable-field - :size 60 - :format "%v " - :value (or (image-dired-get-comment file) ""))) - (widget-insert "\nTags: ") - (setq tag-widget - (widget-create 'editable-field - :size 60 - :format "%v " - :value (or (mapconcat - #'identity - (image-dired-list-tags file) - ",") ""))) - ;; Save information in all widgets so that we can use it when - ;; the user saves the form. - (setq image-dired-widget-list - (append image-dired-widget-list - (list (list file comment-widget tag-widget)))) - (widget-insert "\n\n"))) - - ;; Footer with Save and Cancel button. - (widget-insert "\n") - (widget-create 'push-button - :notify - (lambda (&rest _ignore) - (image-dired-save-information-from-widgets) - (bury-buffer) - (message "Done")) - "Save") - (widget-insert " ") - (widget-create 'push-button - :notify - (lambda (&rest _ignore) - (bury-buffer) - (message "Operation canceled")) - "Cancel") - (widget-insert "\n") - (use-local-map widget-keymap) - (widget-setup) - ;; Jump to the first widget. - (widget-forward 1))) - -(defun image-dired-save-information-from-widgets () - "Save information found in `image-dired-widget-list'. -Use the information in `image-dired-widget-list' to save comments and -tags to their respective image file. Internal function used by -`image-dired-dired-edit-comment-and-tags'." - (let (file comment tag-string tag-list lst) - (image-dired-write-comments - (mapcar - (lambda (widget) - (setq file (car widget) - comment (widget-value (cadr widget))) - (cons file comment)) - image-dired-widget-list)) - (image-dired-write-tags - (dolist (widget image-dired-widget-list lst) - (setq file (car widget) - tag-string (widget-value (car (cddr widget))) - tag-list (split-string tag-string ",")) - (dolist (tag tag-list) - (push (cons file tag) lst)))))) - - -;;; bookmark.el support - -(declare-function bookmark-make-record-default - "bookmark" (&optional no-file no-context posn)) -(declare-function bookmark-prop-get "bookmark" (bookmark prop)) - -(defun image-dired-bookmark-name () - "Create a default bookmark name for the current EWW buffer." - (file-name-nondirectory - (directory-file-name - (file-name-directory (image-dired-original-file-name))))) - -(defun image-dired-bookmark-make-record () - "Create a bookmark for the current EWW buffer." - `(,(image-dired-bookmark-name) - ,@(bookmark-make-record-default t) - (location . ,(file-name-directory (image-dired-original-file-name))) - (image-dired-file . ,(file-name-nondirectory (image-dired-original-file-name))) - (handler . image-dired-bookmark-jump))) - -;;;###autoload -(defun image-dired-bookmark-jump (bookmark) - "Default bookmark handler for Image-Dired buffers." - ;; User already cached thumbnails, so disable any checking. - (let ((image-dired-show-all-from-dir-max-files nil)) - (image-dired (bookmark-prop-get bookmark 'location)) - ;; TODO: Go to the bookmarked file, if it exists. - ;; (bookmark-prop-get bookmark 'image-dired-file) - (goto-char (point-min)))) - -(put 'image-dired-bookmark-jump 'bookmark-handler-type "Image-Dired") - -;;; Obsolete - -;;;###autoload -(define-obsolete-function-alias 'tumme #'image-dired "24.4") - -;;;###autoload -(define-obsolete-function-alias 'image-dired-setup-dired-keybindings - #'image-dired-minor-mode "26.1") - -(defcustom image-dired-temp-image-file - (expand-file-name ".image-dired_temp" image-dired-dir) - "Name of temporary image file used by various commands." - :type 'file) -(make-obsolete-variable 'image-dired-temp-image-file - "no longer used." "29.1") - -(defcustom image-dired-cmd-create-temp-image-program - (if (executable-find "gm") "gm" "convert") - "Executable used to create temporary image. -Used together with `image-dired-cmd-create-temp-image-options'." - :type 'file - :version "29.1") -(make-obsolete-variable 'image-dired-cmd-create-temp-image-program - "no longer used." "29.1") - -(defcustom image-dired-cmd-create-temp-image-options - (let ((opts '("-size" "%wx%h" "%f[0]" - "-resize" "%wx%h>" - "-strip" "jpeg:%t"))) - (if (executable-find "gm") (cons "convert" opts) opts)) - "Options of command used to create temporary image for display window. -Used together with `image-dired-cmd-create-temp-image-program', -Available format specifiers are: %w and %h which are replaced by -the calculated max size for width and height in the image display window, -%f which is replaced by the file name of the original image and %t which -is replaced by the file name of the temporary file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-create-temp-image-options - "no longer used." "29.1") - -(defcustom image-dired-display-window-width-correction 1 - "Number to be used to correct image display window width. -Change if the default (1) does not work (i.e. if the image does not -completely fit)." - :type 'integer) -(make-obsolete-variable 'image-dired-display-window-width-correction - "no longer used." "29.1") - -(defcustom image-dired-display-window-height-correction 0 - "Number to be used to correct image display window height. -Change if the default (0) does not work (i.e. if the image does not -completely fit)." - :type 'integer) -(make-obsolete-variable 'image-dired-display-window-height-correction - "no longer used." "29.1") - -(defun image-dired-display-window-width (window) - "Return width, in pixels, of WINDOW." - (declare (obsolete nil "29.1")) - (- (image-dired-window-width-pixels window) - image-dired-display-window-width-correction)) - -(defun image-dired-display-window-height (window) - "Return height, in pixels, of WINDOW." - (declare (obsolete nil "29.1")) - (- (image-dired-window-height-pixels window) - image-dired-display-window-height-correction)) - -(defun image-dired-window-height-pixels (window) - "Calculate WINDOW height in pixels." - (declare (obsolete nil "29.1")) - ;; Note: The mode-line consumes one line - (* (- (window-height window) 1) (frame-char-height))) - -(defcustom image-dired-cmd-read-exif-data-program "exiftool" - "Program used to read EXIF data to image. -Used together with `image-dired-cmd-read-exif-data-options'." - :type 'file) -(make-obsolete-variable 'image-dired-cmd-read-exif-data-program - "use `exif-parse-file' and `exif-field' instead." "29.1") - -(defcustom image-dired-cmd-read-exif-data-options '("-s" "-s" "-s" "-%t" "%f") - "Arguments of command used to read EXIF data. -Used with `image-dired-cmd-read-exif-data-program'. -Available format specifiers are: %f which is replaced -by the image file name and %t which is replaced by the tag name." - :version "26.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-read-exif-data-options - "use `exif-parse-file' and `exif-field' instead." "29.1") - -(defun image-dired-get-exif-data (file tag-name) - "From FILE, return EXIF tag TAG-NAME." - (declare (obsolete "use `exif-parse-file' and `exif-field' instead." "29.1")) - (image-dired--check-executable-exists - 'image-dired-cmd-read-exif-data-program) - (let ((buf (get-buffer-create "*image-dired-get-exif-data*")) - (spec (list (cons ?f file) (cons ?t tag-name))) - tag-value) - (with-current-buffer buf - (delete-region (point-min) (point-max)) - (if (not (eq (apply #'call-process image-dired-cmd-read-exif-data-program - nil t nil - (mapcar - (lambda (arg) (format-spec arg spec)) - image-dired-cmd-read-exif-data-options)) - 0)) - (error "Could not get EXIF tag") - (goto-char (point-min)) - ;; Clean buffer from newlines and carriage returns before - ;; getting final info - (while (search-forward-regexp "[\n\r]" nil t) - (replace-match "" nil t)) - (setq tag-value (buffer-substring (point-min) (point-max))))) - tag-value)) - -(defcustom image-dired-cmd-rotate-thumbnail-program - (if (executable-find "gm") "gm" "mogrify") - "Executable used to rotate thumbnail. -Used together with `image-dired-cmd-rotate-thumbnail-options'." - :type 'file - :version "29.1") -(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-program nil "29.1") - -(defcustom image-dired-cmd-rotate-thumbnail-options - (let ((opts '("-rotate" "%d" "%t"))) - (if (executable-find "gm") (cons "mogrify" opts) opts)) - "Arguments of command used to rotate thumbnail image. -Used with `image-dired-cmd-rotate-thumbnail-program'. -Available format specifiers are: %d which is replaced by the -number of (positive) degrees to rotate the image, normally 90 or 270 -\(for 90 degrees right and left), %t which is replaced by the file name -of the thumbnail file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) -(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-options nil "29.1") - -(defun image-dired-rotate-thumbnail (degrees) - "Rotate thumbnail DEGREES degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (image-dired--check-executable-exists - 'image-dired-cmd-rotate-thumbnail-program) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (let* ((file (image-dired-thumb-name (image-dired-original-file-name))) - (thumb (expand-file-name file)) - (spec (list (cons ?d degrees) (cons ?t thumb)))) - (apply #'call-process image-dired-cmd-rotate-thumbnail-program nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-rotate-thumbnail-options)) - (clear-image-cache thumb)))) - -(defun image-dired-rotate-thumbnail-left () - "Rotate thumbnail left (counter clockwise) 90 degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (interactive) - (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) - (image-dired-rotate-thumbnail "270"))) - -(defun image-dired-rotate-thumbnail-right () - "Rotate thumbnail counter right (clockwise) 90 degrees." - (declare (obsolete image-dired-refresh-thumb "29.1")) - (interactive) - (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) - (image-dired-rotate-thumbnail "90"))) - -(defun image-dired-modify-mark-on-thumb-original-file (command) - "Modify mark in Dired buffer. -COMMAND is one of `mark' for marking file in Dired, `unmark' for -unmarking file in Dired or `flag' for flagging file for delete in -Dired." - (declare (obsolete image-dired--on-file-in-dired-buffer "29.1")) - (let ((file-name (image-dired-original-file-name)) - (dired-buf (image-dired-associated-dired-buffer))) - (if (not (and dired-buf file-name)) - (message "No image, or image with correct properties, at point") - (with-current-buffer dired-buf - (message "%s" file-name) - (when (dired-goto-file file-name) - (cond ((eq command 'mark) (dired-mark 1)) - ((eq command 'unmark) (dired-unmark 1)) - ((eq command 'toggle) - (if (image-dired-dired-file-marked-p) - (dired-unmark 1) - (dired-mark 1))) - ((eq command 'flag) (dired-flag-file-deletion 1))) - (image-dired-thumb-update-marks)))))) - -(defun image-dired-display-current-image-full () - "Display current image in full size." - (declare (obsolete image-transform-original "29.1")) - (interactive nil image-dired-thumbnail-mode) - (let ((file (image-dired-original-file-name))) - (if file - (progn - (image-dired-display-image file) - (with-current-buffer image-dired-display-image-buffer - (image-transform-original))) - (error "No original file name at point")))) - -(defun image-dired-display-current-image-sized () - "Display current image in sized to fit window dimensions." - (declare (obsolete image-mode-fit-frame "29.1")) - (interactive nil image-dired-thumbnail-mode) - (let ((file (image-dired-original-file-name))) - (if file - (progn - (image-dired-display-image file)) - (error "No original file name at point")))) - -(defun image-dired-add-to-tag-file-list (tag file) - "Add relation between TAG and FILE." - (declare (obsolete nil "29.1")) - (let (curr) - (if image-dired-tag-file-list - (if (setq curr (assoc tag image-dired-tag-file-list)) - (if (not (member file curr)) - (setcdr curr (cons file (cdr curr)))) - (setcdr image-dired-tag-file-list - (cons (list tag file) (cdr image-dired-tag-file-list)))) - (setq image-dired-tag-file-list (list (list tag file)))))) - -(defun image-dired-display-thumb-properties () - "Display thumbnail properties in the echo area." - (declare (obsolete image-dired-update-header-line "29.1")) - (image-dired-update-header-line)) - -(defvar image-dired-slideshow-count 0 - "Keeping track on number of images in slideshow.") -(make-obsolete-variable 'image-dired-slideshow-count "no longer used." "29.1") - -(defvar image-dired-slideshow-times 0 - "Number of pictures to display in slideshow.") -(make-obsolete-variable 'image-dired-slideshow-times "no longer used." "29.1") - -(define-obsolete-function-alias 'image-dired-create-display-image-buffer - #'ignore "29.1") -(define-obsolete-function-alias 'image-dired-create-gallery-lists - #'image-dired--create-gallery-lists "29.1") -(define-obsolete-function-alias 'image-dired-add-to-file-comment-list - #'image-dired--add-to-file-comment-list "29.1") -(define-obsolete-function-alias 'image-dired-add-to-tag-file-lists - #'image-dired--add-to-tag-file-lists "29.1") -(define-obsolete-function-alias 'image-dired-hidden-p - #'image-dired--hidden-p "29.1") - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;;;;;;; TEST-SECTION ;;;;;;;;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; (defvar image-dired-dir-max-size 12300000) - -;; (defun image-dired-test-clean-old-files () -;; "Clean `image-dired-dir' from old thumbnail files. -;; \"Oldness\" measured using last access time. If the total size of all -;; thumbnail files in `image-dired-dir' is larger than 'image-dired-dir-max-size', -;; old files are deleted until the max size is reached." -;; (let* ((files -;; (sort -;; (mapcar -;; (lambda (f) -;; (let ((fattribs (file-attributes f))) -;; `(,(file-attribute-access-time fattribs) -;; ,(file-attribute-size fattribs) ,f))) -;; (directory-files (image-dired-dir) t ".+\\.thumb\\..+$")) -;; ;; Sort function. Compare time between two files. -;; (lambda (l1 l2) -;; (time-less-p (car l1) (car l2))))) -;; (dirsize (apply '+ (mapcar (lambda (x) (cadr x)) files)))) -;; (while (> dirsize image-dired-dir-max-size) -;; (y-or-n-p -;; (format "Size of thumbnail directory: %d, delete old file %s? " -;; dirsize (cadr (cdar files)))) -;; (delete-file (cadr (cdar files))) -;; (setq dirsize (- dirsize (car (cdar files)))) -;; (setq files (cdr files))))) +(provide 'image-dired-util) -(provide 'image-dired) +;; Local Variables: +;; nameless-current-name: "image-dired" +;; End: -;;; image-dired.el ends here +;;; image-dired-util.el ends here diff --git a/lisp/image/image-dired.el b/lisp/image/image-dired.el index 9f12354111..707e70201c 100644 --- a/lisp/image/image-dired.el +++ b/lisp/image/image-dired.el @@ -2,9 +2,9 @@ ;; Copyright (C) 2005-2022 Free Software Foundation, Inc. +;; Author: Mathias Dahl ;; Version: 0.4.11 ;; Keywords: multimedia -;; Author: Mathias Dahl ;; This file is part of GNU Emacs. @@ -134,7 +134,6 @@ ;;; Code: (require 'dired) -(require 'exif) (require 'image-mode) (require 'widget) (require 'xdg) @@ -143,6 +142,10 @@ (require 'cl-lib) (require 'wid-edit)) +(require 'image-dired-external) +(require 'image-dired-tags) +(require 'image-dired-util) + ;;; Customizable variables @@ -200,135 +203,10 @@ https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html (const :tag "Per-directory" per-directory)) :version "29.1") -(defconst image-dired--thumbnail-standard-sizes - '( standard standard-large - standard-x-large standard-xx-large) - "List of symbols representing thumbnail sizes in Thumbnail Managing Standard.") - (defcustom image-dired-db-file (expand-file-name ".image-dired_db" image-dired-dir) "Database file where file names and their associated tags are stored." - :type 'file) - -(defcustom image-dired-cmd-create-thumbnail-program - (if (executable-find "gm") "gm" "convert") - "Executable used to create thumbnail. -Used together with `image-dired-cmd-create-thumbnail-options'." - :type 'file - :version "29.1") - -(defcustom image-dired-cmd-create-thumbnail-options - (let ((opts '("-size" "%wx%h" "%f[0]" - "-resize" "%wx%h>" - "-strip" "jpeg:%t"))) - (if (executable-find "gm") (cons "convert" opts) opts)) - "Options of command used to create thumbnail image. -Used with `image-dired-cmd-create-thumbnail-program'. -Available format specifiers are: %w which is replaced by -`image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', -%f which is replaced by the file name of the original image and %t -which is replaced by the file name of the thumbnail file." - :version "29.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-pngnq-program - ;; Prefer pngquant to pngnq-s9 as it is faster on my machine. - ;; The project also seems more active than the alternatives. - ;; Prefer pngnq-s9 to pngnq as it fixes bugs in pngnq. - ;; The pngnq project seems dead (?) since 2011 or so. - (or (executable-find "pngquant") - (executable-find "pngnq-s9") - (executable-find "pngnq")) - "The file name of the `pngquant' or `pngnq' program. -It quantizes colors of PNG images down to 256 colors or fewer -using the NeuQuant algorithm." - :version "29.1" - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-pngnq-options - (if (executable-find "pngquant") - '("--ext" "-nq8.png" "%t") ; same extension as "pngnq" - '("-f" "%t")) - "Arguments to pass `image-dired-cmd-pngnq-program'. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options'." - :type '(repeat (string :tag "Argument")) - :version "29.1") - -(defcustom image-dired-cmd-pngcrush-program (executable-find "pngcrush") - "The file name of the `pngcrush' program. -It optimizes the compression of PNG images. Also it adds PNG textual chunks -with the information required by the Thumbnail Managing Standard." - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-pngcrush-options - `("-q" - "-text" "b" "Description" "Thumbnail of file://%f" - "-text" "b" "Software" ,(emacs-version) - ;; "-text b \"Thumb::Image::Height\" \"%oh\" " - ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" " - ;; "-text b \"Thumb::Image::Width\" \"%ow\" " - "-text" "b" "Thumb::MTime" "%m" - ;; "-text b \"Thumb::Size\" \"%b\" " - "-text" "b" "Thumb::URI" "file://%f" - "%q" "%t") - "Arguments for `image-dired-cmd-pngcrush-program'. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options', with %q for a -temporary file name (typically generated by pnqnq)." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-optipng-program (executable-find "optipng") - "The file name of the `optipng' program." - :version "26.1" - :type '(choice (const :tag "Not Set" nil) file)) - -(defcustom image-dired-cmd-optipng-options '("-o5" "%t") - "Arguments passed to `image-dired-cmd-optipng-program'. -Available format specifiers are described in -`image-dired-cmd-create-thumbnail-options'." - :version "26.1" - :type '(repeat (string :tag "Argument")) - :link '(url-link "man:optipng(1)")) - -(defcustom image-dired-cmd-create-standard-thumbnail-options - (append '("-size" "%wx%h" "%f[0]") - (unless (or image-dired-cmd-pngcrush-program - image-dired-cmd-pngnq-program) - (list - "-set" "Thumb::MTime" "%m" - "-set" "Thumb::URI" "file://%f" - "-set" "Description" "Thumbnail of file://%f" - "-set" "Software" (emacs-version))) - '("-thumbnail" "%wx%h>" "png:%t")) - "Options for creating thumbnails according to the Thumbnail Managing Standard. -Available format specifiers are the same as in -`image-dired-cmd-create-thumbnail-options', with %m for file modification time." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-cmd-rotate-original-program - "jpegtran" - "Executable used to rotate original image. -Used together with `image-dired-cmd-rotate-original-options'." - :type 'file) - -(defcustom image-dired-cmd-rotate-original-options - '("-rotate" "%d" "-copy" "all" "-outfile" "%t" "%o") - "Arguments of command used to rotate original image. -Used with `image-dired-cmd-rotate-original-program'. -Available format specifiers are: %d which is replaced by the -number of (positive) degrees to rotate the image, normally 90 or -270 \(for 90 degrees right and left), %o which is replaced by the -original image file name and %t which is replaced by -`image-dired-temp-image-file'." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - -(defcustom image-dired-temp-rotate-image-file - (expand-file-name ".image-dired_rotate_temp" image-dired-dir) - "Temporary file for rotate operations." + :group 'image-dired :type 'file) (defcustom image-dired-rotate-original-ask-before-overwrite t @@ -337,22 +215,6 @@ If non-nil, ask user for confirmation before overwriting the original file with `image-dired-temp-rotate-image-file'." :type 'boolean) -(defcustom image-dired-cmd-write-exif-data-program - "exiftool" - "Program used to write EXIF data to image. -Used together with `image-dired-cmd-write-exif-data-options'." - :type 'file) - -(defcustom image-dired-cmd-write-exif-data-options - '("-%t=%v" "%f") - "Arguments of command used to write EXIF data. -Used with `image-dired-cmd-write-exif-data-program'. -Available format specifiers are: %f which is replaced by -the image file name, %t which is replaced by the tag name and %v -which is replaced by the tag value." - :version "26.1" - :type '(repeat (string :tag "Argument"))) - (defcustom image-dired-thumb-size (cond ((eq 'standard image-dired-thumbnail-storage) 128) @@ -433,24 +295,6 @@ For more information, see the documentation for `image-dired-toggle-movement-tracking'." :type 'boolean) -(defcustom image-dired-append-when-browsing nil - "Append thumbnails in thumbnail buffer when browsing. -If non-nil, using `image-dired-next-line-and-display' and -`image-dired-previous-line-and-display' will leave a trail of thumbnail -images in the thumbnail buffer. If you enable this and want to clean -the thumbnail buffer because it is filled with too many thumbnails, -just call `image-dired-display-thumb' to display only the image at point. -This value can be toggled using `image-dired-toggle-append-browsing'." - :type 'boolean) - -(defcustom image-dired-dired-disp-props t - "If non-nil, display properties for Dired file when browsing. -Used by `image-dired-next-line-and-display', -`image-dired-previous-line-and-display' and `image-dired-mark-and-display-next'. -If the database file is large, this can slow down image browsing in -Dired and you might want to turn it off." - :type 'boolean) - (defcustom image-dired-display-properties-format "%b: %f (%t): %c" "Display format for thumbnail properties. %b is replaced with associated Dired buffer name, %f with file @@ -504,34 +348,6 @@ This affects the following commands: ;;; Util functions -(defvar image-dired-debug nil - "Non-nil means enable debug messages.") - -(defun image-dired-debug-message (&rest args) - "Display debug message ARGS when `image-dired-debug' is non-nil." - (when image-dired-debug - (apply #'message args))) - -(defmacro image-dired--with-db-file (&rest body) - "Run BODY in a temp buffer containing `image-dired-db-file'. -Return the last form in BODY." - (declare (indent 0) (debug t)) - `(with-temp-buffer - (if (file-exists-p image-dired-db-file) - (insert-file-contents image-dired-db-file)) - ,@body)) - -(defun image-dired-dir () - "Return the current thumbnail directory (from variable `image-dired-dir'). -Create the thumbnail directory if it does not exist." - (let ((image-dired-dir (file-name-as-directory - (expand-file-name image-dired-dir)))) - (unless (file-directory-p image-dired-dir) - (with-file-modes #o700 - (make-directory image-dired-dir t)) - (message "Thumbnail directory created: %s" image-dired-dir)) - image-dired-dir)) - (defun image-dired-insert-image (file type relief margin) "Insert image FILE of image TYPE, using RELIEF and MARGIN, at point." (let ((i `(image :type ,type @@ -582,234 +398,6 @@ Add text properties ORIGINAL-FILE-NAME and ASSOCIATED-DIRED-BUFFER." 'mouse-face 'highlight 'comment (image-dired-get-comment original-file-name))))) -(defun image-dired-thumb-name (file) - "Return absolute file name for thumbnail FILE. -Depending on the value of `image-dired-thumbnail-storage', the -file name of the thumbnail will vary: -- For `use-image-dired-dir', make a SHA1-hash of the image file's - directory name and add that to make the thumbnail file name - unique. -- For `per-directory' storage, just add a subdirectory. -- For `standard' storage, produce the file name according to the - Thumbnail Managing Standard. Among other things, an MD5-hash - of the image file's directory name will be added to the - filename. -See also `image-dired-thumbnail-storage'." - (cond ((memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - (let ((thumbdir (cl-case image-dired-thumbnail-storage - (standard "thumbnails/normal") - (standard-large "thumbnails/large") - (standard-x-large "thumbnails/x-large") - (standard-xx-large "thumbnails/xx-large")))) - (expand-file-name - ;; MD5 is mandated by the Thumbnail Managing Standard. - (concat (md5 (concat "file://" (expand-file-name file))) ".png") - (expand-file-name thumbdir (xdg-cache-home))))) - ((eq 'use-image-dired-dir image-dired-thumbnail-storage) - (let* ((f (expand-file-name file)) - (hash - (md5 (file-name-as-directory (file-name-directory f))))) - (format "%s%s%s.thumb.%s" - (file-name-as-directory (expand-file-name (image-dired-dir))) - (file-name-base f) - (if hash (concat "_" hash) "") - (file-name-extension f)))) - ((eq 'per-directory image-dired-thumbnail-storage) - (let ((f (expand-file-name file))) - (format "%s.image-dired/%s.thumb.%s" - (file-name-directory f) - (file-name-base f) - (file-name-extension f)))))) - -(defun image-dired--check-executable-exists (executable) - (unless (executable-find (symbol-value executable)) - (error "Executable %S not found" executable))) - - -;;; Creating thumbnails - -(defun image-dired-thumb-size (dimension) - "Return thumb size depending on `image-dired-thumbnail-storage'. -DIMENSION should be either the symbol `width' or `height'." - (cond - ((eq 'standard image-dired-thumbnail-storage) 128) - ((eq 'standard-large image-dired-thumbnail-storage) 256) - ((eq 'standard-x-large image-dired-thumbnail-storage) 512) - ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) - (t (cl-ecase dimension - (width image-dired-thumb-width) - (height image-dired-thumb-height))))) - -(defvar image-dired--generate-thumbs-start nil - "Time when `display-thumbs' was called.") - -(defvar image-dired-queue nil - "List of items in the queue. -Each item has the form (ORIGINAL-FILE TARGET-FILE).") - -(defvar image-dired-queue-active-jobs 0 - "Number of active jobs in `image-dired-queue'.") - -(defvar image-dired-queue-active-limit (min 4 (max 2 (/ (num-processors) 2))) - "Maximum number of concurrent jobs permitted for generating images. -Increase at own risk. If you want to experiment with this, -consider setting `image-dired-debug' to a non-nil value to see -the time spent on generating thumbnails. Run `image-clear-cache' -and remove the cached thumbnail files between each trial run.") - -(defun image-dired-pngnq-thumb (spec) - "Quantize thumbnail described by format SPEC with pngnq(1)." - (let ((process - (apply #'start-process "image-dired-pngnq" nil - image-dired-cmd-pngnq-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-pngnq-options)))) - (setf (process-sentinel process) - (lambda (process status) - (if (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - ;; Pass off to pngcrush, or just rename the - ;; THUMB-nq8.png file back to THUMB.png - (if (and image-dired-cmd-pngcrush-program - (executable-find image-dired-cmd-pngcrush-program)) - (image-dired-pngcrush-thumb spec) - (let ((nq8 (cdr (assq ?q spec))) - (thumb (cdr (assq ?t spec)))) - (rename-file nq8 thumb t))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))))) - process)) - -(defun image-dired-pngcrush-thumb (spec) - "Optimize thumbnail described by format SPEC with pngcrush(1)." - ;; If pngnq wasn't run, then the THUMB-nq8.png file does not exist. - ;; pngcrush needs an infile and outfile, so we just copy THUMB to - ;; THUMB-nq8.png and use the latter as a temp file. - (when (not image-dired-cmd-pngnq-program) - (let ((temp (cdr (assq ?q spec))) - (thumb (cdr (assq ?t spec)))) - (copy-file thumb temp))) - (let ((process - (apply #'start-process "image-dired-pngcrush" nil - image-dired-cmd-pngcrush-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-pngcrush-options)))) - (setf (process-sentinel process) - (lambda (process status) - (unless (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))) - (when (memq (process-status process) '(exit signal)) - (let ((temp (cdr (assq ?q spec)))) - (delete-file temp))))) - process)) - -(defun image-dired-optipng-thumb (spec) - "Optimize thumbnail described by format SPEC with optipng(1)." - (let ((process - (apply #'start-process "image-dired-optipng" nil - image-dired-cmd-optipng-program - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-optipng-options)))) - (setf (process-sentinel process) - (lambda (process status) - (unless (and (eq (process-status process) 'exit) - (zerop (process-exit-status process))) - (message "command %S %s" (process-command process) - (string-replace "\n" "" status))))) - process)) - -(defun image-dired-create-thumb-1 (original-file thumbnail-file) - "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE." - (image-dired--check-executable-exists - 'image-dired-cmd-create-thumbnail-program) - (let* ((width (int-to-string (image-dired-thumb-size 'width))) - (height (int-to-string (image-dired-thumb-size 'height))) - (modif-time (format-time-string - "%s" (file-attribute-modification-time - (file-attributes original-file)))) - (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png" - thumbnail-file)) - (spec - (list - (cons ?w width) - (cons ?h height) - (cons ?m modif-time) - (cons ?f original-file) - (cons ?q thumbnail-nq8-file) - (cons ?t thumbnail-file))) - (thumbnail-dir (file-name-directory thumbnail-file)) - process) - (when (not (file-exists-p thumbnail-dir)) - (with-file-modes #o700 - (make-directory thumbnail-dir t)) - (message "Thumbnail directory created: %s" thumbnail-dir)) - - ;; Thumbnail file creation processes begin here and are marshaled - ;; in a queue by `image-dired-create-thumb'. - (setq process - (apply #'start-process "image-dired-create-thumbnail" nil - image-dired-cmd-create-thumbnail-program - (mapcar - (lambda (arg) (format-spec arg spec)) - (if (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - image-dired-cmd-create-standard-thumbnail-options - image-dired-cmd-create-thumbnail-options)))) - - (setf (process-sentinel process) - (lambda (process status) - ;; Trigger next in queue once a thumbnail has been created - (cl-decf image-dired-queue-active-jobs) - (image-dired-thumb-queue-run) - (when (= image-dired-queue-active-jobs 0) - (image-dired-debug-message - (format-time-string - "Generated thumbnails in %s.%3N seconds" - (time-subtract nil - image-dired--generate-thumbs-start)))) - (if (not (and (eq (process-status process) 'exit) - (zerop (process-exit-status process)))) - (message "Thumb could not be created for %s: %s" - (abbreviate-file-name original-file) - (string-replace "\n" "" status)) - (set-file-modes thumbnail-file #o600) - (clear-image-cache thumbnail-file) - ;; PNG thumbnail has been created since we are - ;; following the XDG thumbnail spec, so try to optimize - (when (memq image-dired-thumbnail-storage - image-dired--thumbnail-standard-sizes) - (cond - ((and image-dired-cmd-pngnq-program - (executable-find image-dired-cmd-pngnq-program)) - (image-dired-pngnq-thumb spec)) - ((and image-dired-cmd-pngcrush-program - (executable-find image-dired-cmd-pngcrush-program)) - (image-dired-pngcrush-thumb spec)) - ((and image-dired-cmd-optipng-program - (executable-find image-dired-cmd-optipng-program)) - (image-dired-optipng-thumb spec))))))) - process)) - -(defun image-dired-thumb-queue-run () - "Run a queued job if one exists and not too many jobs are running. -Queued items live in `image-dired-queue'." - (while (and image-dired-queue - (< image-dired-queue-active-jobs - image-dired-queue-active-limit)) - (cl-incf image-dired-queue-active-jobs) - (apply #'image-dired-create-thumb-1 (pop image-dired-queue)))) - -(defun image-dired-create-thumb (original-file thumbnail-file) - "Add a job for generating ORIGINAL-FILE thumbnail to `image-dired-queue'. -The new file will be named THUMBNAIL-FILE." - (setq image-dired-queue - (nconc image-dired-queue - (list (list original-file thumbnail-file)))) - (run-at-time 0 nil #'image-dired-thumb-queue-run)) - (defmacro image-dired--with-marked (&rest body) "Eval BODY with point on each marked thumbnail. If no marked file could be found, execute BODY on the current @@ -826,101 +414,6 @@ thumbnail." (unless found ,@body)))) -;;;###autoload -(defun image-dired-dired-toggle-marked-thumbs (&optional arg) - "Toggle thumbnails in front of file names in the Dired buffer. -If no marked file could be found, insert or hide thumbnails on the -current line. ARG, if non-nil, specifies the files to use instead -of the marked files. If ARG is an integer, use the next ARG (or -previous -ARG, if ARG<0) files." - (interactive "P") - (dired-map-over-marks - (let ((image-pos (dired-move-to-filename)) - (image-file (dired-get-filename nil t)) - thumb-file - overlay) - (when (and image-file - (string-match-p (image-file-name-regexp) image-file)) - (setq thumb-file (image-dired-get-thumbnail-image image-file)) - ;; If image is not already added, then add it. - (let ((thumb-ov (cl-loop for ov in (overlays-in (point) (1+ (point))) - if (overlay-get ov 'thumb-file) return ov))) - (if thumb-ov - (delete-overlay thumb-ov) - (put-image thumb-file image-pos) - (setq overlay - (cl-loop for ov in (overlays-in (point) (1+ (point))) - if (overlay-get ov 'put-image) return ov)) - (overlay-put overlay 'image-file image-file) - (overlay-put overlay 'thumb-file thumb-file))))) - arg ; Show or hide image on ARG next files. - 'show-progress) ; Update dired display after each image is updated. - (add-hook 'dired-after-readin-hook - 'image-dired-dired-after-readin-hook nil t)) - -(defun image-dired-dired-after-readin-hook () - "Relocate existing thumbnail overlays in Dired buffer after reverting. -Move them to their corresponding files if they still exist. -Otherwise, delete overlays." - (mapc (lambda (overlay) - (when (overlay-get overlay 'put-image) - (let* ((image-file (overlay-get overlay 'image-file)) - (image-pos (dired-goto-file image-file))) - (if image-pos - (move-overlay overlay image-pos image-pos) - (delete-overlay overlay))))) - (overlays-in (point-min) (point-max)))) - -(defun image-dired-next-line-and-display () - "Move to next Dired line and display thumbnail image." - (interactive) - (dired-next-line 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-previous-line-and-display () - "Move to previous Dired line and display thumbnail image." - (interactive) - (dired-previous-line 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-toggle-append-browsing () - "Toggle `image-dired-append-when-browsing'." - (interactive) - (setq image-dired-append-when-browsing - (not image-dired-append-when-browsing)) - (message "Append browsing %s" - (if image-dired-append-when-browsing - "on" - "off"))) - -(defun image-dired-mark-and-display-next () - "Mark current file in Dired and display next thumbnail image." - (interactive) - (dired-mark 1) - (image-dired-display-thumbs - t (or image-dired-append-when-browsing nil) t) - (if image-dired-dired-disp-props - (image-dired-dired-display-properties))) - -(defun image-dired-toggle-dired-display-properties () - "Toggle `image-dired-dired-disp-props'." - (interactive) - (setq image-dired-dired-disp-props - (not image-dired-dired-disp-props)) - (message "Dired display properties %s" - (if image-dired-dired-disp-props - "on" - "off"))) - -(defvar image-dired-thumbnail-buffer "*image-dired*" - "Image-Dired's thumbnail buffer.") - (defun image-dired-create-thumbnail-buffer () "Create thumb buffer and set `image-dired-thumbnail-mode'." (let ((buf (get-buffer-create image-dired-thumbnail-buffer))) @@ -930,9 +423,6 @@ Otherwise, delete overlays." (image-dired-thumbnail-mode))) buf)) -(defvar image-dired-display-image-buffer "*image-dired-display-image*" - "Where larger versions of the images are display.") - (defvar image-dired-saved-window-configuration nil "Saved window configuration.") @@ -1069,173 +559,9 @@ never ask for confirmation." ;;;###autoload (defalias 'image-dired 'image-dired-show-all-from-dir) - -;;; Tags - -(defun image-dired-sane-db-file () - "Check if `image-dired-db-file' exists. -If not, try to create it (including any parent directories). -Signal error if there are problems creating it." - (or (file-exists-p image-dired-db-file) - (let (dir buf) - (unless (file-directory-p (setq dir (file-name-directory - image-dired-db-file))) - (with-file-modes #o700 - (make-directory dir t))) - (with-current-buffer (setq buf (create-file-buffer - image-dired-db-file)) - (with-file-modes #o600 - (write-file image-dired-db-file))) - (kill-buffer buf) - (file-exists-p image-dired-db-file)) - (error "Could not create %s" image-dired-db-file))) - -(defvar image-dired-tag-history nil "Variable holding the tag history.") - -(defun image-dired-write-tags (file-tags) - "Write file tags to database. -Write each file and tag in FILE-TAGS to the database. -FILE-TAGS is an alist in the following form: - ((FILE . TAG) ... )" - (image-dired-sane-db-file) - (let (end file tag) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (dolist (elt file-tags) - (setq file (car elt) - tag (cdr elt)) - (goto-char (point-min)) - (if (search-forward-regexp (format "^%s.*$" file) nil t) - (progn - (setq end (point)) - (beginning-of-line) - (when (not (search-forward (format ";%s" tag) end t)) - (end-of-line) - (insert (format ";%s" tag)))) - (goto-char (point-max)) - (insert (format "%s;%s\n" file tag)))) - (save-buffer)))) - -(defun image-dired-remove-tag (files tag) - "For all FILES, remove TAG from the image database." - (image-dired-sane-db-file) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (let (end) - (unless (listp files) - (if (stringp files) - (setq files (list files)) - (error "Files must be a string or a list of strings!"))) - (dolist (file files) - (goto-char (point-min)) - (when (search-forward-regexp (format "^%s;" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (search-forward-regexp - (format "\\(;%s\\)\\($\\|;\\)" tag) end t) - (delete-region (match-beginning 1) (match-end 1)) - ;; Check if file should still be in the database. If - ;; it has no tags or comments, it will be removed. - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (not (search-forward ";" end t)) - (kill-line 1)))))) - (save-buffer))) - -(defun image-dired-list-tags (file) - "Read all tags for image FILE from the image database." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end (tags "")) - (when (search-forward-regexp (format "^%s" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (if (search-forward ";" end t) - (if (search-forward "comment:" end t) - (if (search-forward ";" end t) - (setq tags (buffer-substring (point) end))) - (setq tags (buffer-substring (point) end))))) - (split-string tags ";")))) - -;;;###autoload -(defun image-dired-tag-files (arg) - "Tag marked file(s) in Dired. With prefix ARG, tag file at point." - (interactive "P") - (let ((tag (completing-read - "Tags to add (separate tags with a semicolon): " - image-dired-tag-history nil nil nil 'image-dired-tag-history)) - files) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (image-dired-write-tags - (mapcar - (lambda (x) - (cons x tag)) - files)))) - -(defun image-dired-tag-thumbnail () - "Tag current or marked thumbnails." - (interactive) - (let ((tag (completing-read - "Tags to add (separate tags with a semicolon): " - image-dired-tag-history nil nil nil 'image-dired-tag-history))) - (image-dired--with-marked - (image-dired-write-tags - (list (cons (image-dired-original-file-name) tag))) - (image-dired-update-property - 'tags (image-dired-list-tags (image-dired-original-file-name)))))) - -;;;###autoload -(defun image-dired-delete-tag (arg) - "Remove tag for selected file(s). -With prefix argument ARG, remove tag from file at point." - (interactive "P") - (let ((tag (completing-read "Tag to remove: " image-dired-tag-history - nil nil nil 'image-dired-tag-history)) - files) - (if arg - (setq files (list (dired-get-filename))) - (setq files (dired-get-marked-files))) - (image-dired-remove-tag files tag))) - -(defun image-dired-tag-thumbnail-remove () - "Remove tag from current or marked thumbnails." - (interactive) - (let ((tag (completing-read "Tag to remove: " image-dired-tag-history - nil nil nil 'image-dired-tag-history))) - (image-dired--with-marked - (image-dired-remove-tag (image-dired-original-file-name) tag) - (image-dired-update-property - 'tags (image-dired-list-tags (image-dired-original-file-name)))))) - ;;; Thumbnail mode (cont.) -(defun image-dired-original-file-name () - "Get original file name for thumbnail or display image at point." - (get-text-property (point) 'original-file-name)) - -(defun image-dired-file-name-at-point () - "Get abbreviated file name for thumbnail or display image at point." - (let ((f (image-dired-original-file-name))) - (when f - (abbreviate-file-name f)))) - -(defun image-dired-associated-dired-buffer () - "Get associated Dired buffer at point." - (get-text-property (point) 'associated-dired-buffer)) - -(defun image-dired-get-buffer-window (buf) - "Return window where buffer BUF is." - (get-window-with-predicate - (lambda (window) - (equal (window-buffer window) buf)) - nil t)) - (defun image-dired-track-original-file () "Track the original file in the associated Dired buffer. See documentation for `image-dired-toggle-movement-tracking'. @@ -1260,46 +586,6 @@ position in the other buffer." (setq image-dired-track-movement (not image-dired-track-movement)) (message "Movement tracking %s" (if image-dired-track-movement "on" "off"))) -(defun image-dired-track-thumbnail () - "Track current Dired file's thumb in `image-dired-thumbnail-buffer'. -This is almost the same as what `image-dired-track-original-file' does, -but the other way around." - (let ((file (dired-get-filename)) - prop-val found window) - (when (get-buffer image-dired-thumbnail-buffer) - (with-current-buffer image-dired-thumbnail-buffer - (goto-char (point-min)) - (while (and (not (eobp)) - (not found)) - (if (and (setq prop-val - (get-text-property (point) 'original-file-name)) - (string= prop-val file)) - (setq found t)) - (if (not found) - (forward-char 1))) - (when found - (if (setq window (image-dired-thumbnail-window)) - (set-window-point window (point))) - (image-dired-update-header-line)))))) - -(defun image-dired-dired-next-line (&optional arg) - "Call `dired-next-line', then track thumbnail. -This can safely replace `dired-next-line'. -With prefix argument, move ARG lines." - (interactive "P") - (dired-next-line (or arg 1)) - (if image-dired-track-movement - (image-dired-track-thumbnail))) - -(defun image-dired-dired-previous-line (&optional arg) - "Call `dired-previous-line', then track thumbnail. -This can safely replace `dired-previous-line'. -With prefix argument, move ARG lines." - (interactive "P") - (dired-previous-line (or arg 1)) - (if image-dired-track-movement - (image-dired-track-thumbnail))) - (defun image-dired--display-thumb-properties-fun () (let ((old-buf (current-buffer)) (old-point (point))) @@ -1363,7 +649,6 @@ On reaching end or beginning of buffer, stop and show a message." (image-dired-track-original-file)) (image-dired-update-header-line)) - (defun image-dired-previous-line () "Move to previous line and display properties." (interactive nil image-dired-thumbnail-mode) @@ -1534,19 +819,6 @@ You probably want to use this together with (select-window window)) (message "Associated dired buffer not visible")))) -;;;###autoload -(defun image-dired-jump-thumbnail-buffer () - "Jump to thumbnail buffer." - (interactive) - (let ((window (image-dired-thumbnail-window)) - frame) - (if window - (progn - (if (not (equal (selected-frame) (setq frame (window-frame window)))) - (select-frame-set-input-focus frame)) - (select-window window)) - (message "Thumbnail buffer not visible")))) - (defvar image-dired-thumbnail-mode-line-up-map (let ((map (make-sparse-keymap))) ;; map it to "g" so that the user can press it more quickly @@ -1696,86 +968,6 @@ Resized or in full-size." :interactive nil (add-hook 'file-name-at-point-functions #'image-dired-file-name-at-point nil t)) -(defvar image-dired-minor-mode-map - (let ((map (make-sparse-keymap))) - ;; (set-keymap-parent map dired-mode-map) - ;; Hijack previous and next line movement. Let C-p and C-b be - ;; though... - (define-key map "p" #'image-dired-dired-previous-line) - (define-key map "n" #'image-dired-dired-next-line) - (define-key map [up] #'image-dired-dired-previous-line) - (define-key map [down] #'image-dired-dired-next-line) - - (define-key map (kbd "C-S-n") #'image-dired-next-line-and-display) - (define-key map (kbd "C-S-p") #'image-dired-previous-line-and-display) - (define-key map (kbd "C-S-m") #'image-dired-mark-and-display-next) - - (define-key map "\C-td" #'image-dired-display-thumbs) - (define-key map [tab] #'image-dired-jump-thumbnail-buffer) - (define-key map "\C-ti" #'image-dired-dired-display-image) - (define-key map "\C-tx" #'image-dired-dired-display-external) - (define-key map "\C-ta" #'image-dired-display-thumbs-append) - (define-key map "\C-t." #'image-dired-display-thumb) - (define-key map "\C-tc" #'image-dired-dired-comment-files) - (define-key map "\C-tf" #'image-dired-mark-tagged-files) - map) - "Keymap for `image-dired-minor-mode'.") - -(easy-menu-define image-dired-minor-mode-menu image-dired-minor-mode-map - "Menu for `image-dired-minor-mode'." - '("Image-dired" - ["Display thumb for next file" image-dired-next-line-and-display] - ["Display thumb for previous file" image-dired-previous-line-and-display] - ["Mark and display next" image-dired-mark-and-display-next] - "---" - ["Create thumbnails for marked files" image-dired-create-thumbs] - "---" - ["Display thumbnails append" image-dired-display-thumbs-append] - ["Display this thumbnail" image-dired-display-thumb] - ["Display image" image-dired-dired-display-image] - ["Display in external viewer" image-dired-dired-display-external] - "---" - ["Toggle display properties" image-dired-toggle-dired-display-properties - :style toggle - :selected image-dired-dired-disp-props] - ["Toggle append browsing" image-dired-toggle-append-browsing - :style toggle - :selected image-dired-append-when-browsing] - ["Toggle movement tracking" image-dired-toggle-movement-tracking - :style toggle - :selected image-dired-track-movement] - "---" - ["Jump to thumbnail buffer" image-dired-jump-thumbnail-buffer] - ["Mark tagged files" image-dired-mark-tagged-files] - ["Comment files" image-dired-dired-comment-files] - ["Copy with EXIF file name" image-dired-copy-with-exif-file-name])) - -;;;###autoload -(define-minor-mode image-dired-minor-mode - "Setup easy-to-use keybindings for the commands to be used in Dired mode. -Note that n, p and and will be hijacked and bound to -`image-dired-dired-next-line' and `image-dired-dired-previous-line'." - :keymap image-dired-minor-mode-map) - -(declare-function clear-image-cache "image.c" (&optional filter)) - -(defun image-dired-create-thumbs (&optional arg) - "Create thumbnail images for all marked files in Dired. -With prefix argument ARG, create thumbnails even if they already exist -\(i.e. use this to refresh your thumbnails)." - (interactive "P") - (let (thumb-name) - (dolist (curr-file (dired-get-marked-files)) - (setq thumb-name (image-dired-thumb-name curr-file)) - ;; If the user overrides the exist check, we must clear the - ;; image cache so that if the user wants to display the - ;; thumbnail, it is not fetched from cache. - (when arg - (clear-image-cache (expand-file-name thumb-name))) - (when (or (not (file-exists-p thumb-name)) - arg) - (image-dired-create-thumb curr-file thumb-name))))) - ;;; Slideshow @@ -1844,18 +1036,6 @@ With a negative prefix argument, prompt user for the delay." (when (= (following-char) ?\s) (delete-char 1)))) -;;;###autoload -(defun image-dired-display-thumbs-append () - "Append thumbnails to `image-dired-thumbnail-buffer'." - (interactive) - (image-dired-display-thumbs nil t t)) - -;;;###autoload -(defun image-dired-display-thumb () - "Shorthand for `image-dired-display-thumbs' with prefix argument." - (interactive) - (image-dired-display-thumbs t nil t)) - (defun image-dired-line-up () "Line up thumbnails according to `image-dired-thumbs-per-row'. See also `image-dired-line-up-dynamic'." @@ -1928,43 +1108,6 @@ Ask user how many thumbnails should be displayed per row." (start-process "image-dired-thumb-external" nil image-dired-external-viewer file))))) -;;;###autoload -(defun image-dired-dired-display-external () - "Display file at point using an external viewer." - (interactive) - (let ((file (dired-get-filename))) - (start-process "image-dired-external" nil - image-dired-external-viewer file))) - -(defun image-dired-window-width-pixels (window) - "Calculate WINDOW width in pixels." - (* (window-width window) (frame-char-width))) - -(defun image-dired-display-window () - "Return window where `image-dired-display-image-buffer' is visible." - (get-window-with-predicate - (lambda (window) - (equal (buffer-name (window-buffer window)) image-dired-display-image-buffer)) - nil t)) - -(defun image-dired-thumbnail-window () - "Return window where `image-dired-thumbnail-buffer' is visible." - (get-window-with-predicate - (lambda (window) - (equal (buffer-name (window-buffer window)) image-dired-thumbnail-buffer)) - nil t)) - -(defun image-dired-associated-dired-buffer-window () - "Return window where associated Dired buffer is visible." - (let (buf) - (if (image-dired-image-at-point-p) - (progn - (setq buf (image-dired-associated-dired-buffer)) - (get-window-with-predicate - (lambda (window) - (equal (window-buffer window) buf)))) - (error "No thumbnail image at point")))) - (defun image-dired-display-image (file &optional _ignored) "Display image FILE in image buffer. Use this when you want to display the image, in a new window. @@ -1998,56 +1141,6 @@ With prefix argument ARG, display image in its original size." (message "No original file name found") (image-dired-display-image file arg)))))) - -;;;###autoload -(defun image-dired-dired-display-image (&optional arg) - "Display current image file. -See documentation for `image-dired-display-image' for more information. -With prefix argument ARG, display image in its original size." - (interactive "P") - (image-dired-display-image (dired-get-filename) arg)) - -(defun image-dired-image-at-point-p () - "Return non-nil if there is an `image-dired' thumbnail at point." - (get-text-property (point) 'image-dired-thumbnail)) - -(defun image-dired-refresh-thumb () - "Force creation of new image for current thumbnail." - (interactive nil image-dired-thumbnail-mode) - (let* ((file (image-dired-original-file-name)) - (thumb (expand-file-name (image-dired-thumb-name file)))) - (clear-image-cache (expand-file-name thumb)) - (image-dired-create-thumb file thumb))) - -(defun image-dired-rotate-original (degrees) - "Rotate original image DEGREES degrees." - (image-dired--check-executable-exists - 'image-dired-cmd-rotate-original-program) - (if (not (image-dired-image-at-point-p)) - (message "No image at point") - (let* ((file (image-dired-original-file-name)) - (spec - (list - (cons ?d degrees) - (cons ?o (expand-file-name file)) - (cons ?t image-dired-temp-rotate-image-file)))) - (unless (eq 'jpeg (image-type file)) - (user-error "Only JPEG images can be rotated")) - (if (not (= 0 (apply #'call-process image-dired-cmd-rotate-original-program - nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-rotate-original-options)))) - (error "Could not rotate image") - (image-dired-display-image image-dired-temp-rotate-image-file) - (if (or (and image-dired-rotate-original-ask-before-overwrite - (y-or-n-p - "Rotate to temp file OK. Overwrite original image? ")) - (not image-dired-rotate-original-ask-before-overwrite)) - (progn - (copy-file image-dired-temp-rotate-image-file file t) - (image-dired-refresh-thumb)) - (image-dired-display-image file)))))) - (defun image-dired-rotate-original-left () "Rotate original image left (counter clockwise) 90 degrees. The result of the rotation is displayed in the image display area @@ -2066,91 +1159,6 @@ overwritten. This confirmation can be turned off using (interactive) (image-dired-rotate-original "90")) - -;;; EXIF support - -(defun image-dired-get-exif-file-name (file) - "Use the image's EXIF information to return a unique file name. -The file name should be unique as long as you do not take more than -one picture per second. The original file name is suffixed at the end -for traceability. The format of the returned file name is -YYYY_MM_DD_HH_MM_DD_ORIG_FILE_NAME.jpg. Used from -`image-dired-copy-with-exif-file-name'." - (let (data no-exif-data-found) - (if (not (eq 'jpeg (image-type (expand-file-name file)))) - (setq no-exif-data-found t - data (format-time-string - "%Y:%m:%d %H:%M:%S" - (file-attribute-modification-time - (file-attributes (expand-file-name file))))) - (setq data (exif-field 'date-time (exif-parse-file - (expand-file-name file))))) - (while (string-match "[ :]" data) - (setq data (replace-match "_" nil nil data))) - (format "%s%s%s" data - (if no-exif-data-found - "_noexif_" - "_") - (file-name-nondirectory file)))) - -(defun image-dired-thumbnail-set-image-description () - "Set the ImageDescription EXIF tag for the original image. -If the image already has a value for this tag, it is used as the -default value at the prompt." - (interactive) - (if (not (image-dired-image-at-point-p)) - (message "No thumbnail at point") - (let* ((file (image-dired-original-file-name)) - (old-value (or (exif-field 'description (exif-parse-file file)) ""))) - (if (eq 0 - (image-dired-set-exif-data file "ImageDescription" - (read-string "Value of ImageDescription: " - old-value))) - (message "Successfully wrote ImageDescription tag") - (error "Could not write ImageDescription tag"))))) - -(defun image-dired-set-exif-data (file tag-name tag-value) - "In FILE, set EXIF tag TAG-NAME to value TAG-VALUE." - (image-dired--check-executable-exists - 'image-dired-cmd-write-exif-data-program) - (let ((spec - (list - (cons ?f (expand-file-name file)) - (cons ?t tag-name) - (cons ?v tag-value)))) - (apply #'call-process image-dired-cmd-write-exif-data-program nil nil nil - (mapcar (lambda (arg) (format-spec arg spec)) - image-dired-cmd-write-exif-data-options)))) - -(defun image-dired-copy-with-exif-file-name () - "Copy file with unique name to main image directory. -Copy current or all marked files in Dired to a new file in your -main image directory, using a file name generated by -`image-dired-get-exif-file-name'. A typical usage for this if when -copying images from a digital camera into the image directory. - - Typically, you would open up the folder with the incoming -digital images, mark the files to be copied, and execute this -function. The result is a couple of new files in -`image-dired-main-image-directory' called -2005_05_08_12_52_00_dscn0319.jpg, -2005_05_08_14_27_45_dscn0320.jpg etc." - (interactive) - (let (new-name - (files (dired-get-marked-files))) - (mapc - (lambda (curr-file) - (setq new-name - (format "%s/%s" - (file-name-as-directory - (expand-file-name image-dired-main-image-directory)) - (image-dired-get-exif-file-name curr-file))) - (message "Copying %s to %s" curr-file new-name) - (copy-file curr-file new-name)) - files))) - -;;; Thumbnail mode (cont.) - (defun image-dired-display-next-thumbnail-original (&optional arg) "Move to the next image in the thumbnail buffer and display it. With prefix ARG, move that many thumbnails." @@ -2168,62 +1176,6 @@ With prefix ARG, move that many thumbnails." ;;; Image Comments -(defun image-dired-write-comments (file-comments) - "Write file comments to database. -Write file comments to one or more files. -FILE-COMMENTS is an alist on the following form: - ((FILE . COMMENT) ... )" - (image-dired-sane-db-file) - (let (end comment-beg-pos comment-end-pos file comment) - (image-dired--with-db-file - (setq buffer-file-name image-dired-db-file) - (dolist (elt file-comments) - (setq file (car elt) - comment (cdr elt)) - (goto-char (point-min)) - (if (search-forward-regexp (format "^%s.*$" file) nil t) - (progn - (setq end (point)) - (beginning-of-line) - ;; Delete old comment, if any - (when (search-forward ";comment:" end t) - (setq comment-beg-pos (match-beginning 0)) - ;; Any tags after the comment? - (if (search-forward ";" end t) - (setq comment-end-pos (- (point) 1)) - (setq comment-end-pos end)) - ;; Delete comment tag and comment - (delete-region comment-beg-pos comment-end-pos)) - ;; Insert new comment - (beginning-of-line) - (unless (search-forward ";" end t) - (end-of-line) - (insert ";")) - (insert (format "comment:%s;" comment))) - ;; File does not exist in database - add it. - (goto-char (point-max)) - (insert (format "%s;comment:%s\n" file comment)))) - (save-buffer)))) - -(defun image-dired-update-property (prop value) - "Update text property PROP with value VALUE at point." - (let ((inhibit-read-only t)) - (put-text-property - (point) (1+ (point)) - prop - value))) - -;;;###autoload -(defun image-dired-dired-comment-files () - "Add comment to current or marked files in Dired." - (interactive) - (let ((comment (image-dired-read-comment))) - (image-dired-write-comments - (mapcar - (lambda (curr-file) - (cons curr-file comment)) - (dired-get-marked-files))))) - (defun image-dired-comment-thumbnail () "Add comment to current thumbnail in thumbnail buffer." (interactive) @@ -2233,71 +1185,6 @@ FILE-COMMENTS is an alist on the following form: (image-dired-update-property 'comment comment)) (image-dired-update-header-line)) -(defun image-dired-read-comment (&optional file) - "Read comment for an image. -Optionally use old comment from FILE as initial value." - (let ((comment - (read-string - "Comment: " - (if file (image-dired-get-comment file))))) - comment)) - -(defun image-dired-get-comment (file) - "Get comment for file FILE." - (image-dired-sane-db-file) - (image-dired--with-db-file - (let (end comment-beg-pos comment-end-pos comment) - (when (search-forward-regexp (format "^%s" file) nil t) - (end-of-line) - (setq end (point)) - (beginning-of-line) - (when (search-forward ";comment:" end t) - (setq comment-beg-pos (point)) - (if (search-forward ";" end t) - (setq comment-end-pos (- (point) 1)) - (setq comment-end-pos end)) - (setq comment (buffer-substring - comment-beg-pos comment-end-pos)))) - comment))) - -;;;###autoload -(defun image-dired-mark-tagged-files (regexp) - "Use REGEXP to mark files with matching tag. -A `tag' is a keyword, a piece of meta data, associated with an -image file and stored in image-dired's database file. This command -lets you input a regexp and this will be matched against all tags -on all image files in the database file. The files that have a -matching tag will be marked in the Dired buffer." - (interactive "sMark tagged files (regexp): ") - (image-dired-sane-db-file) - (let ((hits 0) - files) - (image-dired--with-db-file - ;; Collect matches - (while (search-forward-regexp "\\(^[^;\n]+\\);\\(.*\\)" nil t) - (let ((file (match-string 1)) - (tags (split-string (match-string 2) ";"))) - (when (seq-find (lambda (tag) - (string-match-p regexp tag)) - tags) - (push file files))))) - ;; Mark files - (dolist (curr-file files) - ;; I tried using `dired-mark-files-regexp' but it was waaaay to - ;; slow. Don't bother about hits found in other directories - ;; than the current one. - (when (string= (file-name-as-directory - (expand-file-name default-directory)) - (file-name-as-directory - (file-name-directory curr-file))) - (setq curr-file (file-name-nondirectory curr-file)) - (goto-char (point-min)) - (when (search-forward-regexp (format "\\s %s$" curr-file) nil t) - (setq hits (+ hits 1)) - (dired-mark 1)))) - (message "%d files with matching tag marked" hits))) - - ;;; Mouse support @@ -2408,24 +1295,6 @@ Track this in associated Dired buffer if (image-dired-mouse-toggle-mark-1)) (image-dired-thumb-update-marks)) -(defun image-dired-dired-display-properties () - "Display properties for Dired file in the echo area." - (interactive) - (let* ((file (dired-get-filename)) - (file-name (file-name-nondirectory file)) - (dired-buf (buffer-name (current-buffer))) - (props (mapconcat #'identity (image-dired-list-tags file) ", ")) - (comment (image-dired-get-comment file)) - (message-log-max nil)) - (if file-name - (message "%s" - (image-dired-format-properties-string - dired-buf - file-name - props - comment))))) - - ;;; Gallery support @@ -2660,110 +1529,6 @@ when using per-directory thumbnail file storage")) (insert " \n") (insert "")))) - -;;; Tag support - -(defvar image-dired-widget-list nil - "List to keep track of meta data in edit buffer.") - -(declare-function widget-forward "wid-edit" (arg)) - -;;;###autoload -(defun image-dired-dired-edit-comment-and-tags () - "Edit comment and tags of current or marked image files. -Edit comment and tags for all marked image files in an -easy-to-use form." - (interactive) - (setq image-dired-widget-list nil) - ;; Setup buffer. - (let ((files (dired-get-marked-files))) - (pop-to-buffer-same-window "*Image-Dired Edit Meta Data*") - (kill-all-local-variables) - (let ((inhibit-read-only t)) - (erase-buffer)) - (remove-overlays) - ;; Some help for the user. - (widget-insert -"\nEdit comments and tags for each image. Separate multiple tags -with a comma. Move forward between fields using TAB or RET. -Move to the previous field using backtab (S-TAB). Save by -activating the Save button at the bottom of the form or cancel -the operation by activating the Cancel button.\n\n") - ;; Here comes all images and a comment and tag field for each - ;; image. - (let (thumb-file img comment-widget tag-widget) - - (dolist (file files) - - (setq thumb-file (image-dired-thumb-name file) - img (create-image thumb-file)) - - (insert-image img) - (widget-insert "\n\nComment: ") - (setq comment-widget - (widget-create 'editable-field - :size 60 - :format "%v " - :value (or (image-dired-get-comment file) ""))) - (widget-insert "\nTags: ") - (setq tag-widget - (widget-create 'editable-field - :size 60 - :format "%v " - :value (or (mapconcat - #'identity - (image-dired-list-tags file) - ",") ""))) - ;; Save information in all widgets so that we can use it when - ;; the user saves the form. - (setq image-dired-widget-list - (append image-dired-widget-list - (list (list file comment-widget tag-widget)))) - (widget-insert "\n\n"))) - - ;; Footer with Save and Cancel button. - (widget-insert "\n") - (widget-create 'push-button - :notify - (lambda (&rest _ignore) - (image-dired-save-information-from-widgets) - (bury-buffer) - (message "Done")) - "Save") - (widget-insert " ") - (widget-create 'push-button - :notify - (lambda (&rest _ignore) - (bury-buffer) - (message "Operation canceled")) - "Cancel") - (widget-insert "\n") - (use-local-map widget-keymap) - (widget-setup) - ;; Jump to the first widget. - (widget-forward 1))) - -(defun image-dired-save-information-from-widgets () - "Save information found in `image-dired-widget-list'. -Use the information in `image-dired-widget-list' to save comments and -tags to their respective image file. Internal function used by -`image-dired-dired-edit-comment-and-tags'." - (let (file comment tag-string tag-list lst) - (image-dired-write-comments - (mapcar - (lambda (widget) - (setq file (car widget) - comment (widget-value (cadr widget))) - (cons file comment)) - image-dired-widget-list)) - (image-dired-write-tags - (dolist (widget image-dired-widget-list lst) - (setq file (car widget) - tag-string (widget-value (car (cddr widget))) - tag-list (split-string tag-string ",")) - (dolist (tag tag-list) - (push (cons file tag) lst)))))) - ;;; bookmark.el support commit 0504f39259f0afb0bfeb73b294f523d20b20091c Author: Stefan Kangas Date: Fri Aug 19 20:41:11 2022 +0200 Split image-dired.el into several files (part 1/2) Use a git trick to split a file while preserving line history (for "git blame", "git log --follow", etc.): 1) Make exact copies of the original file, in the same commit as moving it. [this commit] 2) Next, trim down the extra copies to contain only the relevant parts. * lisp/image-dired.el: Move from here... * lisp/image/image-dired-dired.el: * lisp/image/image-dired-external.el: * lisp/image/image-dired-tags.el: * lisp/image/image-dired-util.el: * lisp/image/image-dired.el: ...to here. * test/lisp/image-dired-tests.el: Move from here... * test/lisp/image/image-dired-tests.el: ...to here. diff --git a/lisp/image-dired.el b/lisp/image/image-dired-dired.el similarity index 100% rename from lisp/image-dired.el rename to lisp/image/image-dired-dired.el diff --git a/lisp/image/image-dired-external.el b/lisp/image/image-dired-external.el new file mode 100644 index 0000000000..9f12354111 --- /dev/null +++ b/lisp/image/image-dired-external.el @@ -0,0 +1,3080 @@ +;;; image-dired.el --- use dired to browse and manipulate your images -*- lexical-binding: t -*- + +;; Copyright (C) 2005-2022 Free Software Foundation, Inc. + +;; Version: 0.4.11 +;; Keywords: multimedia +;; Author: Mathias Dahl + +;; 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: + +;; BACKGROUND +;; ========== +;; +;; I needed a program to browse, organize and tag my pictures. I got +;; tired of the old gallery program I used as it did not allow +;; multi-file operations easily. Also, it put things out of my +;; control. Image viewing programs I tested did not allow multi-file +;; operations or did not do what I wanted it to. +;; +;; So, I got the idea to use the wonderful functionality of Emacs and +;; `dired' to do it. It would allow me to do almost anything I wanted, +;; which is basically just to browse all my pictures in an easy way, +;; letting me manipulate and tag them in various ways. `dired' already +;; provide all the file handling and navigation facilities; I only +;; needed to add some functions to display the images. +;; +;; I briefly tried out thumbs.el, and although it seemed more +;; powerful than this package, it did not work the way I wanted to. It +;; was too slow to create thumbnails of all files in a directory (I +;; currently keep all my 2000+ images in the same directory) and +;; browsing the thumbnail buffer was slow too. image-dired.el will not +;; create thumbnails until they are needed and the browsing is done +;; quickly and easily in Dired. I copied a great deal of ideas and +;; code from there though... :) +;; +;; `image-dired' stores the thumbnail files in `image-dired-dir' +;; using the file name format ORIGNAME.thumb.ORIGEXT. For example +;; ~/.emacs.d/image-dired/myimage01.thumb.jpg. The "database" is for +;; now just a plain text file with the following format: +;; +;; file-name-non-directory;comment:comment-text;tag1;tag2;tag3;...;tagN +;; +;; +;; PREREQUISITES +;; ============= +;; +;; * The GraphicsMagick or ImageMagick package; Image-Dired uses +;; whichever is available. +;; +;; A) For GraphicsMagick, `gm' is used. +;; Find it here: http://www.graphicsmagick.org/ +;; +;; B) For ImageMagick, `convert' and `mogrify' are used. +;; Find it here: https://www.imagemagick.org. +;; +;; * For non-lossy rotation of JPEG images, the JpegTRAN program is +;; needed. +;; +;; * For `image-dired-set-exif-data' to work, the command line tool `exiftool' is +;; needed. It can be found here: https://exiftool.org/. This +;; function is, among other things, used for writing comments to +;; image files using `image-dired-thumbnail-set-image-description'. +;; +;; +;; USAGE +;; ===== +;; +;; This information has been moved to the manual. Type `C-h r' to open +;; the Emacs manual and go to the node Thumbnails by typing `g +;; Image-Dired RET'. +;; +;; Quickstart: M-x image-dired RET DIRNAME RET +;; +;; where DIRNAME is a directory containing image files. +;; +;; LIMITATIONS +;; =========== +;; +;; * Supports all image formats that Emacs and convert supports, but +;; the thumbnails are hard-coded to JPEG or PNG format. It uses +;; JPEG by default, but can optionally follow the Thumbnail Managing +;; Standard (v0.9.0, Dec 2020), which mandates PNG. See the user +;; option `image-dired-thumbnail-storage'. +;; +;; * WARNING: The "database" format used might be changed so keep a +;; backup of `image-dired-db-file' when testing new versions. +;; +;; TODO +;; ==== +;; +;; * Investigate if it is possible to also write the tags to the image +;; files. +;; +;; * From thumbs.el: Add an option for clean-up/max-size functionality +;; for thumbnail directory. +;; +;; * From thumbs.el: Add setroot function. +;; +;; * Add `image-dired-display-thumbs-ring' and functions to cycle that. Find out +;; which is best, saving old batch just before inserting new, or +;; saving the current batch in the ring when inserting it. Adding +;; it probably needs rewriting `image-dired-display-thumbs' to be more general. +;; +;; * Find some way of toggling on and off really nice keybindings in +;; Dired (for example, using C-n or instead of C-S-n). +;; Richard suggested that we could keep C-t as prefix for +;; image-dired commands as it is currently not used in Dired. He +;; also suggested that `dired-next-line' and `dired-previous-line' +;; figure out if image-dired is enabled in the current buffer and, +;; if it is, call `image-dired-dired-next-line' and `image-dired-dired-previous-line', +;; respectively. Update: This is partly done; some bindings have +;; now been added to Dired. +;; +;; * In some way keep track of buffers and windows and stuff so that +;; it works as the user expects. +;; +;; * More/better documentation. + +;;; Code: + +(require 'dired) +(require 'exif) +(require 'image-mode) +(require 'widget) +(require 'xdg) + +(eval-when-compile + (require 'cl-lib) + (require 'wid-edit)) + + +;;; Customizable variables + +(defgroup image-dired nil + "Use Dired to browse your images as thumbnails, and more." + :prefix "image-dired-" + :link '(info-link "(emacs) Image-Dired") + :group 'multimedia) + +(defcustom image-dired-dir (locate-user-emacs-file "image-dired/") + "Directory where thumbnail images are stored. + +The value of this option will be ignored if Image-Dired is +customized to use the Thumbnail Managing Standard; they will be +saved in \"$XDG_CACHE_HOME/thumbnails/\" instead. See +`image-dired-thumbnail-storage'." + :type 'directory) + +(defcustom image-dired-thumbnail-storage 'use-image-dired-dir + "How `image-dired' stores thumbnail files. +There are two ways that Image-Dired can store and generate +thumbnails. If you set this variable to one of the two following +values, they will be stored in the JPEG format: + +- `use-image-dired-dir' means that the thumbnails are stored in a + central directory. + +- `per-directory' means that each thumbnail is stored in a + subdirectory called \".image-dired\" in the same directory + where the image file is. + +It can also use the \"Thumbnail Managing Standard\", which allows +sharing of thumbnails across different programs. Thumbnails will +be stored in \"$XDG_CACHE_HOME/thumbnails/\" instead of in +`image-dired-dir'. Thumbnails are saved in the PNG format, and +can be one of the following sizes: + +- `standard' means use thumbnails sized 128x128. +- `standard-large' means use thumbnails sized 256x256. +- `standard-x-large' means use thumbnails sized 512x512. +- `standard-xx-large' means use thumbnails sized 1024x1024. + +For more information on the Thumbnail Managing Standard, see: +https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html" + :type '(choice :tag "How to store thumbnail files" + (const :tag "Use image-dired-dir" use-image-dired-dir) + (const :tag "Thumbnail Managing Standard (normal 128x128)" + standard) + (const :tag "Thumbnail Managing Standard (large 256x256)" + standard-large) + (const :tag "Thumbnail Managing Standard (larger 512x512)" + standard-x-large) + (const :tag "Thumbnail Managing Standard (extra large 1024x1024)" + standard-xx-large) + (const :tag "Per-directory" per-directory)) + :version "29.1") + +(defconst image-dired--thumbnail-standard-sizes + '( standard standard-large + standard-x-large standard-xx-large) + "List of symbols representing thumbnail sizes in Thumbnail Managing Standard.") + +(defcustom image-dired-db-file + (expand-file-name ".image-dired_db" image-dired-dir) + "Database file where file names and their associated tags are stored." + :type 'file) + +(defcustom image-dired-cmd-create-thumbnail-program + (if (executable-find "gm") "gm" "convert") + "Executable used to create thumbnail. +Used together with `image-dired-cmd-create-thumbnail-options'." + :type 'file + :version "29.1") + +(defcustom image-dired-cmd-create-thumbnail-options + (let ((opts '("-size" "%wx%h" "%f[0]" + "-resize" "%wx%h>" + "-strip" "jpeg:%t"))) + (if (executable-find "gm") (cons "convert" opts) opts)) + "Options of command used to create thumbnail image. +Used with `image-dired-cmd-create-thumbnail-program'. +Available format specifiers are: %w which is replaced by +`image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', +%f which is replaced by the file name of the original image and %t +which is replaced by the file name of the thumbnail file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-pngnq-program + ;; Prefer pngquant to pngnq-s9 as it is faster on my machine. + ;; The project also seems more active than the alternatives. + ;; Prefer pngnq-s9 to pngnq as it fixes bugs in pngnq. + ;; The pngnq project seems dead (?) since 2011 or so. + (or (executable-find "pngquant") + (executable-find "pngnq-s9") + (executable-find "pngnq")) + "The file name of the `pngquant' or `pngnq' program. +It quantizes colors of PNG images down to 256 colors or fewer +using the NeuQuant algorithm." + :version "29.1" + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-pngnq-options + (if (executable-find "pngquant") + '("--ext" "-nq8.png" "%t") ; same extension as "pngnq" + '("-f" "%t")) + "Arguments to pass `image-dired-cmd-pngnq-program'. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options'." + :type '(repeat (string :tag "Argument")) + :version "29.1") + +(defcustom image-dired-cmd-pngcrush-program (executable-find "pngcrush") + "The file name of the `pngcrush' program. +It optimizes the compression of PNG images. Also it adds PNG textual chunks +with the information required by the Thumbnail Managing Standard." + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-pngcrush-options + `("-q" + "-text" "b" "Description" "Thumbnail of file://%f" + "-text" "b" "Software" ,(emacs-version) + ;; "-text b \"Thumb::Image::Height\" \"%oh\" " + ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" " + ;; "-text b \"Thumb::Image::Width\" \"%ow\" " + "-text" "b" "Thumb::MTime" "%m" + ;; "-text b \"Thumb::Size\" \"%b\" " + "-text" "b" "Thumb::URI" "file://%f" + "%q" "%t") + "Arguments for `image-dired-cmd-pngcrush-program'. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options', with %q for a +temporary file name (typically generated by pnqnq)." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-optipng-program (executable-find "optipng") + "The file name of the `optipng' program." + :version "26.1" + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-optipng-options '("-o5" "%t") + "Arguments passed to `image-dired-cmd-optipng-program'. +Available format specifiers are described in +`image-dired-cmd-create-thumbnail-options'." + :version "26.1" + :type '(repeat (string :tag "Argument")) + :link '(url-link "man:optipng(1)")) + +(defcustom image-dired-cmd-create-standard-thumbnail-options + (append '("-size" "%wx%h" "%f[0]") + (unless (or image-dired-cmd-pngcrush-program + image-dired-cmd-pngnq-program) + (list + "-set" "Thumb::MTime" "%m" + "-set" "Thumb::URI" "file://%f" + "-set" "Description" "Thumbnail of file://%f" + "-set" "Software" (emacs-version))) + '("-thumbnail" "%wx%h>" "png:%t")) + "Options for creating thumbnails according to the Thumbnail Managing Standard. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options', with %m for file modification time." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-rotate-original-program + "jpegtran" + "Executable used to rotate original image. +Used together with `image-dired-cmd-rotate-original-options'." + :type 'file) + +(defcustom image-dired-cmd-rotate-original-options + '("-rotate" "%d" "-copy" "all" "-outfile" "%t" "%o") + "Arguments of command used to rotate original image. +Used with `image-dired-cmd-rotate-original-program'. +Available format specifiers are: %d which is replaced by the +number of (positive) degrees to rotate the image, normally 90 or +270 \(for 90 degrees right and left), %o which is replaced by the +original image file name and %t which is replaced by +`image-dired-temp-image-file'." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-temp-rotate-image-file + (expand-file-name ".image-dired_rotate_temp" image-dired-dir) + "Temporary file for rotate operations." + :type 'file) + +(defcustom image-dired-rotate-original-ask-before-overwrite t + "Confirm overwrite of original file after rotate operation. +If non-nil, ask user for confirmation before overwriting the +original file with `image-dired-temp-rotate-image-file'." + :type 'boolean) + +(defcustom image-dired-cmd-write-exif-data-program + "exiftool" + "Program used to write EXIF data to image. +Used together with `image-dired-cmd-write-exif-data-options'." + :type 'file) + +(defcustom image-dired-cmd-write-exif-data-options + '("-%t=%v" "%f") + "Arguments of command used to write EXIF data. +Used with `image-dired-cmd-write-exif-data-program'. +Available format specifiers are: %f which is replaced by +the image file name, %t which is replaced by the tag name and %v +which is replaced by the tag value." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-thumb-size + (cond + ((eq 'standard image-dired-thumbnail-storage) 128) + ((eq 'standard-large image-dired-thumbnail-storage) 256) + ((eq 'standard-x-large image-dired-thumbnail-storage) 512) + ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) + (t 100)) + "Size of thumbnails, in pixels. +This is the default size for both `image-dired-thumb-width' +and `image-dired-thumb-height'. + +The value of this option will be ignored if Image-Dired is +customized to use the Thumbnail Managing Standard; the standard +sizes will be used instead. See `image-dired-thumbnail-storage'." + :type 'integer) + +(defcustom image-dired-thumb-width image-dired-thumb-size + "Width of thumbnails, in pixels." + :type 'integer) + +(defcustom image-dired-thumb-height image-dired-thumb-size + "Height of thumbnails, in pixels." + :type 'integer) + +(defcustom image-dired-thumb-relief 2 + "Size of button-like border around thumbnails." + :type 'integer) + +(defcustom image-dired-thumb-margin 2 + "Size of the margin around thumbnails. +This is where you see the cursor." + :type 'integer) + +(defcustom image-dired-thumb-visible-marks t + "Make marks and flags visible in thumbnail buffer. +If non-nil, apply the `image-dired-thumb-mark' face to marked +images and `image-dired-thumb-flagged' to images flagged for +deletion." + :type 'boolean + :version "28.1") + +(defface image-dired-thumb-mark + '((((class color) (min-colors 16)) :background "DarkOrange") + (((class color)) :foreground "yellow")) + "Face for marked images in thumbnail buffer." + :version "29.1") + +(defface image-dired-thumb-flagged + '((((class color) (min-colors 88) (background light)) :background "Red3") + (((class color) (min-colors 88) (background dark)) :background "Pink") + (((class color) (min-colors 16) (background light)) :background "Red3") + (((class color) (min-colors 16) (background dark)) :background "Pink") + (((class color) (min-colors 8)) :background "red") + (t :inverse-video t)) + "Face for images flagged for deletion in thumbnail buffer." + :version "29.1") + +(defcustom image-dired-line-up-method 'dynamic + "Default method for line-up of thumbnails in thumbnail buffer. +Used by `image-dired-display-thumbs' and other functions that needs +to line-up thumbnails. Dynamic means to use the available width of +the window containing the thumbnail buffer, Fixed means to use +`image-dired-thumbs-per-row', Interactive is for asking the user, +and No line-up means that no automatic line-up will be done." + :type '(choice :tag "Default line-up method" + (const :tag "Dynamic" dynamic) + (const :tag "Fixed" fixed) + (const :tag "Interactive" interactive) + (const :tag "No line-up" none))) + +(defcustom image-dired-thumbs-per-row 3 + "Number of thumbnails to display per row in thumb buffer." + :type 'integer) + +(defcustom image-dired-track-movement t + "The current state of the tracking and mirroring. +For more information, see the documentation for +`image-dired-toggle-movement-tracking'." + :type 'boolean) + +(defcustom image-dired-append-when-browsing nil + "Append thumbnails in thumbnail buffer when browsing. +If non-nil, using `image-dired-next-line-and-display' and +`image-dired-previous-line-and-display' will leave a trail of thumbnail +images in the thumbnail buffer. If you enable this and want to clean +the thumbnail buffer because it is filled with too many thumbnails, +just call `image-dired-display-thumb' to display only the image at point. +This value can be toggled using `image-dired-toggle-append-browsing'." + :type 'boolean) + +(defcustom image-dired-dired-disp-props t + "If non-nil, display properties for Dired file when browsing. +Used by `image-dired-next-line-and-display', +`image-dired-previous-line-and-display' and `image-dired-mark-and-display-next'. +If the database file is large, this can slow down image browsing in +Dired and you might want to turn it off." + :type 'boolean) + +(defcustom image-dired-display-properties-format "%b: %f (%t): %c" + "Display format for thumbnail properties. +%b is replaced with associated Dired buffer name, %f with file +name (without path) of original image file, %t with the list of +tags and %c with the comment." + :type 'string) + +(defcustom image-dired-external-viewer + ;; TODO: Use mailcap, dired-guess-shell-alist-default, + ;; dired-view-command-alist. + (cond ((executable-find "display")) + ((executable-find "xli")) + ((executable-find "qiv") "qiv -t") + ((executable-find "feh") "feh")) + "Name of external viewer. +Including parameters. Used when displaying original image from +`image-dired-thumbnail-mode'." + :version "28.1" + :type '(choice string + (const :tag "Not Set" nil))) + +(defcustom image-dired-main-image-directory + (or (xdg-user-dir "PICTURES") "~/pics/") + "Name of main image directory, if any. +Used by `image-dired-copy-with-exif-file-name'." + :type 'string + :version "29.1") + +(defcustom image-dired-show-all-from-dir-max-files 500 + "Maximum number of files in directory before prompting. + +If there are more image files than this in a selected directory, +the `image-dired-show-all-from-dir' command will ask for +confirmation before creating the thumbnail buffer. If this +variable is nil, it will never ask." + :type '(choice integer + (const :tag "Disable warning" nil)) + :version "29.1") + +(defcustom image-dired-marking-shows-next t + "If non-nil, marking, unmarking or flagging an image shows the next image. + +This affects the following commands: +\\ + `image-dired-flag-thumb-original-file' (bound to \\[image-dired-flag-thumb-original-file]) + `image-dired-mark-thumb-original-file' (bound to \\[image-dired-mark-thumb-original-file]) + `image-dired-unmark-thumb-original-file' (bound to \\[image-dired-unmark-thumb-original-file])" + :type 'boolean + :version "29.1") + + +;;; Util functions + +(defvar image-dired-debug nil + "Non-nil means enable debug messages.") + +(defun image-dired-debug-message (&rest args) + "Display debug message ARGS when `image-dired-debug' is non-nil." + (when image-dired-debug + (apply #'message args))) + +(defmacro image-dired--with-db-file (&rest body) + "Run BODY in a temp buffer containing `image-dired-db-file'. +Return the last form in BODY." + (declare (indent 0) (debug t)) + `(with-temp-buffer + (if (file-exists-p image-dired-db-file) + (insert-file-contents image-dired-db-file)) + ,@body)) + +(defun image-dired-dir () + "Return the current thumbnail directory (from variable `image-dired-dir'). +Create the thumbnail directory if it does not exist." + (let ((image-dired-dir (file-name-as-directory + (expand-file-name image-dired-dir)))) + (unless (file-directory-p image-dired-dir) + (with-file-modes #o700 + (make-directory image-dired-dir t)) + (message "Thumbnail directory created: %s" image-dired-dir)) + image-dired-dir)) + +(defun image-dired-insert-image (file type relief margin) + "Insert image FILE of image TYPE, using RELIEF and MARGIN, at point." + (let ((i `(image :type ,type + :file ,file + :relief ,relief + :margin ,margin))) + (insert-image i))) + +(defun image-dired-get-thumbnail-image (file) + "Return the image descriptor for a thumbnail of image file FILE." + (unless (string-match-p (image-file-name-regexp) file) + (error "%s is not a valid image file" file)) + (let* ((thumb-file (image-dired-thumb-name file)) + (thumb-attr (file-attributes thumb-file))) + (when (or (not thumb-attr) + (time-less-p (file-attribute-modification-time thumb-attr) + (file-attribute-modification-time + (file-attributes file)))) + (image-dired-create-thumb file thumb-file)) + (create-image thumb-file))) + +(defun image-dired-insert-thumbnail (file original-file-name + associated-dired-buffer) + "Insert thumbnail image FILE. +Add text properties ORIGINAL-FILE-NAME and ASSOCIATED-DIRED-BUFFER." + (let (beg end) + (setq beg (point)) + (image-dired-insert-image + file + ;; Thumbnails are created asynchronously, so we might not yet + ;; have a file. But if it exists, it might have been cached from + ;; before and we should use it instead of our current settings. + (or (and (file-exists-p file) + (image-type-from-file-header file)) + (and (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + 'png) + 'jpeg) + image-dired-thumb-relief + image-dired-thumb-margin) + (setq end (point)) + (add-text-properties + beg end + (list 'image-dired-thumbnail t + 'original-file-name original-file-name + 'associated-dired-buffer associated-dired-buffer + 'tags (image-dired-list-tags original-file-name) + 'mouse-face 'highlight + 'comment (image-dired-get-comment original-file-name))))) + +(defun image-dired-thumb-name (file) + "Return absolute file name for thumbnail FILE. +Depending on the value of `image-dired-thumbnail-storage', the +file name of the thumbnail will vary: +- For `use-image-dired-dir', make a SHA1-hash of the image file's + directory name and add that to make the thumbnail file name + unique. +- For `per-directory' storage, just add a subdirectory. +- For `standard' storage, produce the file name according to the + Thumbnail Managing Standard. Among other things, an MD5-hash + of the image file's directory name will be added to the + filename. +See also `image-dired-thumbnail-storage'." + (cond ((memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + (let ((thumbdir (cl-case image-dired-thumbnail-storage + (standard "thumbnails/normal") + (standard-large "thumbnails/large") + (standard-x-large "thumbnails/x-large") + (standard-xx-large "thumbnails/xx-large")))) + (expand-file-name + ;; MD5 is mandated by the Thumbnail Managing Standard. + (concat (md5 (concat "file://" (expand-file-name file))) ".png") + (expand-file-name thumbdir (xdg-cache-home))))) + ((eq 'use-image-dired-dir image-dired-thumbnail-storage) + (let* ((f (expand-file-name file)) + (hash + (md5 (file-name-as-directory (file-name-directory f))))) + (format "%s%s%s.thumb.%s" + (file-name-as-directory (expand-file-name (image-dired-dir))) + (file-name-base f) + (if hash (concat "_" hash) "") + (file-name-extension f)))) + ((eq 'per-directory image-dired-thumbnail-storage) + (let ((f (expand-file-name file))) + (format "%s.image-dired/%s.thumb.%s" + (file-name-directory f) + (file-name-base f) + (file-name-extension f)))))) + +(defun image-dired--check-executable-exists (executable) + (unless (executable-find (symbol-value executable)) + (error "Executable %S not found" executable))) + + +;;; Creating thumbnails + +(defun image-dired-thumb-size (dimension) + "Return thumb size depending on `image-dired-thumbnail-storage'. +DIMENSION should be either the symbol `width' or `height'." + (cond + ((eq 'standard image-dired-thumbnail-storage) 128) + ((eq 'standard-large image-dired-thumbnail-storage) 256) + ((eq 'standard-x-large image-dired-thumbnail-storage) 512) + ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) + (t (cl-ecase dimension + (width image-dired-thumb-width) + (height image-dired-thumb-height))))) + +(defvar image-dired--generate-thumbs-start nil + "Time when `display-thumbs' was called.") + +(defvar image-dired-queue nil + "List of items in the queue. +Each item has the form (ORIGINAL-FILE TARGET-FILE).") + +(defvar image-dired-queue-active-jobs 0 + "Number of active jobs in `image-dired-queue'.") + +(defvar image-dired-queue-active-limit (min 4 (max 2 (/ (num-processors) 2))) + "Maximum number of concurrent jobs permitted for generating images. +Increase at own risk. If you want to experiment with this, +consider setting `image-dired-debug' to a non-nil value to see +the time spent on generating thumbnails. Run `image-clear-cache' +and remove the cached thumbnail files between each trial run.") + +(defun image-dired-pngnq-thumb (spec) + "Quantize thumbnail described by format SPEC with pngnq(1)." + (let ((process + (apply #'start-process "image-dired-pngnq" nil + image-dired-cmd-pngnq-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-pngnq-options)))) + (setf (process-sentinel process) + (lambda (process status) + (if (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + ;; Pass off to pngcrush, or just rename the + ;; THUMB-nq8.png file back to THUMB.png + (if (and image-dired-cmd-pngcrush-program + (executable-find image-dired-cmd-pngcrush-program)) + (image-dired-pngcrush-thumb spec) + (let ((nq8 (cdr (assq ?q spec))) + (thumb (cdr (assq ?t spec)))) + (rename-file nq8 thumb t))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))))) + process)) + +(defun image-dired-pngcrush-thumb (spec) + "Optimize thumbnail described by format SPEC with pngcrush(1)." + ;; If pngnq wasn't run, then the THUMB-nq8.png file does not exist. + ;; pngcrush needs an infile and outfile, so we just copy THUMB to + ;; THUMB-nq8.png and use the latter as a temp file. + (when (not image-dired-cmd-pngnq-program) + (let ((temp (cdr (assq ?q spec))) + (thumb (cdr (assq ?t spec)))) + (copy-file thumb temp))) + (let ((process + (apply #'start-process "image-dired-pngcrush" nil + image-dired-cmd-pngcrush-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-pngcrush-options)))) + (setf (process-sentinel process) + (lambda (process status) + (unless (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))) + (when (memq (process-status process) '(exit signal)) + (let ((temp (cdr (assq ?q spec)))) + (delete-file temp))))) + process)) + +(defun image-dired-optipng-thumb (spec) + "Optimize thumbnail described by format SPEC with optipng(1)." + (let ((process + (apply #'start-process "image-dired-optipng" nil + image-dired-cmd-optipng-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-optipng-options)))) + (setf (process-sentinel process) + (lambda (process status) + (unless (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))))) + process)) + +(defun image-dired-create-thumb-1 (original-file thumbnail-file) + "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE." + (image-dired--check-executable-exists + 'image-dired-cmd-create-thumbnail-program) + (let* ((width (int-to-string (image-dired-thumb-size 'width))) + (height (int-to-string (image-dired-thumb-size 'height))) + (modif-time (format-time-string + "%s" (file-attribute-modification-time + (file-attributes original-file)))) + (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png" + thumbnail-file)) + (spec + (list + (cons ?w width) + (cons ?h height) + (cons ?m modif-time) + (cons ?f original-file) + (cons ?q thumbnail-nq8-file) + (cons ?t thumbnail-file))) + (thumbnail-dir (file-name-directory thumbnail-file)) + process) + (when (not (file-exists-p thumbnail-dir)) + (with-file-modes #o700 + (make-directory thumbnail-dir t)) + (message "Thumbnail directory created: %s" thumbnail-dir)) + + ;; Thumbnail file creation processes begin here and are marshaled + ;; in a queue by `image-dired-create-thumb'. + (setq process + (apply #'start-process "image-dired-create-thumbnail" nil + image-dired-cmd-create-thumbnail-program + (mapcar + (lambda (arg) (format-spec arg spec)) + (if (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + image-dired-cmd-create-standard-thumbnail-options + image-dired-cmd-create-thumbnail-options)))) + + (setf (process-sentinel process) + (lambda (process status) + ;; Trigger next in queue once a thumbnail has been created + (cl-decf image-dired-queue-active-jobs) + (image-dired-thumb-queue-run) + (when (= image-dired-queue-active-jobs 0) + (image-dired-debug-message + (format-time-string + "Generated thumbnails in %s.%3N seconds" + (time-subtract nil + image-dired--generate-thumbs-start)))) + (if (not (and (eq (process-status process) 'exit) + (zerop (process-exit-status process)))) + (message "Thumb could not be created for %s: %s" + (abbreviate-file-name original-file) + (string-replace "\n" "" status)) + (set-file-modes thumbnail-file #o600) + (clear-image-cache thumbnail-file) + ;; PNG thumbnail has been created since we are + ;; following the XDG thumbnail spec, so try to optimize + (when (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + (cond + ((and image-dired-cmd-pngnq-program + (executable-find image-dired-cmd-pngnq-program)) + (image-dired-pngnq-thumb spec)) + ((and image-dired-cmd-pngcrush-program + (executable-find image-dired-cmd-pngcrush-program)) + (image-dired-pngcrush-thumb spec)) + ((and image-dired-cmd-optipng-program + (executable-find image-dired-cmd-optipng-program)) + (image-dired-optipng-thumb spec))))))) + process)) + +(defun image-dired-thumb-queue-run () + "Run a queued job if one exists and not too many jobs are running. +Queued items live in `image-dired-queue'." + (while (and image-dired-queue + (< image-dired-queue-active-jobs + image-dired-queue-active-limit)) + (cl-incf image-dired-queue-active-jobs) + (apply #'image-dired-create-thumb-1 (pop image-dired-queue)))) + +(defun image-dired-create-thumb (original-file thumbnail-file) + "Add a job for generating ORIGINAL-FILE thumbnail to `image-dired-queue'. +The new file will be named THUMBNAIL-FILE." + (setq image-dired-queue + (nconc image-dired-queue + (list (list original-file thumbnail-file)))) + (run-at-time 0 nil #'image-dired-thumb-queue-run)) + +(defmacro image-dired--with-marked (&rest body) + "Eval BODY with point on each marked thumbnail. +If no marked file could be found, execute BODY on the current +thumbnail." + `(with-current-buffer image-dired-thumbnail-buffer + (let (found) + (save-mark-and-excursion + (goto-char (point-min)) + (while (not (eobp)) + (when (image-dired-thumb-file-marked-p) + (setq found t) + ,@body) + (forward-char))) + (unless found + ,@body)))) + +;;;###autoload +(defun image-dired-dired-toggle-marked-thumbs (&optional arg) + "Toggle thumbnails in front of file names in the Dired buffer. +If no marked file could be found, insert or hide thumbnails on the +current line. ARG, if non-nil, specifies the files to use instead +of the marked files. If ARG is an integer, use the next ARG (or +previous -ARG, if ARG<0) files." + (interactive "P") + (dired-map-over-marks + (let ((image-pos (dired-move-to-filename)) + (image-file (dired-get-filename nil t)) + thumb-file + overlay) + (when (and image-file + (string-match-p (image-file-name-regexp) image-file)) + (setq thumb-file (image-dired-get-thumbnail-image image-file)) + ;; If image is not already added, then add it. + (let ((thumb-ov (cl-loop for ov in (overlays-in (point) (1+ (point))) + if (overlay-get ov 'thumb-file) return ov))) + (if thumb-ov + (delete-overlay thumb-ov) + (put-image thumb-file image-pos) + (setq overlay + (cl-loop for ov in (overlays-in (point) (1+ (point))) + if (overlay-get ov 'put-image) return ov)) + (overlay-put overlay 'image-file image-file) + (overlay-put overlay 'thumb-file thumb-file))))) + arg ; Show or hide image on ARG next files. + 'show-progress) ; Update dired display after each image is updated. + (add-hook 'dired-after-readin-hook + 'image-dired-dired-after-readin-hook nil t)) + +(defun image-dired-dired-after-readin-hook () + "Relocate existing thumbnail overlays in Dired buffer after reverting. +Move them to their corresponding files if they still exist. +Otherwise, delete overlays." + (mapc (lambda (overlay) + (when (overlay-get overlay 'put-image) + (let* ((image-file (overlay-get overlay 'image-file)) + (image-pos (dired-goto-file image-file))) + (if image-pos + (move-overlay overlay image-pos image-pos) + (delete-overlay overlay))))) + (overlays-in (point-min) (point-max)))) + +(defun image-dired-next-line-and-display () + "Move to next Dired line and display thumbnail image." + (interactive) + (dired-next-line 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-previous-line-and-display () + "Move to previous Dired line and display thumbnail image." + (interactive) + (dired-previous-line 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-toggle-append-browsing () + "Toggle `image-dired-append-when-browsing'." + (interactive) + (setq image-dired-append-when-browsing + (not image-dired-append-when-browsing)) + (message "Append browsing %s" + (if image-dired-append-when-browsing + "on" + "off"))) + +(defun image-dired-mark-and-display-next () + "Mark current file in Dired and display next thumbnail image." + (interactive) + (dired-mark 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-toggle-dired-display-properties () + "Toggle `image-dired-dired-disp-props'." + (interactive) + (setq image-dired-dired-disp-props + (not image-dired-dired-disp-props)) + (message "Dired display properties %s" + (if image-dired-dired-disp-props + "on" + "off"))) + +(defvar image-dired-thumbnail-buffer "*image-dired*" + "Image-Dired's thumbnail buffer.") + +(defun image-dired-create-thumbnail-buffer () + "Create thumb buffer and set `image-dired-thumbnail-mode'." + (let ((buf (get-buffer-create image-dired-thumbnail-buffer))) + (with-current-buffer buf + (setq buffer-read-only t) + (if (not (eq major-mode 'image-dired-thumbnail-mode)) + (image-dired-thumbnail-mode))) + buf)) + +(defvar image-dired-display-image-buffer "*image-dired-display-image*" + "Where larger versions of the images are display.") + +(defvar image-dired-saved-window-configuration nil + "Saved window configuration.") + +;;;###autoload +(defun image-dired-dired-with-window-configuration (dir &optional arg) + "Open directory DIR and create a default window configuration. + +Convenience command that: + + - Opens Dired in folder DIR + - Splits windows in most useful (?) way + - Sets `truncate-lines' to t + +After the command has finished, you would typically mark some +image files in Dired and type +\\[image-dired-display-thumbs] (`image-dired-display-thumbs'). + +If called with prefix argument ARG, skip splitting of windows. + +The current window configuration is saved and can be restored by +calling `image-dired-restore-window-configuration'." + (interactive "DDirectory: \nP") + (let ((buf (image-dired-create-thumbnail-buffer)) + (buf2 (get-buffer-create image-dired-display-image-buffer))) + (setq image-dired-saved-window-configuration + (current-window-configuration)) + (dired dir) + (delete-other-windows) + (when (not arg) + (split-window-right) + (setq truncate-lines t) + (save-excursion + (other-window 1) + (pop-to-buffer-same-window buf) + (select-window (split-window-below)) + (pop-to-buffer-same-window buf2) + (other-window -2))))) + +(defun image-dired-restore-window-configuration () + "Restore window configuration. +Restore any changes to the window configuration made by calling +`image-dired-dired-with-window-configuration'." + (interactive nil image-dired-thumbnail-mode) + (if image-dired-saved-window-configuration + (set-window-configuration image-dired-saved-window-configuration) + (message "No saved window configuration"))) + +(defun image-dired--line-up-with-method () + "Line up thumbnails according to `image-dired-line-up-method'." + (cond ((eq 'dynamic image-dired-line-up-method) + (image-dired-line-up-dynamic)) + ((eq 'fixed image-dired-line-up-method) + (image-dired-line-up)) + ((eq 'interactive image-dired-line-up-method) + (image-dired-line-up-interactive)) + ((eq 'none image-dired-line-up-method) + nil) + (t + (image-dired-line-up-dynamic)))) + +;;;###autoload +(defun image-dired-display-thumbs (&optional arg append do-not-pop) + "Display thumbnails of all marked files, in `image-dired-thumbnail-buffer'. +If a thumbnail image does not exist for a file, it is created on the +fly. With prefix argument ARG, display only thumbnail for file at +point (this is useful if you have marked some files but want to show +another one). + +Recommended usage is to split the current frame horizontally so that +you have the Dired buffer in the left window and the +`image-dired-thumbnail-buffer' buffer in the right window. + +With optional argument APPEND, append thumbnail to thumbnail buffer +instead of erasing it first. + +Optional argument DO-NOT-POP controls if `pop-to-buffer' should be +used or not. If non-nil, use `display-buffer' instead of +`pop-to-buffer'. This is used from functions like +`image-dired-next-line-and-display' and +`image-dired-previous-line-and-display' where we do not want the +thumbnail buffer to be selected." + (interactive "P") + (setq image-dired--generate-thumbs-start (current-time)) + (let ((buf (image-dired-create-thumbnail-buffer)) + thumb-name files dired-buf) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (setq dired-buf (current-buffer)) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (if (not append) + (erase-buffer) + (goto-char (point-max))) + (dolist (curr-file files) + (setq thumb-name (image-dired-thumb-name curr-file)) + (when (not (file-exists-p thumb-name)) + (image-dired-create-thumb curr-file thumb-name)) + (image-dired-insert-thumbnail thumb-name curr-file dired-buf))) + (if do-not-pop + (display-buffer buf) + (pop-to-buffer buf)) + (image-dired--line-up-with-method)))) + +;;;###autoload +(defun image-dired-show-all-from-dir (dir) + "Make a thumbnail buffer for all images in DIR and display it. +Any file matching `image-file-name-regexp' is considered an image +file. + +If the number of image files in DIR exceeds +`image-dired-show-all-from-dir-max-files', ask for confirmation +before creating the thumbnail buffer. If that variable is nil, +never ask for confirmation." + (interactive "DImage-Dired: ") + (dired dir) + (dired-mark-files-regexp (image-file-name-regexp)) + (let ((files (dired-get-marked-files nil nil nil t))) + (cond ((and (null (cdr files))) + (message "No image files in directory")) + ((or (not image-dired-show-all-from-dir-max-files) + (<= (length (cdr files)) image-dired-show-all-from-dir-max-files) + (and (> (length (cdr files)) image-dired-show-all-from-dir-max-files) + (y-or-n-p + (format + "Directory contains more than %d image files. Proceed?" + image-dired-show-all-from-dir-max-files)))) + (image-dired-display-thumbs) + (pop-to-buffer image-dired-thumbnail-buffer) + (setq default-directory dir) + (image-dired-unmark-all-marks)) + (t (message "Image-Dired canceled"))))) + +;;;###autoload +(defalias 'image-dired 'image-dired-show-all-from-dir) + + +;;; Tags + +(defun image-dired-sane-db-file () + "Check if `image-dired-db-file' exists. +If not, try to create it (including any parent directories). +Signal error if there are problems creating it." + (or (file-exists-p image-dired-db-file) + (let (dir buf) + (unless (file-directory-p (setq dir (file-name-directory + image-dired-db-file))) + (with-file-modes #o700 + (make-directory dir t))) + (with-current-buffer (setq buf (create-file-buffer + image-dired-db-file)) + (with-file-modes #o600 + (write-file image-dired-db-file))) + (kill-buffer buf) + (file-exists-p image-dired-db-file)) + (error "Could not create %s" image-dired-db-file))) + +(defvar image-dired-tag-history nil "Variable holding the tag history.") + +(defun image-dired-write-tags (file-tags) + "Write file tags to database. +Write each file and tag in FILE-TAGS to the database. +FILE-TAGS is an alist in the following form: + ((FILE . TAG) ... )" + (image-dired-sane-db-file) + (let (end file tag) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (dolist (elt file-tags) + (setq file (car elt) + tag (cdr elt)) + (goto-char (point-min)) + (if (search-forward-regexp (format "^%s.*$" file) nil t) + (progn + (setq end (point)) + (beginning-of-line) + (when (not (search-forward (format ";%s" tag) end t)) + (end-of-line) + (insert (format ";%s" tag)))) + (goto-char (point-max)) + (insert (format "%s;%s\n" file tag)))) + (save-buffer)))) + +(defun image-dired-remove-tag (files tag) + "For all FILES, remove TAG from the image database." + (image-dired-sane-db-file) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (let (end) + (unless (listp files) + (if (stringp files) + (setq files (list files)) + (error "Files must be a string or a list of strings!"))) + (dolist (file files) + (goto-char (point-min)) + (when (search-forward-regexp (format "^%s;" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (search-forward-regexp + (format "\\(;%s\\)\\($\\|;\\)" tag) end t) + (delete-region (match-beginning 1) (match-end 1)) + ;; Check if file should still be in the database. If + ;; it has no tags or comments, it will be removed. + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (not (search-forward ";" end t)) + (kill-line 1)))))) + (save-buffer))) + +(defun image-dired-list-tags (file) + "Read all tags for image FILE from the image database." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end (tags "")) + (when (search-forward-regexp (format "^%s" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (if (search-forward ";" end t) + (if (search-forward "comment:" end t) + (if (search-forward ";" end t) + (setq tags (buffer-substring (point) end))) + (setq tags (buffer-substring (point) end))))) + (split-string tags ";")))) + +;;;###autoload +(defun image-dired-tag-files (arg) + "Tag marked file(s) in Dired. With prefix ARG, tag file at point." + (interactive "P") + (let ((tag (completing-read + "Tags to add (separate tags with a semicolon): " + image-dired-tag-history nil nil nil 'image-dired-tag-history)) + files) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (image-dired-write-tags + (mapcar + (lambda (x) + (cons x tag)) + files)))) + +(defun image-dired-tag-thumbnail () + "Tag current or marked thumbnails." + (interactive) + (let ((tag (completing-read + "Tags to add (separate tags with a semicolon): " + image-dired-tag-history nil nil nil 'image-dired-tag-history))) + (image-dired--with-marked + (image-dired-write-tags + (list (cons (image-dired-original-file-name) tag))) + (image-dired-update-property + 'tags (image-dired-list-tags (image-dired-original-file-name)))))) + +;;;###autoload +(defun image-dired-delete-tag (arg) + "Remove tag for selected file(s). +With prefix argument ARG, remove tag from file at point." + (interactive "P") + (let ((tag (completing-read "Tag to remove: " image-dired-tag-history + nil nil nil 'image-dired-tag-history)) + files) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (image-dired-remove-tag files tag))) + +(defun image-dired-tag-thumbnail-remove () + "Remove tag from current or marked thumbnails." + (interactive) + (let ((tag (completing-read "Tag to remove: " image-dired-tag-history + nil nil nil 'image-dired-tag-history))) + (image-dired--with-marked + (image-dired-remove-tag (image-dired-original-file-name) tag) + (image-dired-update-property + 'tags (image-dired-list-tags (image-dired-original-file-name)))))) + + +;;; Thumbnail mode (cont.) + +(defun image-dired-original-file-name () + "Get original file name for thumbnail or display image at point." + (get-text-property (point) 'original-file-name)) + +(defun image-dired-file-name-at-point () + "Get abbreviated file name for thumbnail or display image at point." + (let ((f (image-dired-original-file-name))) + (when f + (abbreviate-file-name f)))) + +(defun image-dired-associated-dired-buffer () + "Get associated Dired buffer at point." + (get-text-property (point) 'associated-dired-buffer)) + +(defun image-dired-get-buffer-window (buf) + "Return window where buffer BUF is." + (get-window-with-predicate + (lambda (window) + (equal (window-buffer window) buf)) + nil t)) + +(defun image-dired-track-original-file () + "Track the original file in the associated Dired buffer. +See documentation for `image-dired-toggle-movement-tracking'. +Interactive use only useful if `image-dired-track-movement' is nil." + (interactive) + (let* ((dired-buf (image-dired-associated-dired-buffer)) + (file-name (image-dired-original-file-name)) + (window (image-dired-get-buffer-window dired-buf))) + (and (buffer-live-p dired-buf) file-name + (with-current-buffer dired-buf + (if (not (dired-goto-file file-name)) + (message "Could not track file") + (if window (set-window-point window (point)))))))) + +(defun image-dired-toggle-movement-tracking () + "Turn on and off `image-dired-track-movement'. +Tracking of the movements between thumbnail and Dired buffer so that +they are \"mirrored\" in the dired buffer. When this is on, moving +around in the thumbnail or dired buffer will find the matching +position in the other buffer." + (interactive) + (setq image-dired-track-movement (not image-dired-track-movement)) + (message "Movement tracking %s" (if image-dired-track-movement "on" "off"))) + +(defun image-dired-track-thumbnail () + "Track current Dired file's thumb in `image-dired-thumbnail-buffer'. +This is almost the same as what `image-dired-track-original-file' does, +but the other way around." + (let ((file (dired-get-filename)) + prop-val found window) + (when (get-buffer image-dired-thumbnail-buffer) + (with-current-buffer image-dired-thumbnail-buffer + (goto-char (point-min)) + (while (and (not (eobp)) + (not found)) + (if (and (setq prop-val + (get-text-property (point) 'original-file-name)) + (string= prop-val file)) + (setq found t)) + (if (not found) + (forward-char 1))) + (when found + (if (setq window (image-dired-thumbnail-window)) + (set-window-point window (point))) + (image-dired-update-header-line)))))) + +(defun image-dired-dired-next-line (&optional arg) + "Call `dired-next-line', then track thumbnail. +This can safely replace `dired-next-line'. +With prefix argument, move ARG lines." + (interactive "P") + (dired-next-line (or arg 1)) + (if image-dired-track-movement + (image-dired-track-thumbnail))) + +(defun image-dired-dired-previous-line (&optional arg) + "Call `dired-previous-line', then track thumbnail. +This can safely replace `dired-previous-line'. +With prefix argument, move ARG lines." + (interactive "P") + (dired-previous-line (or arg 1)) + (if image-dired-track-movement + (image-dired-track-thumbnail))) + +(defun image-dired--display-thumb-properties-fun () + (let ((old-buf (current-buffer)) + (old-point (point))) + (lambda () + (when (and (equal (current-buffer) old-buf) + (= (point) old-point)) + (ignore-errors + (image-dired-update-header-line)))))) + +(defun image-dired-forward-image (&optional arg wrap-around) + "Move to next image and display properties. +Optional prefix ARG says how many images to move; the default is +one image. Negative means move backwards. +On reaching end or beginning of buffer, stop and show a message. + +If optional argument WRAP-AROUND is non-nil, wrap around: if +point is on the last image, move to the last one and vice versa." + (interactive "p") + (setq arg (or arg 1)) + (let (pos) + (dotimes (_ (abs arg)) + (if (and (not (if (> arg 0) (eobp) (bobp))) + (save-excursion + (forward-char (if (> arg 0) 1 -1)) + (while (and (not (if (> arg 0) (eobp) (bobp))) + (not (image-dired-image-at-point-p))) + (forward-char (if (> arg 0) 1 -1))) + (setq pos (point)) + (image-dired-image-at-point-p))) + (progn (goto-char pos) + (image-dired-update-header-line)) + (if wrap-around + (progn (goto-char (if (> arg 0) + (point-min) + ;; There are two spaces after the last image. + (- (point-max) 2))) + (image-dired-update-header-line)) + (message "At %s image" (if (> arg 0) "last" "first")) + (run-at-time 1 nil (image-dired--display-thumb-properties-fun)))))) + (when image-dired-track-movement + (image-dired-track-original-file))) + +(defun image-dired-backward-image (&optional arg) + "Move to previous image and display properties. +Optional prefix ARG says how many images to move; the default is +one image. Negative means move forward. +On reaching end or beginning of buffer, stop and show a message." + (interactive "p") + (image-dired-forward-image (- (or arg 1)))) + +(defun image-dired-next-line () + "Move to next line and display properties." + (interactive nil image-dired-thumbnail-mode) + (let ((goal-column (current-column))) + (forward-line 1) + (move-to-column goal-column)) + ;; If we end up in an empty spot, back up to the next thumbnail. + (if (not (image-dired-image-at-point-p)) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + + +(defun image-dired-previous-line () + "Move to previous line and display properties." + (interactive nil image-dired-thumbnail-mode) + (let ((goal-column (current-column))) + (forward-line -1) + (move-to-column goal-column)) + ;; If we end up in an empty spot, back up to the next + ;; thumbnail. This should only happen if the user deleted a + ;; thumbnail and did not refresh, so it is not very common. But we + ;; can handle it in a good manner, so why not? + (if (not (image-dired-image-at-point-p)) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-beginning-of-buffer () + "Move to the first image in the buffer and display properties." + (interactive nil image-dired-thumbnail-mode) + (goto-char (point-min)) + (while (and (not (image-at-point-p)) + (not (eobp))) + (forward-char 1)) + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-end-of-buffer () + "Move to the last image in the buffer and display properties." + (interactive nil image-dired-thumbnail-mode) + (goto-char (point-max)) + (while (and (not (image-at-point-p)) + (not (bobp))) + (forward-char -1)) + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-format-properties-string (buf file props comment) + "Format display properties. +BUF is the associated Dired buffer, FILE is the original image file +name, PROPS is a stringified list of tags and COMMENT is the image file's +comment." + (format-spec + image-dired-display-properties-format + (list + (cons ?b (or buf "")) + (cons ?f file) + (cons ?t (or props "")) + (cons ?c (or comment ""))))) + +(defun image-dired-update-header-line () + "Update image information in the header line." + (when (and (not (eobp)) + (memq major-mode '(image-dired-thumbnail-mode + image-dired-display-image-mode))) + (let ((file-name (file-name-nondirectory (image-dired-original-file-name))) + (dired-buf (buffer-name (image-dired-associated-dired-buffer))) + (props (mapconcat #'identity (get-text-property (point) 'tags) ", ")) + (comment (get-text-property (point) 'comment)) + (message-log-max nil)) + (if file-name + (setq header-line-format + (image-dired-format-properties-string + dired-buf + file-name + props + comment)))))) + +(defun image-dired-dired-file-marked-p (&optional marker) + "In Dired, return t if file on current line is marked. +If optional argument MARKER is non-nil, it is a character to look +for. The default is to look for `dired-marker-char'." + (setq marker (or marker dired-marker-char)) + (save-excursion + (beginning-of-line) + (and (looking-at dired-re-mark) + (= (aref (match-string 0) 0) marker)))) + +(defun image-dired-dired-file-flagged-p () + "In Dired, return t if file on current line is flagged for deletion." + (image-dired-dired-file-marked-p dired-del-marker)) + +(defmacro image-dired--with-thumbnail-buffer (&rest body) + (declare (indent defun) (debug t)) + `(if-let ((buf (get-buffer image-dired-thumbnail-buffer))) + (with-current-buffer buf + (if-let ((win (get-buffer-window buf))) + (with-selected-window win + ,@body) + ,@body)) + (user-error "No such buffer: %s" image-dired-thumbnail-buffer))) + +(defmacro image-dired--on-file-in-dired-buffer (&rest body) + "Run BODY with point on file at point in Dired buffer. +Should be called from commands in `image-dired-thumbnail-mode'." + (declare (indent defun) (debug t)) + `(let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (if (not (and dired-buf file-name)) + (message "No image, or image with correct properties, at point") + (with-current-buffer dired-buf + (when (dired-goto-file file-name) + ,@body + (image-dired-thumb-update-marks)))))) + +(defmacro image-dired--do-mark-command (maybe-next &rest body) + "Helper macro for the mark, unmark and flag commands. +Run BODY in Dired buffer. +If optional argument MAYBE-NEXT is non-nil, show next image +according to `image-dired-marking-shows-next'." + (declare (indent defun) (debug t)) + `(image-dired--with-thumbnail-buffer + (image-dired--on-file-in-dired-buffer + ,@body) + ,(when maybe-next + '(if image-dired-marking-shows-next + (image-dired-display-next-thumbnail-original) + (image-dired-next-line))))) + +(defun image-dired-mark-thumb-original-file () + "Mark original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-mark 1))) + +(defun image-dired-unmark-thumb-original-file () + "Unmark original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-unmark 1))) + +(defun image-dired-flag-thumb-original-file () + "Flag original image file for deletion in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-flag-file-deletion 1))) + +(defun image-dired-toggle-mark-thumb-original-file () + "Toggle mark on original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command nil + (if (image-dired-dired-file-marked-p) + (dired-unmark 1) + (dired-mark 1)))) + +(defun image-dired-unmark-all-marks () + "Remove all marks from all files in associated Dired buffer. +Also update the marks in the thumbnail buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command nil + (dired-unmark-all-marks)) + (image-dired--with-thumbnail-buffer + (image-dired-thumb-update-marks))) + +(defun image-dired-jump-original-dired-buffer () + "Jump to the Dired buffer associated with the current image file. +You probably want to use this together with +`image-dired-track-original-file'." + (interactive nil image-dired-thumbnail-mode) + (let ((buf (image-dired-associated-dired-buffer)) + window frame) + (setq window (image-dired-get-buffer-window buf)) + (if window + (progn + (if (not (equal (selected-frame) (setq frame (window-frame window)))) + (select-frame-set-input-focus frame)) + (select-window window)) + (message "Associated dired buffer not visible")))) + +;;;###autoload +(defun image-dired-jump-thumbnail-buffer () + "Jump to thumbnail buffer." + (interactive) + (let ((window (image-dired-thumbnail-window)) + frame) + (if window + (progn + (if (not (equal (selected-frame) (setq frame (window-frame window)))) + (select-frame-set-input-focus frame)) + (select-window window)) + (message "Thumbnail buffer not visible")))) + +(defvar image-dired-thumbnail-mode-line-up-map + (let ((map (make-sparse-keymap))) + ;; map it to "g" so that the user can press it more quickly + (define-key map "g" #'image-dired-line-up-dynamic) + ;; "f" for "fixed" number of thumbs per row + (define-key map "f" #'image-dired-line-up) + ;; "i" for "interactive" + (define-key map "i" #'image-dired-line-up-interactive) + map) + "Keymap for line-up commands in `image-dired-thumbnail-mode'.") + +(defvar image-dired-thumbnail-mode-tag-map + (let ((map (make-sparse-keymap))) + ;; map it to "t" so that the user can press it more quickly + (define-key map "t" #'image-dired-tag-thumbnail) + ;; "r" for "remove" + (define-key map "r" #'image-dired-tag-thumbnail-remove) + map) + "Keymap for tag commands in `image-dired-thumbnail-mode'.") + +(defvar image-dired-thumbnail-mode-map + (let ((map (make-sparse-keymap))) + (define-key map [right] #'image-dired-forward-image) + (define-key map [left] #'image-dired-backward-image) + (define-key map [up] #'image-dired-previous-line) + (define-key map [down] #'image-dired-next-line) + (define-key map "\C-f" #'image-dired-forward-image) + (define-key map "\C-b" #'image-dired-backward-image) + (define-key map "\C-p" #'image-dired-previous-line) + (define-key map "\C-n" #'image-dired-next-line) + + (define-key map "<" #'image-dired-beginning-of-buffer) + (define-key map ">" #'image-dired-end-of-buffer) + (define-key map (kbd "M-<") #'image-dired-beginning-of-buffer) + (define-key map (kbd "M->") #'image-dired-end-of-buffer) + + (define-key map "d" #'image-dired-flag-thumb-original-file) + (define-key map [delete] #'image-dired-flag-thumb-original-file) + (define-key map "m" #'image-dired-mark-thumb-original-file) + (define-key map "u" #'image-dired-unmark-thumb-original-file) + (define-key map "U" #'image-dired-unmark-all-marks) + (define-key map "." #'image-dired-track-original-file) + (define-key map [tab] #'image-dired-jump-original-dired-buffer) + + ;; add line-up map + (define-key map "g" image-dired-thumbnail-mode-line-up-map) + ;; add tag map + (define-key map "t" image-dired-thumbnail-mode-tag-map) + + (define-key map "\C-m" #'image-dired-display-thumbnail-original-image) + (define-key map [C-return] #'image-dired-thumbnail-display-external) + + (define-key map "L" #'image-dired-rotate-original-left) + (define-key map "R" #'image-dired-rotate-original-right) + + (define-key map "D" #'image-dired-thumbnail-set-image-description) + (define-key map "S" #'image-dired-slideshow-start) + (define-key map "\C-d" #'image-dired-delete-char) + (define-key map " " #'image-dired-display-next-thumbnail-original) + (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) + (define-key map "c" #'image-dired-comment-thumbnail) + + ;; Mouse + (define-key map [mouse-2] #'image-dired-mouse-display-image) + (define-key map [mouse-1] #'image-dired-mouse-select-thumbnail) + (define-key map [mouse-3] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-1] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-2] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-3] #'image-dired-mouse-select-thumbnail) + ;; Seems I must first set C-down-mouse-1 to undefined, or else it + ;; will trigger the buffer menu. If I try to instead bind + ;; C-down-mouse-1 to `image-dired-mouse-toggle-mark', I get a message + ;; about C-mouse-1 not being defined afterwards. Annoying, but I + ;; probably do not completely understand mouse events. + (define-key map [C-down-mouse-1] #'undefined) + (define-key map [C-mouse-1] #'image-dired-mouse-toggle-mark) + map) + "Keymap for `image-dired-thumbnail-mode'.") + +(easy-menu-define image-dired-thumbnail-mode-menu image-dired-thumbnail-mode-map + "Menu for `image-dired-thumbnail-mode'." + '("Image-Dired" + ["Display image" image-dired-display-thumbnail-original-image] + ["Display in external viewer" image-dired-thumbnail-display-external] + ["Jump to Dired buffer" image-dired-jump-original-dired-buffer] + "---" + ["Mark image" image-dired-mark-thumb-original-file] + ["Unmark image" image-dired-unmark-thumb-original-file] + ["Unmark all images" image-dired-unmark-all-marks] + ["Flag for deletion" image-dired-flag-thumb-original-file] + ["Delete marked images" image-dired-delete-marked] + "---" + ["Rotate original right" image-dired-rotate-original-right] + ["Rotate original left" image-dired-rotate-original-left] + "---" + ["Comment thumbnail" image-dired-comment-thumbnail] + ["Tag current or marked thumbnails" image-dired-tag-thumbnail] + ["Remove tag from current or marked thumbnails" + image-dired-tag-thumbnail-remove] + ["Start slideshow" image-dired-slideshow-start] + "---" + ("View Options" + ["Toggle movement tracking" image-dired-toggle-movement-tracking + :style toggle + :selected image-dired-track-movement] + "---" + ["Line up thumbnails" image-dired-line-up] + ["Dynamic line up" image-dired-line-up-dynamic] + ["Refresh thumb" image-dired-refresh-thumb]) + ["Quit" quit-window])) + +(defvar image-dired-display-image-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "S" #'image-dired-slideshow-start) + (define-key map (kbd "SPC") #'image-dired-display-next-thumbnail-original) + (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) + (define-key map "n" #'image-dired-display-next-thumbnail-original) + (define-key map "p" #'image-dired-display-previous-thumbnail-original) + (define-key map "m" #'image-dired-mark-thumb-original-file) + (define-key map "d" #'image-dired-flag-thumb-original-file) + (define-key map "u" #'image-dired-unmark-thumb-original-file) + (define-key map "U" #'image-dired-unmark-all-marks) + ;; Disable keybindings from `image-mode-map' that doesn't make sense here. + (define-key map "o" nil) ; image-save + map) + "Keymap for `image-dired-display-image-mode'.") + +(define-derived-mode image-dired-thumbnail-mode + special-mode "image-dired-thumbnail" + "Browse and manipulate thumbnail images using Dired. +Use `image-dired-minor-mode' to get a nice setup." + :interactive nil + (buffer-disable-undo) + (add-hook 'file-name-at-point-functions 'image-dired-file-name-at-point nil t) + (setq-local window-resize-pixelwise t) + (setq-local bookmark-make-record-function #'image-dired-bookmark-make-record) + ;; Use approximately as much vertical spacing as horizontal. + (setq-local line-spacing (frame-char-width))) + + +;;; Display image mode + +(define-derived-mode image-dired-display-image-mode + image-mode "image-dired-image-display" + "Mode for displaying and manipulating original image. +Resized or in full-size." + :interactive nil + (add-hook 'file-name-at-point-functions #'image-dired-file-name-at-point nil t)) + +(defvar image-dired-minor-mode-map + (let ((map (make-sparse-keymap))) + ;; (set-keymap-parent map dired-mode-map) + ;; Hijack previous and next line movement. Let C-p and C-b be + ;; though... + (define-key map "p" #'image-dired-dired-previous-line) + (define-key map "n" #'image-dired-dired-next-line) + (define-key map [up] #'image-dired-dired-previous-line) + (define-key map [down] #'image-dired-dired-next-line) + + (define-key map (kbd "C-S-n") #'image-dired-next-line-and-display) + (define-key map (kbd "C-S-p") #'image-dired-previous-line-and-display) + (define-key map (kbd "C-S-m") #'image-dired-mark-and-display-next) + + (define-key map "\C-td" #'image-dired-display-thumbs) + (define-key map [tab] #'image-dired-jump-thumbnail-buffer) + (define-key map "\C-ti" #'image-dired-dired-display-image) + (define-key map "\C-tx" #'image-dired-dired-display-external) + (define-key map "\C-ta" #'image-dired-display-thumbs-append) + (define-key map "\C-t." #'image-dired-display-thumb) + (define-key map "\C-tc" #'image-dired-dired-comment-files) + (define-key map "\C-tf" #'image-dired-mark-tagged-files) + map) + "Keymap for `image-dired-minor-mode'.") + +(easy-menu-define image-dired-minor-mode-menu image-dired-minor-mode-map + "Menu for `image-dired-minor-mode'." + '("Image-dired" + ["Display thumb for next file" image-dired-next-line-and-display] + ["Display thumb for previous file" image-dired-previous-line-and-display] + ["Mark and display next" image-dired-mark-and-display-next] + "---" + ["Create thumbnails for marked files" image-dired-create-thumbs] + "---" + ["Display thumbnails append" image-dired-display-thumbs-append] + ["Display this thumbnail" image-dired-display-thumb] + ["Display image" image-dired-dired-display-image] + ["Display in external viewer" image-dired-dired-display-external] + "---" + ["Toggle display properties" image-dired-toggle-dired-display-properties + :style toggle + :selected image-dired-dired-disp-props] + ["Toggle append browsing" image-dired-toggle-append-browsing + :style toggle + :selected image-dired-append-when-browsing] + ["Toggle movement tracking" image-dired-toggle-movement-tracking + :style toggle + :selected image-dired-track-movement] + "---" + ["Jump to thumbnail buffer" image-dired-jump-thumbnail-buffer] + ["Mark tagged files" image-dired-mark-tagged-files] + ["Comment files" image-dired-dired-comment-files] + ["Copy with EXIF file name" image-dired-copy-with-exif-file-name])) + +;;;###autoload +(define-minor-mode image-dired-minor-mode + "Setup easy-to-use keybindings for the commands to be used in Dired mode. +Note that n, p and and will be hijacked and bound to +`image-dired-dired-next-line' and `image-dired-dired-previous-line'." + :keymap image-dired-minor-mode-map) + +(declare-function clear-image-cache "image.c" (&optional filter)) + +(defun image-dired-create-thumbs (&optional arg) + "Create thumbnail images for all marked files in Dired. +With prefix argument ARG, create thumbnails even if they already exist +\(i.e. use this to refresh your thumbnails)." + (interactive "P") + (let (thumb-name) + (dolist (curr-file (dired-get-marked-files)) + (setq thumb-name (image-dired-thumb-name curr-file)) + ;; If the user overrides the exist check, we must clear the + ;; image cache so that if the user wants to display the + ;; thumbnail, it is not fetched from cache. + (when arg + (clear-image-cache (expand-file-name thumb-name))) + (when (or (not (file-exists-p thumb-name)) + arg) + (image-dired-create-thumb curr-file thumb-name))))) + + +;;; Slideshow + +(defcustom image-dired-slideshow-delay 5.0 + "Seconds to wait before showing the next image in a slideshow. +This is used by `image-dired-slideshow-start'." + :type 'float + :version "29.1") + +(define-obsolete-variable-alias 'image-dired-slideshow-timer + 'image-dired--slideshow-timer "29.1") +(defvar image-dired--slideshow-timer nil + "Slideshow timer.") + +(defvar image-dired--slideshow-initial nil) + +(defun image-dired-slideshow-step () + "Step to next image in a slideshow." + (if-let ((buf (get-buffer image-dired-thumbnail-buffer))) + (with-current-buffer buf + (image-dired-display-next-thumbnail-original)) + (image-dired-slideshow-stop))) + +(defun image-dired-slideshow-start (&optional arg) + "Start a slideshow, waiting `image-dired-slideshow-delay' between images. + +With prefix argument ARG, wait that many seconds before going to +the next image. + +With a negative prefix argument, prompt user for the delay." + (interactive "P" image-dired-thumbnail-mode image-dired-display-image-mode) + (let ((delay (if (not arg) + image-dired-slideshow-delay + (if (> arg 0) + arg + (string-to-number + (let ((delay (number-to-string image-dired-slideshow-delay))) + (read-string + (format-prompt "Delay, in seconds. Decimals are accepted" delay)) + delay)))))) + (setq image-dired--slideshow-timer + (run-with-timer + 0 delay + 'image-dired-slideshow-step)) + (add-hook 'post-command-hook 'image-dired-slideshow-stop) + (setq image-dired--slideshow-initial t) + (message "Running slideshow; use any command to stop"))) + +(defun image-dired-slideshow-stop () + "Cancel slideshow." + ;; Make sure we don't immediately stop after + ;; `image-dired-slideshow-start'. + (unless image-dired--slideshow-initial + (remove-hook 'post-command-hook 'image-dired-slideshow-stop) + (cancel-timer image-dired--slideshow-timer)) + (setq image-dired--slideshow-initial nil)) + + +;;; Thumbnail mode (cont. 3) + +(defun image-dired-delete-char () + "Remove current thumbnail from thumbnail buffer and line up." + (interactive nil image-dired-thumbnail-mode) + (let ((inhibit-read-only t)) + (delete-char 1) + (when (= (following-char) ?\s) + (delete-char 1)))) + +;;;###autoload +(defun image-dired-display-thumbs-append () + "Append thumbnails to `image-dired-thumbnail-buffer'." + (interactive) + (image-dired-display-thumbs nil t t)) + +;;;###autoload +(defun image-dired-display-thumb () + "Shorthand for `image-dired-display-thumbs' with prefix argument." + (interactive) + (image-dired-display-thumbs t nil t)) + +(defun image-dired-line-up () + "Line up thumbnails according to `image-dired-thumbs-per-row'. +See also `image-dired-line-up-dynamic'." + (interactive) + (let ((inhibit-read-only t)) + (goto-char (point-min)) + (while (and (not (image-dired-image-at-point-p)) + (not (eobp))) + (delete-char 1)) + (while (not (eobp)) + (forward-char) + (while (and (not (image-dired-image-at-point-p)) + (not (eobp))) + (delete-char 1))) + (goto-char (point-min)) + (let ((seen 0) + (thumb-prev-pos 0) + (thumb-width-chars + (ceiling (/ (+ (* 2 image-dired-thumb-relief) + (* 2 image-dired-thumb-margin) + (image-dired-thumb-size 'width)) + (float (frame-char-width)))))) + (while (not (eobp)) + (forward-char) + (if (= image-dired-thumbs-per-row 1) + (insert "\n") + (cl-incf thumb-prev-pos thumb-width-chars) + (insert (propertize " " 'display `(space :align-to ,thumb-prev-pos))) + (cl-incf seen) + (when (and (= seen (- image-dired-thumbs-per-row 1)) + (not (eobp))) + (forward-char) + (insert "\n") + (setq seen 0) + (setq thumb-prev-pos 0))))) + (goto-char (point-min)))) + +(defun image-dired-line-up-dynamic () + "Line up thumbnails images dynamically. +Calculate how many thumbnails fit." + (interactive) + (let* ((char-width (frame-char-width)) + (width (image-dired-window-width-pixels (image-dired-thumbnail-window))) + (image-dired-thumbs-per-row + (/ width + (+ (* 2 image-dired-thumb-relief) + (* 2 image-dired-thumb-margin) + (image-dired-thumb-size 'width) + char-width)))) + (image-dired-line-up))) + +(defun image-dired-line-up-interactive () + "Line up thumbnails interactively. +Ask user how many thumbnails should be displayed per row." + (interactive) + (let ((image-dired-thumbs-per-row + (string-to-number (read-string "How many thumbs per row: ")))) + (if (not (> image-dired-thumbs-per-row 0)) + (message "Number must be greater than 0") + (image-dired-line-up)))) + +(defun image-dired-thumbnail-display-external () + "Display original image for thumbnail at point using external viewer." + (interactive) + (let ((file (image-dired-original-file-name))) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (if (not file) + (message "No original file name found") + (start-process "image-dired-thumb-external" nil + image-dired-external-viewer file))))) + +;;;###autoload +(defun image-dired-dired-display-external () + "Display file at point using an external viewer." + (interactive) + (let ((file (dired-get-filename))) + (start-process "image-dired-external" nil + image-dired-external-viewer file))) + +(defun image-dired-window-width-pixels (window) + "Calculate WINDOW width in pixels." + (* (window-width window) (frame-char-width))) + +(defun image-dired-display-window () + "Return window where `image-dired-display-image-buffer' is visible." + (get-window-with-predicate + (lambda (window) + (equal (buffer-name (window-buffer window)) image-dired-display-image-buffer)) + nil t)) + +(defun image-dired-thumbnail-window () + "Return window where `image-dired-thumbnail-buffer' is visible." + (get-window-with-predicate + (lambda (window) + (equal (buffer-name (window-buffer window)) image-dired-thumbnail-buffer)) + nil t)) + +(defun image-dired-associated-dired-buffer-window () + "Return window where associated Dired buffer is visible." + (let (buf) + (if (image-dired-image-at-point-p) + (progn + (setq buf (image-dired-associated-dired-buffer)) + (get-window-with-predicate + (lambda (window) + (equal (window-buffer window) buf)))) + (error "No thumbnail image at point")))) + +(defun image-dired-display-image (file &optional _ignored) + "Display image FILE in image buffer. +Use this when you want to display the image, in a new window. +The window will use `image-dired-display-image-mode' which is +based on `image-mode'." + (declare (advertised-calling-convention (file) "29.1")) + (setq file (expand-file-name file)) + (when (not (file-exists-p file)) + (error "No such file: %s" file)) + (let ((buf (get-buffer image-dired-display-image-buffer)) + (cur-win (selected-window))) + (when buf + (kill-buffer buf)) + (when-let ((buf (find-file-noselect file nil t))) + (pop-to-buffer buf) + (rename-buffer image-dired-display-image-buffer) + (image-dired-display-image-mode) + (select-window cur-win)))) + +(defun image-dired-display-thumbnail-original-image (&optional arg) + "Display current thumbnail's original image in display buffer. +See documentation for `image-dired-display-image' for more information. +With prefix argument ARG, display image in its original size." + (interactive "P") + (let ((file (image-dired-original-file-name))) + (if (not (string-equal major-mode "image-dired-thumbnail-mode")) + (message "Not in image-dired-thumbnail-mode") + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (if (not file) + (message "No original file name found") + (image-dired-display-image file arg)))))) + + +;;;###autoload +(defun image-dired-dired-display-image (&optional arg) + "Display current image file. +See documentation for `image-dired-display-image' for more information. +With prefix argument ARG, display image in its original size." + (interactive "P") + (image-dired-display-image (dired-get-filename) arg)) + +(defun image-dired-image-at-point-p () + "Return non-nil if there is an `image-dired' thumbnail at point." + (get-text-property (point) 'image-dired-thumbnail)) + +(defun image-dired-refresh-thumb () + "Force creation of new image for current thumbnail." + (interactive nil image-dired-thumbnail-mode) + (let* ((file (image-dired-original-file-name)) + (thumb (expand-file-name (image-dired-thumb-name file)))) + (clear-image-cache (expand-file-name thumb)) + (image-dired-create-thumb file thumb))) + +(defun image-dired-rotate-original (degrees) + "Rotate original image DEGREES degrees." + (image-dired--check-executable-exists + 'image-dired-cmd-rotate-original-program) + (if (not (image-dired-image-at-point-p)) + (message "No image at point") + (let* ((file (image-dired-original-file-name)) + (spec + (list + (cons ?d degrees) + (cons ?o (expand-file-name file)) + (cons ?t image-dired-temp-rotate-image-file)))) + (unless (eq 'jpeg (image-type file)) + (user-error "Only JPEG images can be rotated")) + (if (not (= 0 (apply #'call-process image-dired-cmd-rotate-original-program + nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-rotate-original-options)))) + (error "Could not rotate image") + (image-dired-display-image image-dired-temp-rotate-image-file) + (if (or (and image-dired-rotate-original-ask-before-overwrite + (y-or-n-p + "Rotate to temp file OK. Overwrite original image? ")) + (not image-dired-rotate-original-ask-before-overwrite)) + (progn + (copy-file image-dired-temp-rotate-image-file file t) + (image-dired-refresh-thumb)) + (image-dired-display-image file)))))) + +(defun image-dired-rotate-original-left () + "Rotate original image left (counter clockwise) 90 degrees. +The result of the rotation is displayed in the image display area +and a confirmation is needed before the original image files is +overwritten. This confirmation can be turned off using +`image-dired-rotate-original-ask-before-overwrite'." + (interactive) + (image-dired-rotate-original "270")) + +(defun image-dired-rotate-original-right () + "Rotate original image right (clockwise) 90 degrees. +The result of the rotation is displayed in the image display area +and a confirmation is needed before the original image files is +overwritten. This confirmation can be turned off using +`image-dired-rotate-original-ask-before-overwrite'." + (interactive) + (image-dired-rotate-original "90")) + + +;;; EXIF support + +(defun image-dired-get-exif-file-name (file) + "Use the image's EXIF information to return a unique file name. +The file name should be unique as long as you do not take more than +one picture per second. The original file name is suffixed at the end +for traceability. The format of the returned file name is +YYYY_MM_DD_HH_MM_DD_ORIG_FILE_NAME.jpg. Used from +`image-dired-copy-with-exif-file-name'." + (let (data no-exif-data-found) + (if (not (eq 'jpeg (image-type (expand-file-name file)))) + (setq no-exif-data-found t + data (format-time-string + "%Y:%m:%d %H:%M:%S" + (file-attribute-modification-time + (file-attributes (expand-file-name file))))) + (setq data (exif-field 'date-time (exif-parse-file + (expand-file-name file))))) + (while (string-match "[ :]" data) + (setq data (replace-match "_" nil nil data))) + (format "%s%s%s" data + (if no-exif-data-found + "_noexif_" + "_") + (file-name-nondirectory file)))) + +(defun image-dired-thumbnail-set-image-description () + "Set the ImageDescription EXIF tag for the original image. +If the image already has a value for this tag, it is used as the +default value at the prompt." + (interactive) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (let* ((file (image-dired-original-file-name)) + (old-value (or (exif-field 'description (exif-parse-file file)) ""))) + (if (eq 0 + (image-dired-set-exif-data file "ImageDescription" + (read-string "Value of ImageDescription: " + old-value))) + (message "Successfully wrote ImageDescription tag") + (error "Could not write ImageDescription tag"))))) + +(defun image-dired-set-exif-data (file tag-name tag-value) + "In FILE, set EXIF tag TAG-NAME to value TAG-VALUE." + (image-dired--check-executable-exists + 'image-dired-cmd-write-exif-data-program) + (let ((spec + (list + (cons ?f (expand-file-name file)) + (cons ?t tag-name) + (cons ?v tag-value)))) + (apply #'call-process image-dired-cmd-write-exif-data-program nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-write-exif-data-options)))) + +(defun image-dired-copy-with-exif-file-name () + "Copy file with unique name to main image directory. +Copy current or all marked files in Dired to a new file in your +main image directory, using a file name generated by +`image-dired-get-exif-file-name'. A typical usage for this if when +copying images from a digital camera into the image directory. + + Typically, you would open up the folder with the incoming +digital images, mark the files to be copied, and execute this +function. The result is a couple of new files in +`image-dired-main-image-directory' called +2005_05_08_12_52_00_dscn0319.jpg, +2005_05_08_14_27_45_dscn0320.jpg etc." + (interactive) + (let (new-name + (files (dired-get-marked-files))) + (mapc + (lambda (curr-file) + (setq new-name + (format "%s/%s" + (file-name-as-directory + (expand-file-name image-dired-main-image-directory)) + (image-dired-get-exif-file-name curr-file))) + (message "Copying %s to %s" curr-file new-name) + (copy-file curr-file new-name)) + files))) + +;;; Thumbnail mode (cont.) + +(defun image-dired-display-next-thumbnail-original (&optional arg) + "Move to the next image in the thumbnail buffer and display it. +With prefix ARG, move that many thumbnails." + (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--with-thumbnail-buffer + (image-dired-forward-image arg t) + (image-dired-display-thumbnail-original-image))) + +(defun image-dired-display-previous-thumbnail-original (arg) + "Move to the previous image in the thumbnail buffer and display it. +With prefix ARG, move that many thumbnails." + (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired-display-next-thumbnail-original (- arg))) + + +;;; Image Comments + +(defun image-dired-write-comments (file-comments) + "Write file comments to database. +Write file comments to one or more files. +FILE-COMMENTS is an alist on the following form: + ((FILE . COMMENT) ... )" + (image-dired-sane-db-file) + (let (end comment-beg-pos comment-end-pos file comment) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (dolist (elt file-comments) + (setq file (car elt) + comment (cdr elt)) + (goto-char (point-min)) + (if (search-forward-regexp (format "^%s.*$" file) nil t) + (progn + (setq end (point)) + (beginning-of-line) + ;; Delete old comment, if any + (when (search-forward ";comment:" end t) + (setq comment-beg-pos (match-beginning 0)) + ;; Any tags after the comment? + (if (search-forward ";" end t) + (setq comment-end-pos (- (point) 1)) + (setq comment-end-pos end)) + ;; Delete comment tag and comment + (delete-region comment-beg-pos comment-end-pos)) + ;; Insert new comment + (beginning-of-line) + (unless (search-forward ";" end t) + (end-of-line) + (insert ";")) + (insert (format "comment:%s;" comment))) + ;; File does not exist in database - add it. + (goto-char (point-max)) + (insert (format "%s;comment:%s\n" file comment)))) + (save-buffer)))) + +(defun image-dired-update-property (prop value) + "Update text property PROP with value VALUE at point." + (let ((inhibit-read-only t)) + (put-text-property + (point) (1+ (point)) + prop + value))) + +;;;###autoload +(defun image-dired-dired-comment-files () + "Add comment to current or marked files in Dired." + (interactive) + (let ((comment (image-dired-read-comment))) + (image-dired-write-comments + (mapcar + (lambda (curr-file) + (cons curr-file comment)) + (dired-get-marked-files))))) + +(defun image-dired-comment-thumbnail () + "Add comment to current thumbnail in thumbnail buffer." + (interactive) + (let* ((file (image-dired-original-file-name)) + (comment (image-dired-read-comment file))) + (image-dired-write-comments (list (cons file comment))) + (image-dired-update-property 'comment comment)) + (image-dired-update-header-line)) + +(defun image-dired-read-comment (&optional file) + "Read comment for an image. +Optionally use old comment from FILE as initial value." + (let ((comment + (read-string + "Comment: " + (if file (image-dired-get-comment file))))) + comment)) + +(defun image-dired-get-comment (file) + "Get comment for file FILE." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end comment-beg-pos comment-end-pos comment) + (when (search-forward-regexp (format "^%s" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (search-forward ";comment:" end t) + (setq comment-beg-pos (point)) + (if (search-forward ";" end t) + (setq comment-end-pos (- (point) 1)) + (setq comment-end-pos end)) + (setq comment (buffer-substring + comment-beg-pos comment-end-pos)))) + comment))) + +;;;###autoload +(defun image-dired-mark-tagged-files (regexp) + "Use REGEXP to mark files with matching tag. +A `tag' is a keyword, a piece of meta data, associated with an +image file and stored in image-dired's database file. This command +lets you input a regexp and this will be matched against all tags +on all image files in the database file. The files that have a +matching tag will be marked in the Dired buffer." + (interactive "sMark tagged files (regexp): ") + (image-dired-sane-db-file) + (let ((hits 0) + files) + (image-dired--with-db-file + ;; Collect matches + (while (search-forward-regexp "\\(^[^;\n]+\\);\\(.*\\)" nil t) + (let ((file (match-string 1)) + (tags (split-string (match-string 2) ";"))) + (when (seq-find (lambda (tag) + (string-match-p regexp tag)) + tags) + (push file files))))) + ;; Mark files + (dolist (curr-file files) + ;; I tried using `dired-mark-files-regexp' but it was waaaay to + ;; slow. Don't bother about hits found in other directories + ;; than the current one. + (when (string= (file-name-as-directory + (expand-file-name default-directory)) + (file-name-as-directory + (file-name-directory curr-file))) + (setq curr-file (file-name-nondirectory curr-file)) + (goto-char (point-min)) + (when (search-forward-regexp (format "\\s %s$" curr-file) nil t) + (setq hits (+ hits 1)) + (dired-mark 1)))) + (message "%d files with matching tag marked" hits))) + + + +;;; Mouse support + +(defun image-dired-mouse-display-image (event) + "Use mouse EVENT, call `image-dired-display-image' to display image. +Track this in associated Dired buffer if `image-dired-track-movement' is +non-nil." + (interactive "e") + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (unless (image-at-point-p) + (image-dired-backward-image)) + (let ((file (image-dired-original-file-name))) + (when file + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-display-image file)))) + +(defun image-dired-mouse-select-thumbnail (event) + "Use mouse EVENT to select thumbnail image. +Track this in associated Dired buffer if `image-dired-track-movement' is +non-nil." + (interactive "e") + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (unless (image-at-point-p) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + + + +;;; Dired marks and tags + +(defun image-dired-thumb-file-marked-p (&optional flagged) + "Check if file is marked in associated Dired buffer. +If optional argument FLAGGED is non-nil, check if file is flagged +for deletion instead." + (let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (when (and dired-buf file-name) + (with-current-buffer dired-buf + (save-excursion + (when (dired-goto-file file-name) + (if flagged + (image-dired-dired-file-flagged-p) + (image-dired-dired-file-marked-p)))))))) + +(defun image-dired-thumb-file-flagged-p () + "Check if file is flagged for deletion in associated Dired buffer." + (image-dired-thumb-file-marked-p t)) + +(defun image-dired-delete-marked () + "Delete current or marked thumbnails and associated images." + (interactive) + (image-dired--with-marked + (image-dired-delete-char) + (unless (bobp) + (backward-char))) + (image-dired--line-up-with-method) + (with-current-buffer (image-dired-associated-dired-buffer) + (dired-do-delete))) + +(defun image-dired-thumb-update-marks () + "Update the marks in the thumbnail buffer." + (when image-dired-thumb-visible-marks + (with-current-buffer image-dired-thumbnail-buffer + (save-mark-and-excursion + (goto-char (point-min)) + (let ((inhibit-read-only t)) + (while (not (eobp)) + (with-silent-modifications + (cond ((image-dired-thumb-file-marked-p) + (add-face-text-property (point) (1+ (point)) + 'image-dired-thumb-mark)) + ((image-dired-thumb-file-flagged-p) + (add-face-text-property (point) (1+ (point)) + 'image-dired-thumb-flagged)) + (t (remove-text-properties (point) (1+ (point)) + '(face image-dired-thumb-mark))))) + (forward-char))))))) + +(defun image-dired-mouse-toggle-mark-1 () + "Toggle Dired mark for current thumbnail. +Track this in associated Dired buffer if +`image-dired-track-movement' is non-nil." + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-toggle-mark-thumb-original-file)) + +(defun image-dired-mouse-toggle-mark (event) + "Use mouse EVENT to toggle Dired mark for thumbnail. +Toggle marks of all thumbnails in region, if it's active. +Track this in associated Dired buffer if +`image-dired-track-movement' is non-nil." + (interactive "e") + (if (use-region-p) + (let ((end (region-end))) + (save-excursion + (goto-char (region-beginning)) + (while (<= (point) end) + (when (image-dired-image-at-point-p) + (image-dired-mouse-toggle-mark-1)) + (forward-char)))) + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (image-dired-mouse-toggle-mark-1)) + (image-dired-thumb-update-marks)) + +(defun image-dired-dired-display-properties () + "Display properties for Dired file in the echo area." + (interactive) + (let* ((file (dired-get-filename)) + (file-name (file-name-nondirectory file)) + (dired-buf (buffer-name (current-buffer))) + (props (mapconcat #'identity (image-dired-list-tags file) ", ")) + (comment (image-dired-get-comment file)) + (message-log-max nil)) + (if file-name + (message "%s" + (image-dired-format-properties-string + dired-buf + file-name + props + comment))))) + + + +;;; Gallery support + +;; TODO: +;; * Support gallery creation when using per-directory thumbnail +;; storage. +;; * Enhanced gallery creation with basic CSS-support and pagination +;; of tag pages with many pictures. + +(defgroup image-dired-gallery nil + "Image-Dired support for generating a HTML gallery." + :prefix "image-dired-" + :group 'image-dired + :version "29.1") + +(defcustom image-dired-gallery-dir + (expand-file-name ".image-dired_gallery" image-dired-dir) + "Directory to store generated gallery html pages. +The name of this directory needs to be \"shared\" to the public +so that it can access the index.html page that image-dired creates." + :type 'directory) + +(defcustom image-dired-gallery-image-root-url + "https://example.org/image-diredpics" + "URL where the full size images are to be found on your web server. +Note that this URL has to be configured on your web server. +Image-Dired expects to find pictures in this directory. +This is used by `image-dired-gallery-generate'." + :type 'string + :version "29.1") + +(defcustom image-dired-gallery-thumb-image-root-url + "https://example.org/image-diredthumbs" + "URL where the thumbnail images are to be found on your web server. +Note that URL path has to be configured on your web server. +Image-Dired expects to find pictures in this directory. +This is used by `image-dired-gallery-generate'." + :type 'string + :version "29.1") + +(defcustom image-dired-gallery-hidden-tags + (list "private" "hidden" "pending") + "List of \"hidden\" tags. +Used by `image-dired-gallery-generate' to leave out \"hidden\" images." + :type '(repeat string)) + +(defvar image-dired-tag-file-list nil + "List to store tag-file structure.") + +(defvar image-dired-file-tag-list nil + "List to store file-tag structure.") + +(defvar image-dired-file-comment-list nil + "List to store file comments.") + +(defun image-dired--add-to-tag-file-lists (tag file) + "Helper function used from `image-dired--create-gallery-lists'. + +Add TAG to FILE in one list and FILE to TAG in the other. + +Lisp structures look like the following: + +image-dired-file-tag-list: + + ((\"filename1\" \"tag1\" \"tag2\" \"tag3\" ...) + (\"filename2\" \"tag1\" \"tag2\" \"tag3\" ...) + ...) + +image-dired-tag-file-list: + + ((\"tag1\" \"filename1\" \"filename2\" \"filename3\" ...) + (\"tag2\" \"filename1\" \"filename2\" \"filename3\" ...) + ...)" + ;; Add tag to file list + (let (curr) + (if image-dired-file-tag-list + (if (setq curr (assoc file image-dired-file-tag-list)) + (setcdr curr (cons tag (cdr curr))) + (setcdr image-dired-file-tag-list + (cons (list file tag) (cdr image-dired-file-tag-list)))) + (setq image-dired-file-tag-list (list (list file tag)))) + ;; Add file to tag list + (if image-dired-tag-file-list + (if (setq curr (assoc tag image-dired-tag-file-list)) + (if (not (member file curr)) + (setcdr curr (cons file (cdr curr)))) + (setcdr image-dired-tag-file-list + (cons (list tag file) (cdr image-dired-tag-file-list)))) + (setq image-dired-tag-file-list (list (list tag file)))))) + +(defun image-dired--add-to-file-comment-list (file comment) + "Helper function used from `image-dired--create-gallery-lists'. + +For FILE, add COMMENT to list. + +Lisp structure looks like the following: + +image-dired-file-comment-list: + + ((\"filename1\" . \"comment1\") + (\"filename2\" . \"comment2\") + ...)" + (if image-dired-file-comment-list + (if (not (assoc file image-dired-file-comment-list)) + (setcdr image-dired-file-comment-list + (cons (cons file comment) + (cdr image-dired-file-comment-list)))) + (setq image-dired-file-comment-list (list (cons file comment))))) + +(defun image-dired--create-gallery-lists () + "Create temporary lists used by `image-dired-gallery-generate'." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end beg file row-tags) + (setq image-dired-tag-file-list nil) + (setq image-dired-file-tag-list nil) + (setq image-dired-file-comment-list nil) + (goto-char (point-min)) + (while (search-forward-regexp "^." nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (setq beg (point)) + (unless (search-forward ";" end nil) + (error "Something is really wrong, check format of database")) + (setq row-tags (split-string + (buffer-substring beg end) ";")) + (setq file (car row-tags)) + (dolist (x (cdr row-tags)) + (if (not (string-match "^comment:\\(.*\\)" x)) + (image-dired--add-to-tag-file-lists x file) + (image-dired--add-to-file-comment-list file (match-string 1 x))))))) + ;; Sort tag-file list + (setq image-dired-tag-file-list + (sort image-dired-tag-file-list + (lambda (x y) + (string< (car x) (car y)))))) + +(defun image-dired--hidden-p (file) + "Return t if image FILE has a \"hidden\" tag." + (cl-loop for tag in (cdr (assoc file image-dired-file-tag-list)) + if (member tag image-dired-gallery-hidden-tags) return t)) + +(defun image-dired-gallery-generate () + "Generate gallery pages. +First we create a couple of Lisp structures from the database to make +it easier to generate, then HTML-files are created in +`image-dired-gallery-dir'." + (interactive) + (if (eq 'per-directory image-dired-thumbnail-storage) + (error "Currently, gallery generation is not supported \ +when using per-directory thumbnail file storage")) + (image-dired--create-gallery-lists) + (let ((tags image-dired-tag-file-list) + (index-file (format "%s/index.html" image-dired-gallery-dir)) + count tag tag-file + comment file-tags tag-link tag-link-list) + ;; Make sure gallery root exist + (if (file-exists-p image-dired-gallery-dir) + (if (not (file-directory-p image-dired-gallery-dir)) + (error "Variable image-dired-gallery-dir is not a directory")) + ;; FIXME: Should we set umask to 077 here, as we do for thumbnails? + (make-directory image-dired-gallery-dir)) + ;; Open index file + (with-temp-file index-file + (if (file-exists-p index-file) + (insert-file-contents index-file)) + (insert "\n") + (insert " \n") + (insert "

Image-Dired Gallery

\n") + (insert (format "

\n Gallery generated %s\n

\n" + (current-time-string))) + (insert "

Tag index

\n") + (setq count 1) + ;; Pre-generate list of all tag links + (dolist (curr tags) + (setq tag (car curr)) + (when (not (member tag image-dired-gallery-hidden-tags)) + (setq tag-link (format "%s" count tag)) + (if tag-link-list + (setq tag-link-list + (append tag-link-list (list (cons tag tag-link)))) + (setq tag-link-list (list (cons tag tag-link)))) + (setq count (1+ count)))) + (setq count 1) + ;; Main loop where we generated thumbnail pages per tag + (dolist (curr tags) + (setq tag (car curr)) + ;; Don't display hidden tags + (when (not (member tag image-dired-gallery-hidden-tags)) + ;; Insert link to tag page in index + (insert (format " %s
\n" (cdr (assoc tag tag-link-list)))) + ;; Open per-tag file + (setq tag-file (format "%s/%s.html" image-dired-gallery-dir count)) + (with-temp-file tag-file + (if (file-exists-p tag-file) + (insert-file-contents tag-file)) + (erase-buffer) + (insert "\n") + (insert " \n") + (insert "

Index

\n") + (insert (format "

Images with tag "%s"

" tag)) + ;; Main loop for files per tag page + (dolist (file (cdr curr)) + (unless (image-dired-hidden-p file) + ;; Insert thumbnail with link to full image + (insert + (format "\n" + image-dired-gallery-image-root-url + (file-name-nondirectory file) + image-dired-gallery-thumb-image-root-url + (file-name-nondirectory (image-dired-thumb-name file)) file)) + ;; Insert comment, if any + (if (setq comment (cdr (assoc file image-dired-file-comment-list))) + (insert (format "
\n%s
\n" comment)) + (insert "
\n")) + ;; Insert links to other tags, if any + (when (> (length + (setq file-tags (assoc file image-dired-file-tag-list))) 2) + (insert "[ ") + (dolist (extra-tag file-tags) + ;; Only insert if not file name or the main tag + (if (and (not (equal extra-tag tag)) + (not (equal extra-tag file))) + (insert + (format "%s " (cdr (assoc extra-tag tag-link-list)))))) + (insert "]
\n")))) + (insert "

Index

\n") + (insert " \n") + (insert "\n")) + (setq count (1+ count)))) + (insert " \n") + (insert "")))) + + +;;; Tag support + +(defvar image-dired-widget-list nil + "List to keep track of meta data in edit buffer.") + +(declare-function widget-forward "wid-edit" (arg)) + +;;;###autoload +(defun image-dired-dired-edit-comment-and-tags () + "Edit comment and tags of current or marked image files. +Edit comment and tags for all marked image files in an +easy-to-use form." + (interactive) + (setq image-dired-widget-list nil) + ;; Setup buffer. + (let ((files (dired-get-marked-files))) + (pop-to-buffer-same-window "*Image-Dired Edit Meta Data*") + (kill-all-local-variables) + (let ((inhibit-read-only t)) + (erase-buffer)) + (remove-overlays) + ;; Some help for the user. + (widget-insert +"\nEdit comments and tags for each image. Separate multiple tags +with a comma. Move forward between fields using TAB or RET. +Move to the previous field using backtab (S-TAB). Save by +activating the Save button at the bottom of the form or cancel +the operation by activating the Cancel button.\n\n") + ;; Here comes all images and a comment and tag field for each + ;; image. + (let (thumb-file img comment-widget tag-widget) + + (dolist (file files) + + (setq thumb-file (image-dired-thumb-name file) + img (create-image thumb-file)) + + (insert-image img) + (widget-insert "\n\nComment: ") + (setq comment-widget + (widget-create 'editable-field + :size 60 + :format "%v " + :value (or (image-dired-get-comment file) ""))) + (widget-insert "\nTags: ") + (setq tag-widget + (widget-create 'editable-field + :size 60 + :format "%v " + :value (or (mapconcat + #'identity + (image-dired-list-tags file) + ",") ""))) + ;; Save information in all widgets so that we can use it when + ;; the user saves the form. + (setq image-dired-widget-list + (append image-dired-widget-list + (list (list file comment-widget tag-widget)))) + (widget-insert "\n\n"))) + + ;; Footer with Save and Cancel button. + (widget-insert "\n") + (widget-create 'push-button + :notify + (lambda (&rest _ignore) + (image-dired-save-information-from-widgets) + (bury-buffer) + (message "Done")) + "Save") + (widget-insert " ") + (widget-create 'push-button + :notify + (lambda (&rest _ignore) + (bury-buffer) + (message "Operation canceled")) + "Cancel") + (widget-insert "\n") + (use-local-map widget-keymap) + (widget-setup) + ;; Jump to the first widget. + (widget-forward 1))) + +(defun image-dired-save-information-from-widgets () + "Save information found in `image-dired-widget-list'. +Use the information in `image-dired-widget-list' to save comments and +tags to their respective image file. Internal function used by +`image-dired-dired-edit-comment-and-tags'." + (let (file comment tag-string tag-list lst) + (image-dired-write-comments + (mapcar + (lambda (widget) + (setq file (car widget) + comment (widget-value (cadr widget))) + (cons file comment)) + image-dired-widget-list)) + (image-dired-write-tags + (dolist (widget image-dired-widget-list lst) + (setq file (car widget) + tag-string (widget-value (car (cddr widget))) + tag-list (split-string tag-string ",")) + (dolist (tag tag-list) + (push (cons file tag) lst)))))) + + +;;; bookmark.el support + +(declare-function bookmark-make-record-default + "bookmark" (&optional no-file no-context posn)) +(declare-function bookmark-prop-get "bookmark" (bookmark prop)) + +(defun image-dired-bookmark-name () + "Create a default bookmark name for the current EWW buffer." + (file-name-nondirectory + (directory-file-name + (file-name-directory (image-dired-original-file-name))))) + +(defun image-dired-bookmark-make-record () + "Create a bookmark for the current EWW buffer." + `(,(image-dired-bookmark-name) + ,@(bookmark-make-record-default t) + (location . ,(file-name-directory (image-dired-original-file-name))) + (image-dired-file . ,(file-name-nondirectory (image-dired-original-file-name))) + (handler . image-dired-bookmark-jump))) + +;;;###autoload +(defun image-dired-bookmark-jump (bookmark) + "Default bookmark handler for Image-Dired buffers." + ;; User already cached thumbnails, so disable any checking. + (let ((image-dired-show-all-from-dir-max-files nil)) + (image-dired (bookmark-prop-get bookmark 'location)) + ;; TODO: Go to the bookmarked file, if it exists. + ;; (bookmark-prop-get bookmark 'image-dired-file) + (goto-char (point-min)))) + +(put 'image-dired-bookmark-jump 'bookmark-handler-type "Image-Dired") + +;;; Obsolete + +;;;###autoload +(define-obsolete-function-alias 'tumme #'image-dired "24.4") + +;;;###autoload +(define-obsolete-function-alias 'image-dired-setup-dired-keybindings + #'image-dired-minor-mode "26.1") + +(defcustom image-dired-temp-image-file + (expand-file-name ".image-dired_temp" image-dired-dir) + "Name of temporary image file used by various commands." + :type 'file) +(make-obsolete-variable 'image-dired-temp-image-file + "no longer used." "29.1") + +(defcustom image-dired-cmd-create-temp-image-program + (if (executable-find "gm") "gm" "convert") + "Executable used to create temporary image. +Used together with `image-dired-cmd-create-temp-image-options'." + :type 'file + :version "29.1") +(make-obsolete-variable 'image-dired-cmd-create-temp-image-program + "no longer used." "29.1") + +(defcustom image-dired-cmd-create-temp-image-options + (let ((opts '("-size" "%wx%h" "%f[0]" + "-resize" "%wx%h>" + "-strip" "jpeg:%t"))) + (if (executable-find "gm") (cons "convert" opts) opts)) + "Options of command used to create temporary image for display window. +Used together with `image-dired-cmd-create-temp-image-program', +Available format specifiers are: %w and %h which are replaced by +the calculated max size for width and height in the image display window, +%f which is replaced by the file name of the original image and %t which +is replaced by the file name of the temporary file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-create-temp-image-options + "no longer used." "29.1") + +(defcustom image-dired-display-window-width-correction 1 + "Number to be used to correct image display window width. +Change if the default (1) does not work (i.e. if the image does not +completely fit)." + :type 'integer) +(make-obsolete-variable 'image-dired-display-window-width-correction + "no longer used." "29.1") + +(defcustom image-dired-display-window-height-correction 0 + "Number to be used to correct image display window height. +Change if the default (0) does not work (i.e. if the image does not +completely fit)." + :type 'integer) +(make-obsolete-variable 'image-dired-display-window-height-correction + "no longer used." "29.1") + +(defun image-dired-display-window-width (window) + "Return width, in pixels, of WINDOW." + (declare (obsolete nil "29.1")) + (- (image-dired-window-width-pixels window) + image-dired-display-window-width-correction)) + +(defun image-dired-display-window-height (window) + "Return height, in pixels, of WINDOW." + (declare (obsolete nil "29.1")) + (- (image-dired-window-height-pixels window) + image-dired-display-window-height-correction)) + +(defun image-dired-window-height-pixels (window) + "Calculate WINDOW height in pixels." + (declare (obsolete nil "29.1")) + ;; Note: The mode-line consumes one line + (* (- (window-height window) 1) (frame-char-height))) + +(defcustom image-dired-cmd-read-exif-data-program "exiftool" + "Program used to read EXIF data to image. +Used together with `image-dired-cmd-read-exif-data-options'." + :type 'file) +(make-obsolete-variable 'image-dired-cmd-read-exif-data-program + "use `exif-parse-file' and `exif-field' instead." "29.1") + +(defcustom image-dired-cmd-read-exif-data-options '("-s" "-s" "-s" "-%t" "%f") + "Arguments of command used to read EXIF data. +Used with `image-dired-cmd-read-exif-data-program'. +Available format specifiers are: %f which is replaced +by the image file name and %t which is replaced by the tag name." + :version "26.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-read-exif-data-options + "use `exif-parse-file' and `exif-field' instead." "29.1") + +(defun image-dired-get-exif-data (file tag-name) + "From FILE, return EXIF tag TAG-NAME." + (declare (obsolete "use `exif-parse-file' and `exif-field' instead." "29.1")) + (image-dired--check-executable-exists + 'image-dired-cmd-read-exif-data-program) + (let ((buf (get-buffer-create "*image-dired-get-exif-data*")) + (spec (list (cons ?f file) (cons ?t tag-name))) + tag-value) + (with-current-buffer buf + (delete-region (point-min) (point-max)) + (if (not (eq (apply #'call-process image-dired-cmd-read-exif-data-program + nil t nil + (mapcar + (lambda (arg) (format-spec arg spec)) + image-dired-cmd-read-exif-data-options)) + 0)) + (error "Could not get EXIF tag") + (goto-char (point-min)) + ;; Clean buffer from newlines and carriage returns before + ;; getting final info + (while (search-forward-regexp "[\n\r]" nil t) + (replace-match "" nil t)) + (setq tag-value (buffer-substring (point-min) (point-max))))) + tag-value)) + +(defcustom image-dired-cmd-rotate-thumbnail-program + (if (executable-find "gm") "gm" "mogrify") + "Executable used to rotate thumbnail. +Used together with `image-dired-cmd-rotate-thumbnail-options'." + :type 'file + :version "29.1") +(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-program nil "29.1") + +(defcustom image-dired-cmd-rotate-thumbnail-options + (let ((opts '("-rotate" "%d" "%t"))) + (if (executable-find "gm") (cons "mogrify" opts) opts)) + "Arguments of command used to rotate thumbnail image. +Used with `image-dired-cmd-rotate-thumbnail-program'. +Available format specifiers are: %d which is replaced by the +number of (positive) degrees to rotate the image, normally 90 or 270 +\(for 90 degrees right and left), %t which is replaced by the file name +of the thumbnail file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-options nil "29.1") + +(defun image-dired-rotate-thumbnail (degrees) + "Rotate thumbnail DEGREES degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (image-dired--check-executable-exists + 'image-dired-cmd-rotate-thumbnail-program) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (let* ((file (image-dired-thumb-name (image-dired-original-file-name))) + (thumb (expand-file-name file)) + (spec (list (cons ?d degrees) (cons ?t thumb)))) + (apply #'call-process image-dired-cmd-rotate-thumbnail-program nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-rotate-thumbnail-options)) + (clear-image-cache thumb)))) + +(defun image-dired-rotate-thumbnail-left () + "Rotate thumbnail left (counter clockwise) 90 degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (interactive) + (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) + (image-dired-rotate-thumbnail "270"))) + +(defun image-dired-rotate-thumbnail-right () + "Rotate thumbnail counter right (clockwise) 90 degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (interactive) + (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) + (image-dired-rotate-thumbnail "90"))) + +(defun image-dired-modify-mark-on-thumb-original-file (command) + "Modify mark in Dired buffer. +COMMAND is one of `mark' for marking file in Dired, `unmark' for +unmarking file in Dired or `flag' for flagging file for delete in +Dired." + (declare (obsolete image-dired--on-file-in-dired-buffer "29.1")) + (let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (if (not (and dired-buf file-name)) + (message "No image, or image with correct properties, at point") + (with-current-buffer dired-buf + (message "%s" file-name) + (when (dired-goto-file file-name) + (cond ((eq command 'mark) (dired-mark 1)) + ((eq command 'unmark) (dired-unmark 1)) + ((eq command 'toggle) + (if (image-dired-dired-file-marked-p) + (dired-unmark 1) + (dired-mark 1))) + ((eq command 'flag) (dired-flag-file-deletion 1))) + (image-dired-thumb-update-marks)))))) + +(defun image-dired-display-current-image-full () + "Display current image in full size." + (declare (obsolete image-transform-original "29.1")) + (interactive nil image-dired-thumbnail-mode) + (let ((file (image-dired-original-file-name))) + (if file + (progn + (image-dired-display-image file) + (with-current-buffer image-dired-display-image-buffer + (image-transform-original))) + (error "No original file name at point")))) + +(defun image-dired-display-current-image-sized () + "Display current image in sized to fit window dimensions." + (declare (obsolete image-mode-fit-frame "29.1")) + (interactive nil image-dired-thumbnail-mode) + (let ((file (image-dired-original-file-name))) + (if file + (progn + (image-dired-display-image file)) + (error "No original file name at point")))) + +(defun image-dired-add-to-tag-file-list (tag file) + "Add relation between TAG and FILE." + (declare (obsolete nil "29.1")) + (let (curr) + (if image-dired-tag-file-list + (if (setq curr (assoc tag image-dired-tag-file-list)) + (if (not (member file curr)) + (setcdr curr (cons file (cdr curr)))) + (setcdr image-dired-tag-file-list + (cons (list tag file) (cdr image-dired-tag-file-list)))) + (setq image-dired-tag-file-list (list (list tag file)))))) + +(defun image-dired-display-thumb-properties () + "Display thumbnail properties in the echo area." + (declare (obsolete image-dired-update-header-line "29.1")) + (image-dired-update-header-line)) + +(defvar image-dired-slideshow-count 0 + "Keeping track on number of images in slideshow.") +(make-obsolete-variable 'image-dired-slideshow-count "no longer used." "29.1") + +(defvar image-dired-slideshow-times 0 + "Number of pictures to display in slideshow.") +(make-obsolete-variable 'image-dired-slideshow-times "no longer used." "29.1") + +(define-obsolete-function-alias 'image-dired-create-display-image-buffer + #'ignore "29.1") +(define-obsolete-function-alias 'image-dired-create-gallery-lists + #'image-dired--create-gallery-lists "29.1") +(define-obsolete-function-alias 'image-dired-add-to-file-comment-list + #'image-dired--add-to-file-comment-list "29.1") +(define-obsolete-function-alias 'image-dired-add-to-tag-file-lists + #'image-dired--add-to-tag-file-lists "29.1") +(define-obsolete-function-alias 'image-dired-hidden-p + #'image-dired--hidden-p "29.1") + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;; TEST-SECTION ;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; (defvar image-dired-dir-max-size 12300000) + +;; (defun image-dired-test-clean-old-files () +;; "Clean `image-dired-dir' from old thumbnail files. +;; \"Oldness\" measured using last access time. If the total size of all +;; thumbnail files in `image-dired-dir' is larger than 'image-dired-dir-max-size', +;; old files are deleted until the max size is reached." +;; (let* ((files +;; (sort +;; (mapcar +;; (lambda (f) +;; (let ((fattribs (file-attributes f))) +;; `(,(file-attribute-access-time fattribs) +;; ,(file-attribute-size fattribs) ,f))) +;; (directory-files (image-dired-dir) t ".+\\.thumb\\..+$")) +;; ;; Sort function. Compare time between two files. +;; (lambda (l1 l2) +;; (time-less-p (car l1) (car l2))))) +;; (dirsize (apply '+ (mapcar (lambda (x) (cadr x)) files)))) +;; (while (> dirsize image-dired-dir-max-size) +;; (y-or-n-p +;; (format "Size of thumbnail directory: %d, delete old file %s? " +;; dirsize (cadr (cdar files)))) +;; (delete-file (cadr (cdar files))) +;; (setq dirsize (- dirsize (car (cdar files)))) +;; (setq files (cdr files))))) + +(provide 'image-dired) + +;;; image-dired.el ends here diff --git a/lisp/image/image-dired-tags.el b/lisp/image/image-dired-tags.el new file mode 100644 index 0000000000..9f12354111 --- /dev/null +++ b/lisp/image/image-dired-tags.el @@ -0,0 +1,3080 @@ +;;; image-dired.el --- use dired to browse and manipulate your images -*- lexical-binding: t -*- + +;; Copyright (C) 2005-2022 Free Software Foundation, Inc. + +;; Version: 0.4.11 +;; Keywords: multimedia +;; Author: Mathias Dahl + +;; 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: + +;; BACKGROUND +;; ========== +;; +;; I needed a program to browse, organize and tag my pictures. I got +;; tired of the old gallery program I used as it did not allow +;; multi-file operations easily. Also, it put things out of my +;; control. Image viewing programs I tested did not allow multi-file +;; operations or did not do what I wanted it to. +;; +;; So, I got the idea to use the wonderful functionality of Emacs and +;; `dired' to do it. It would allow me to do almost anything I wanted, +;; which is basically just to browse all my pictures in an easy way, +;; letting me manipulate and tag them in various ways. `dired' already +;; provide all the file handling and navigation facilities; I only +;; needed to add some functions to display the images. +;; +;; I briefly tried out thumbs.el, and although it seemed more +;; powerful than this package, it did not work the way I wanted to. It +;; was too slow to create thumbnails of all files in a directory (I +;; currently keep all my 2000+ images in the same directory) and +;; browsing the thumbnail buffer was slow too. image-dired.el will not +;; create thumbnails until they are needed and the browsing is done +;; quickly and easily in Dired. I copied a great deal of ideas and +;; code from there though... :) +;; +;; `image-dired' stores the thumbnail files in `image-dired-dir' +;; using the file name format ORIGNAME.thumb.ORIGEXT. For example +;; ~/.emacs.d/image-dired/myimage01.thumb.jpg. The "database" is for +;; now just a plain text file with the following format: +;; +;; file-name-non-directory;comment:comment-text;tag1;tag2;tag3;...;tagN +;; +;; +;; PREREQUISITES +;; ============= +;; +;; * The GraphicsMagick or ImageMagick package; Image-Dired uses +;; whichever is available. +;; +;; A) For GraphicsMagick, `gm' is used. +;; Find it here: http://www.graphicsmagick.org/ +;; +;; B) For ImageMagick, `convert' and `mogrify' are used. +;; Find it here: https://www.imagemagick.org. +;; +;; * For non-lossy rotation of JPEG images, the JpegTRAN program is +;; needed. +;; +;; * For `image-dired-set-exif-data' to work, the command line tool `exiftool' is +;; needed. It can be found here: https://exiftool.org/. This +;; function is, among other things, used for writing comments to +;; image files using `image-dired-thumbnail-set-image-description'. +;; +;; +;; USAGE +;; ===== +;; +;; This information has been moved to the manual. Type `C-h r' to open +;; the Emacs manual and go to the node Thumbnails by typing `g +;; Image-Dired RET'. +;; +;; Quickstart: M-x image-dired RET DIRNAME RET +;; +;; where DIRNAME is a directory containing image files. +;; +;; LIMITATIONS +;; =========== +;; +;; * Supports all image formats that Emacs and convert supports, but +;; the thumbnails are hard-coded to JPEG or PNG format. It uses +;; JPEG by default, but can optionally follow the Thumbnail Managing +;; Standard (v0.9.0, Dec 2020), which mandates PNG. See the user +;; option `image-dired-thumbnail-storage'. +;; +;; * WARNING: The "database" format used might be changed so keep a +;; backup of `image-dired-db-file' when testing new versions. +;; +;; TODO +;; ==== +;; +;; * Investigate if it is possible to also write the tags to the image +;; files. +;; +;; * From thumbs.el: Add an option for clean-up/max-size functionality +;; for thumbnail directory. +;; +;; * From thumbs.el: Add setroot function. +;; +;; * Add `image-dired-display-thumbs-ring' and functions to cycle that. Find out +;; which is best, saving old batch just before inserting new, or +;; saving the current batch in the ring when inserting it. Adding +;; it probably needs rewriting `image-dired-display-thumbs' to be more general. +;; +;; * Find some way of toggling on and off really nice keybindings in +;; Dired (for example, using C-n or instead of C-S-n). +;; Richard suggested that we could keep C-t as prefix for +;; image-dired commands as it is currently not used in Dired. He +;; also suggested that `dired-next-line' and `dired-previous-line' +;; figure out if image-dired is enabled in the current buffer and, +;; if it is, call `image-dired-dired-next-line' and `image-dired-dired-previous-line', +;; respectively. Update: This is partly done; some bindings have +;; now been added to Dired. +;; +;; * In some way keep track of buffers and windows and stuff so that +;; it works as the user expects. +;; +;; * More/better documentation. + +;;; Code: + +(require 'dired) +(require 'exif) +(require 'image-mode) +(require 'widget) +(require 'xdg) + +(eval-when-compile + (require 'cl-lib) + (require 'wid-edit)) + + +;;; Customizable variables + +(defgroup image-dired nil + "Use Dired to browse your images as thumbnails, and more." + :prefix "image-dired-" + :link '(info-link "(emacs) Image-Dired") + :group 'multimedia) + +(defcustom image-dired-dir (locate-user-emacs-file "image-dired/") + "Directory where thumbnail images are stored. + +The value of this option will be ignored if Image-Dired is +customized to use the Thumbnail Managing Standard; they will be +saved in \"$XDG_CACHE_HOME/thumbnails/\" instead. See +`image-dired-thumbnail-storage'." + :type 'directory) + +(defcustom image-dired-thumbnail-storage 'use-image-dired-dir + "How `image-dired' stores thumbnail files. +There are two ways that Image-Dired can store and generate +thumbnails. If you set this variable to one of the two following +values, they will be stored in the JPEG format: + +- `use-image-dired-dir' means that the thumbnails are stored in a + central directory. + +- `per-directory' means that each thumbnail is stored in a + subdirectory called \".image-dired\" in the same directory + where the image file is. + +It can also use the \"Thumbnail Managing Standard\", which allows +sharing of thumbnails across different programs. Thumbnails will +be stored in \"$XDG_CACHE_HOME/thumbnails/\" instead of in +`image-dired-dir'. Thumbnails are saved in the PNG format, and +can be one of the following sizes: + +- `standard' means use thumbnails sized 128x128. +- `standard-large' means use thumbnails sized 256x256. +- `standard-x-large' means use thumbnails sized 512x512. +- `standard-xx-large' means use thumbnails sized 1024x1024. + +For more information on the Thumbnail Managing Standard, see: +https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html" + :type '(choice :tag "How to store thumbnail files" + (const :tag "Use image-dired-dir" use-image-dired-dir) + (const :tag "Thumbnail Managing Standard (normal 128x128)" + standard) + (const :tag "Thumbnail Managing Standard (large 256x256)" + standard-large) + (const :tag "Thumbnail Managing Standard (larger 512x512)" + standard-x-large) + (const :tag "Thumbnail Managing Standard (extra large 1024x1024)" + standard-xx-large) + (const :tag "Per-directory" per-directory)) + :version "29.1") + +(defconst image-dired--thumbnail-standard-sizes + '( standard standard-large + standard-x-large standard-xx-large) + "List of symbols representing thumbnail sizes in Thumbnail Managing Standard.") + +(defcustom image-dired-db-file + (expand-file-name ".image-dired_db" image-dired-dir) + "Database file where file names and their associated tags are stored." + :type 'file) + +(defcustom image-dired-cmd-create-thumbnail-program + (if (executable-find "gm") "gm" "convert") + "Executable used to create thumbnail. +Used together with `image-dired-cmd-create-thumbnail-options'." + :type 'file + :version "29.1") + +(defcustom image-dired-cmd-create-thumbnail-options + (let ((opts '("-size" "%wx%h" "%f[0]" + "-resize" "%wx%h>" + "-strip" "jpeg:%t"))) + (if (executable-find "gm") (cons "convert" opts) opts)) + "Options of command used to create thumbnail image. +Used with `image-dired-cmd-create-thumbnail-program'. +Available format specifiers are: %w which is replaced by +`image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', +%f which is replaced by the file name of the original image and %t +which is replaced by the file name of the thumbnail file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-pngnq-program + ;; Prefer pngquant to pngnq-s9 as it is faster on my machine. + ;; The project also seems more active than the alternatives. + ;; Prefer pngnq-s9 to pngnq as it fixes bugs in pngnq. + ;; The pngnq project seems dead (?) since 2011 or so. + (or (executable-find "pngquant") + (executable-find "pngnq-s9") + (executable-find "pngnq")) + "The file name of the `pngquant' or `pngnq' program. +It quantizes colors of PNG images down to 256 colors or fewer +using the NeuQuant algorithm." + :version "29.1" + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-pngnq-options + (if (executable-find "pngquant") + '("--ext" "-nq8.png" "%t") ; same extension as "pngnq" + '("-f" "%t")) + "Arguments to pass `image-dired-cmd-pngnq-program'. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options'." + :type '(repeat (string :tag "Argument")) + :version "29.1") + +(defcustom image-dired-cmd-pngcrush-program (executable-find "pngcrush") + "The file name of the `pngcrush' program. +It optimizes the compression of PNG images. Also it adds PNG textual chunks +with the information required by the Thumbnail Managing Standard." + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-pngcrush-options + `("-q" + "-text" "b" "Description" "Thumbnail of file://%f" + "-text" "b" "Software" ,(emacs-version) + ;; "-text b \"Thumb::Image::Height\" \"%oh\" " + ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" " + ;; "-text b \"Thumb::Image::Width\" \"%ow\" " + "-text" "b" "Thumb::MTime" "%m" + ;; "-text b \"Thumb::Size\" \"%b\" " + "-text" "b" "Thumb::URI" "file://%f" + "%q" "%t") + "Arguments for `image-dired-cmd-pngcrush-program'. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options', with %q for a +temporary file name (typically generated by pnqnq)." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-optipng-program (executable-find "optipng") + "The file name of the `optipng' program." + :version "26.1" + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-optipng-options '("-o5" "%t") + "Arguments passed to `image-dired-cmd-optipng-program'. +Available format specifiers are described in +`image-dired-cmd-create-thumbnail-options'." + :version "26.1" + :type '(repeat (string :tag "Argument")) + :link '(url-link "man:optipng(1)")) + +(defcustom image-dired-cmd-create-standard-thumbnail-options + (append '("-size" "%wx%h" "%f[0]") + (unless (or image-dired-cmd-pngcrush-program + image-dired-cmd-pngnq-program) + (list + "-set" "Thumb::MTime" "%m" + "-set" "Thumb::URI" "file://%f" + "-set" "Description" "Thumbnail of file://%f" + "-set" "Software" (emacs-version))) + '("-thumbnail" "%wx%h>" "png:%t")) + "Options for creating thumbnails according to the Thumbnail Managing Standard. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options', with %m for file modification time." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-rotate-original-program + "jpegtran" + "Executable used to rotate original image. +Used together with `image-dired-cmd-rotate-original-options'." + :type 'file) + +(defcustom image-dired-cmd-rotate-original-options + '("-rotate" "%d" "-copy" "all" "-outfile" "%t" "%o") + "Arguments of command used to rotate original image. +Used with `image-dired-cmd-rotate-original-program'. +Available format specifiers are: %d which is replaced by the +number of (positive) degrees to rotate the image, normally 90 or +270 \(for 90 degrees right and left), %o which is replaced by the +original image file name and %t which is replaced by +`image-dired-temp-image-file'." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-temp-rotate-image-file + (expand-file-name ".image-dired_rotate_temp" image-dired-dir) + "Temporary file for rotate operations." + :type 'file) + +(defcustom image-dired-rotate-original-ask-before-overwrite t + "Confirm overwrite of original file after rotate operation. +If non-nil, ask user for confirmation before overwriting the +original file with `image-dired-temp-rotate-image-file'." + :type 'boolean) + +(defcustom image-dired-cmd-write-exif-data-program + "exiftool" + "Program used to write EXIF data to image. +Used together with `image-dired-cmd-write-exif-data-options'." + :type 'file) + +(defcustom image-dired-cmd-write-exif-data-options + '("-%t=%v" "%f") + "Arguments of command used to write EXIF data. +Used with `image-dired-cmd-write-exif-data-program'. +Available format specifiers are: %f which is replaced by +the image file name, %t which is replaced by the tag name and %v +which is replaced by the tag value." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-thumb-size + (cond + ((eq 'standard image-dired-thumbnail-storage) 128) + ((eq 'standard-large image-dired-thumbnail-storage) 256) + ((eq 'standard-x-large image-dired-thumbnail-storage) 512) + ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) + (t 100)) + "Size of thumbnails, in pixels. +This is the default size for both `image-dired-thumb-width' +and `image-dired-thumb-height'. + +The value of this option will be ignored if Image-Dired is +customized to use the Thumbnail Managing Standard; the standard +sizes will be used instead. See `image-dired-thumbnail-storage'." + :type 'integer) + +(defcustom image-dired-thumb-width image-dired-thumb-size + "Width of thumbnails, in pixels." + :type 'integer) + +(defcustom image-dired-thumb-height image-dired-thumb-size + "Height of thumbnails, in pixels." + :type 'integer) + +(defcustom image-dired-thumb-relief 2 + "Size of button-like border around thumbnails." + :type 'integer) + +(defcustom image-dired-thumb-margin 2 + "Size of the margin around thumbnails. +This is where you see the cursor." + :type 'integer) + +(defcustom image-dired-thumb-visible-marks t + "Make marks and flags visible in thumbnail buffer. +If non-nil, apply the `image-dired-thumb-mark' face to marked +images and `image-dired-thumb-flagged' to images flagged for +deletion." + :type 'boolean + :version "28.1") + +(defface image-dired-thumb-mark + '((((class color) (min-colors 16)) :background "DarkOrange") + (((class color)) :foreground "yellow")) + "Face for marked images in thumbnail buffer." + :version "29.1") + +(defface image-dired-thumb-flagged + '((((class color) (min-colors 88) (background light)) :background "Red3") + (((class color) (min-colors 88) (background dark)) :background "Pink") + (((class color) (min-colors 16) (background light)) :background "Red3") + (((class color) (min-colors 16) (background dark)) :background "Pink") + (((class color) (min-colors 8)) :background "red") + (t :inverse-video t)) + "Face for images flagged for deletion in thumbnail buffer." + :version "29.1") + +(defcustom image-dired-line-up-method 'dynamic + "Default method for line-up of thumbnails in thumbnail buffer. +Used by `image-dired-display-thumbs' and other functions that needs +to line-up thumbnails. Dynamic means to use the available width of +the window containing the thumbnail buffer, Fixed means to use +`image-dired-thumbs-per-row', Interactive is for asking the user, +and No line-up means that no automatic line-up will be done." + :type '(choice :tag "Default line-up method" + (const :tag "Dynamic" dynamic) + (const :tag "Fixed" fixed) + (const :tag "Interactive" interactive) + (const :tag "No line-up" none))) + +(defcustom image-dired-thumbs-per-row 3 + "Number of thumbnails to display per row in thumb buffer." + :type 'integer) + +(defcustom image-dired-track-movement t + "The current state of the tracking and mirroring. +For more information, see the documentation for +`image-dired-toggle-movement-tracking'." + :type 'boolean) + +(defcustom image-dired-append-when-browsing nil + "Append thumbnails in thumbnail buffer when browsing. +If non-nil, using `image-dired-next-line-and-display' and +`image-dired-previous-line-and-display' will leave a trail of thumbnail +images in the thumbnail buffer. If you enable this and want to clean +the thumbnail buffer because it is filled with too many thumbnails, +just call `image-dired-display-thumb' to display only the image at point. +This value can be toggled using `image-dired-toggle-append-browsing'." + :type 'boolean) + +(defcustom image-dired-dired-disp-props t + "If non-nil, display properties for Dired file when browsing. +Used by `image-dired-next-line-and-display', +`image-dired-previous-line-and-display' and `image-dired-mark-and-display-next'. +If the database file is large, this can slow down image browsing in +Dired and you might want to turn it off." + :type 'boolean) + +(defcustom image-dired-display-properties-format "%b: %f (%t): %c" + "Display format for thumbnail properties. +%b is replaced with associated Dired buffer name, %f with file +name (without path) of original image file, %t with the list of +tags and %c with the comment." + :type 'string) + +(defcustom image-dired-external-viewer + ;; TODO: Use mailcap, dired-guess-shell-alist-default, + ;; dired-view-command-alist. + (cond ((executable-find "display")) + ((executable-find "xli")) + ((executable-find "qiv") "qiv -t") + ((executable-find "feh") "feh")) + "Name of external viewer. +Including parameters. Used when displaying original image from +`image-dired-thumbnail-mode'." + :version "28.1" + :type '(choice string + (const :tag "Not Set" nil))) + +(defcustom image-dired-main-image-directory + (or (xdg-user-dir "PICTURES") "~/pics/") + "Name of main image directory, if any. +Used by `image-dired-copy-with-exif-file-name'." + :type 'string + :version "29.1") + +(defcustom image-dired-show-all-from-dir-max-files 500 + "Maximum number of files in directory before prompting. + +If there are more image files than this in a selected directory, +the `image-dired-show-all-from-dir' command will ask for +confirmation before creating the thumbnail buffer. If this +variable is nil, it will never ask." + :type '(choice integer + (const :tag "Disable warning" nil)) + :version "29.1") + +(defcustom image-dired-marking-shows-next t + "If non-nil, marking, unmarking or flagging an image shows the next image. + +This affects the following commands: +\\ + `image-dired-flag-thumb-original-file' (bound to \\[image-dired-flag-thumb-original-file]) + `image-dired-mark-thumb-original-file' (bound to \\[image-dired-mark-thumb-original-file]) + `image-dired-unmark-thumb-original-file' (bound to \\[image-dired-unmark-thumb-original-file])" + :type 'boolean + :version "29.1") + + +;;; Util functions + +(defvar image-dired-debug nil + "Non-nil means enable debug messages.") + +(defun image-dired-debug-message (&rest args) + "Display debug message ARGS when `image-dired-debug' is non-nil." + (when image-dired-debug + (apply #'message args))) + +(defmacro image-dired--with-db-file (&rest body) + "Run BODY in a temp buffer containing `image-dired-db-file'. +Return the last form in BODY." + (declare (indent 0) (debug t)) + `(with-temp-buffer + (if (file-exists-p image-dired-db-file) + (insert-file-contents image-dired-db-file)) + ,@body)) + +(defun image-dired-dir () + "Return the current thumbnail directory (from variable `image-dired-dir'). +Create the thumbnail directory if it does not exist." + (let ((image-dired-dir (file-name-as-directory + (expand-file-name image-dired-dir)))) + (unless (file-directory-p image-dired-dir) + (with-file-modes #o700 + (make-directory image-dired-dir t)) + (message "Thumbnail directory created: %s" image-dired-dir)) + image-dired-dir)) + +(defun image-dired-insert-image (file type relief margin) + "Insert image FILE of image TYPE, using RELIEF and MARGIN, at point." + (let ((i `(image :type ,type + :file ,file + :relief ,relief + :margin ,margin))) + (insert-image i))) + +(defun image-dired-get-thumbnail-image (file) + "Return the image descriptor for a thumbnail of image file FILE." + (unless (string-match-p (image-file-name-regexp) file) + (error "%s is not a valid image file" file)) + (let* ((thumb-file (image-dired-thumb-name file)) + (thumb-attr (file-attributes thumb-file))) + (when (or (not thumb-attr) + (time-less-p (file-attribute-modification-time thumb-attr) + (file-attribute-modification-time + (file-attributes file)))) + (image-dired-create-thumb file thumb-file)) + (create-image thumb-file))) + +(defun image-dired-insert-thumbnail (file original-file-name + associated-dired-buffer) + "Insert thumbnail image FILE. +Add text properties ORIGINAL-FILE-NAME and ASSOCIATED-DIRED-BUFFER." + (let (beg end) + (setq beg (point)) + (image-dired-insert-image + file + ;; Thumbnails are created asynchronously, so we might not yet + ;; have a file. But if it exists, it might have been cached from + ;; before and we should use it instead of our current settings. + (or (and (file-exists-p file) + (image-type-from-file-header file)) + (and (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + 'png) + 'jpeg) + image-dired-thumb-relief + image-dired-thumb-margin) + (setq end (point)) + (add-text-properties + beg end + (list 'image-dired-thumbnail t + 'original-file-name original-file-name + 'associated-dired-buffer associated-dired-buffer + 'tags (image-dired-list-tags original-file-name) + 'mouse-face 'highlight + 'comment (image-dired-get-comment original-file-name))))) + +(defun image-dired-thumb-name (file) + "Return absolute file name for thumbnail FILE. +Depending on the value of `image-dired-thumbnail-storage', the +file name of the thumbnail will vary: +- For `use-image-dired-dir', make a SHA1-hash of the image file's + directory name and add that to make the thumbnail file name + unique. +- For `per-directory' storage, just add a subdirectory. +- For `standard' storage, produce the file name according to the + Thumbnail Managing Standard. Among other things, an MD5-hash + of the image file's directory name will be added to the + filename. +See also `image-dired-thumbnail-storage'." + (cond ((memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + (let ((thumbdir (cl-case image-dired-thumbnail-storage + (standard "thumbnails/normal") + (standard-large "thumbnails/large") + (standard-x-large "thumbnails/x-large") + (standard-xx-large "thumbnails/xx-large")))) + (expand-file-name + ;; MD5 is mandated by the Thumbnail Managing Standard. + (concat (md5 (concat "file://" (expand-file-name file))) ".png") + (expand-file-name thumbdir (xdg-cache-home))))) + ((eq 'use-image-dired-dir image-dired-thumbnail-storage) + (let* ((f (expand-file-name file)) + (hash + (md5 (file-name-as-directory (file-name-directory f))))) + (format "%s%s%s.thumb.%s" + (file-name-as-directory (expand-file-name (image-dired-dir))) + (file-name-base f) + (if hash (concat "_" hash) "") + (file-name-extension f)))) + ((eq 'per-directory image-dired-thumbnail-storage) + (let ((f (expand-file-name file))) + (format "%s.image-dired/%s.thumb.%s" + (file-name-directory f) + (file-name-base f) + (file-name-extension f)))))) + +(defun image-dired--check-executable-exists (executable) + (unless (executable-find (symbol-value executable)) + (error "Executable %S not found" executable))) + + +;;; Creating thumbnails + +(defun image-dired-thumb-size (dimension) + "Return thumb size depending on `image-dired-thumbnail-storage'. +DIMENSION should be either the symbol `width' or `height'." + (cond + ((eq 'standard image-dired-thumbnail-storage) 128) + ((eq 'standard-large image-dired-thumbnail-storage) 256) + ((eq 'standard-x-large image-dired-thumbnail-storage) 512) + ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) + (t (cl-ecase dimension + (width image-dired-thumb-width) + (height image-dired-thumb-height))))) + +(defvar image-dired--generate-thumbs-start nil + "Time when `display-thumbs' was called.") + +(defvar image-dired-queue nil + "List of items in the queue. +Each item has the form (ORIGINAL-FILE TARGET-FILE).") + +(defvar image-dired-queue-active-jobs 0 + "Number of active jobs in `image-dired-queue'.") + +(defvar image-dired-queue-active-limit (min 4 (max 2 (/ (num-processors) 2))) + "Maximum number of concurrent jobs permitted for generating images. +Increase at own risk. If you want to experiment with this, +consider setting `image-dired-debug' to a non-nil value to see +the time spent on generating thumbnails. Run `image-clear-cache' +and remove the cached thumbnail files between each trial run.") + +(defun image-dired-pngnq-thumb (spec) + "Quantize thumbnail described by format SPEC with pngnq(1)." + (let ((process + (apply #'start-process "image-dired-pngnq" nil + image-dired-cmd-pngnq-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-pngnq-options)))) + (setf (process-sentinel process) + (lambda (process status) + (if (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + ;; Pass off to pngcrush, or just rename the + ;; THUMB-nq8.png file back to THUMB.png + (if (and image-dired-cmd-pngcrush-program + (executable-find image-dired-cmd-pngcrush-program)) + (image-dired-pngcrush-thumb spec) + (let ((nq8 (cdr (assq ?q spec))) + (thumb (cdr (assq ?t spec)))) + (rename-file nq8 thumb t))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))))) + process)) + +(defun image-dired-pngcrush-thumb (spec) + "Optimize thumbnail described by format SPEC with pngcrush(1)." + ;; If pngnq wasn't run, then the THUMB-nq8.png file does not exist. + ;; pngcrush needs an infile and outfile, so we just copy THUMB to + ;; THUMB-nq8.png and use the latter as a temp file. + (when (not image-dired-cmd-pngnq-program) + (let ((temp (cdr (assq ?q spec))) + (thumb (cdr (assq ?t spec)))) + (copy-file thumb temp))) + (let ((process + (apply #'start-process "image-dired-pngcrush" nil + image-dired-cmd-pngcrush-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-pngcrush-options)))) + (setf (process-sentinel process) + (lambda (process status) + (unless (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))) + (when (memq (process-status process) '(exit signal)) + (let ((temp (cdr (assq ?q spec)))) + (delete-file temp))))) + process)) + +(defun image-dired-optipng-thumb (spec) + "Optimize thumbnail described by format SPEC with optipng(1)." + (let ((process + (apply #'start-process "image-dired-optipng" nil + image-dired-cmd-optipng-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-optipng-options)))) + (setf (process-sentinel process) + (lambda (process status) + (unless (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))))) + process)) + +(defun image-dired-create-thumb-1 (original-file thumbnail-file) + "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE." + (image-dired--check-executable-exists + 'image-dired-cmd-create-thumbnail-program) + (let* ((width (int-to-string (image-dired-thumb-size 'width))) + (height (int-to-string (image-dired-thumb-size 'height))) + (modif-time (format-time-string + "%s" (file-attribute-modification-time + (file-attributes original-file)))) + (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png" + thumbnail-file)) + (spec + (list + (cons ?w width) + (cons ?h height) + (cons ?m modif-time) + (cons ?f original-file) + (cons ?q thumbnail-nq8-file) + (cons ?t thumbnail-file))) + (thumbnail-dir (file-name-directory thumbnail-file)) + process) + (when (not (file-exists-p thumbnail-dir)) + (with-file-modes #o700 + (make-directory thumbnail-dir t)) + (message "Thumbnail directory created: %s" thumbnail-dir)) + + ;; Thumbnail file creation processes begin here and are marshaled + ;; in a queue by `image-dired-create-thumb'. + (setq process + (apply #'start-process "image-dired-create-thumbnail" nil + image-dired-cmd-create-thumbnail-program + (mapcar + (lambda (arg) (format-spec arg spec)) + (if (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + image-dired-cmd-create-standard-thumbnail-options + image-dired-cmd-create-thumbnail-options)))) + + (setf (process-sentinel process) + (lambda (process status) + ;; Trigger next in queue once a thumbnail has been created + (cl-decf image-dired-queue-active-jobs) + (image-dired-thumb-queue-run) + (when (= image-dired-queue-active-jobs 0) + (image-dired-debug-message + (format-time-string + "Generated thumbnails in %s.%3N seconds" + (time-subtract nil + image-dired--generate-thumbs-start)))) + (if (not (and (eq (process-status process) 'exit) + (zerop (process-exit-status process)))) + (message "Thumb could not be created for %s: %s" + (abbreviate-file-name original-file) + (string-replace "\n" "" status)) + (set-file-modes thumbnail-file #o600) + (clear-image-cache thumbnail-file) + ;; PNG thumbnail has been created since we are + ;; following the XDG thumbnail spec, so try to optimize + (when (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + (cond + ((and image-dired-cmd-pngnq-program + (executable-find image-dired-cmd-pngnq-program)) + (image-dired-pngnq-thumb spec)) + ((and image-dired-cmd-pngcrush-program + (executable-find image-dired-cmd-pngcrush-program)) + (image-dired-pngcrush-thumb spec)) + ((and image-dired-cmd-optipng-program + (executable-find image-dired-cmd-optipng-program)) + (image-dired-optipng-thumb spec))))))) + process)) + +(defun image-dired-thumb-queue-run () + "Run a queued job if one exists and not too many jobs are running. +Queued items live in `image-dired-queue'." + (while (and image-dired-queue + (< image-dired-queue-active-jobs + image-dired-queue-active-limit)) + (cl-incf image-dired-queue-active-jobs) + (apply #'image-dired-create-thumb-1 (pop image-dired-queue)))) + +(defun image-dired-create-thumb (original-file thumbnail-file) + "Add a job for generating ORIGINAL-FILE thumbnail to `image-dired-queue'. +The new file will be named THUMBNAIL-FILE." + (setq image-dired-queue + (nconc image-dired-queue + (list (list original-file thumbnail-file)))) + (run-at-time 0 nil #'image-dired-thumb-queue-run)) + +(defmacro image-dired--with-marked (&rest body) + "Eval BODY with point on each marked thumbnail. +If no marked file could be found, execute BODY on the current +thumbnail." + `(with-current-buffer image-dired-thumbnail-buffer + (let (found) + (save-mark-and-excursion + (goto-char (point-min)) + (while (not (eobp)) + (when (image-dired-thumb-file-marked-p) + (setq found t) + ,@body) + (forward-char))) + (unless found + ,@body)))) + +;;;###autoload +(defun image-dired-dired-toggle-marked-thumbs (&optional arg) + "Toggle thumbnails in front of file names in the Dired buffer. +If no marked file could be found, insert or hide thumbnails on the +current line. ARG, if non-nil, specifies the files to use instead +of the marked files. If ARG is an integer, use the next ARG (or +previous -ARG, if ARG<0) files." + (interactive "P") + (dired-map-over-marks + (let ((image-pos (dired-move-to-filename)) + (image-file (dired-get-filename nil t)) + thumb-file + overlay) + (when (and image-file + (string-match-p (image-file-name-regexp) image-file)) + (setq thumb-file (image-dired-get-thumbnail-image image-file)) + ;; If image is not already added, then add it. + (let ((thumb-ov (cl-loop for ov in (overlays-in (point) (1+ (point))) + if (overlay-get ov 'thumb-file) return ov))) + (if thumb-ov + (delete-overlay thumb-ov) + (put-image thumb-file image-pos) + (setq overlay + (cl-loop for ov in (overlays-in (point) (1+ (point))) + if (overlay-get ov 'put-image) return ov)) + (overlay-put overlay 'image-file image-file) + (overlay-put overlay 'thumb-file thumb-file))))) + arg ; Show or hide image on ARG next files. + 'show-progress) ; Update dired display after each image is updated. + (add-hook 'dired-after-readin-hook + 'image-dired-dired-after-readin-hook nil t)) + +(defun image-dired-dired-after-readin-hook () + "Relocate existing thumbnail overlays in Dired buffer after reverting. +Move them to their corresponding files if they still exist. +Otherwise, delete overlays." + (mapc (lambda (overlay) + (when (overlay-get overlay 'put-image) + (let* ((image-file (overlay-get overlay 'image-file)) + (image-pos (dired-goto-file image-file))) + (if image-pos + (move-overlay overlay image-pos image-pos) + (delete-overlay overlay))))) + (overlays-in (point-min) (point-max)))) + +(defun image-dired-next-line-and-display () + "Move to next Dired line and display thumbnail image." + (interactive) + (dired-next-line 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-previous-line-and-display () + "Move to previous Dired line and display thumbnail image." + (interactive) + (dired-previous-line 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-toggle-append-browsing () + "Toggle `image-dired-append-when-browsing'." + (interactive) + (setq image-dired-append-when-browsing + (not image-dired-append-when-browsing)) + (message "Append browsing %s" + (if image-dired-append-when-browsing + "on" + "off"))) + +(defun image-dired-mark-and-display-next () + "Mark current file in Dired and display next thumbnail image." + (interactive) + (dired-mark 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-toggle-dired-display-properties () + "Toggle `image-dired-dired-disp-props'." + (interactive) + (setq image-dired-dired-disp-props + (not image-dired-dired-disp-props)) + (message "Dired display properties %s" + (if image-dired-dired-disp-props + "on" + "off"))) + +(defvar image-dired-thumbnail-buffer "*image-dired*" + "Image-Dired's thumbnail buffer.") + +(defun image-dired-create-thumbnail-buffer () + "Create thumb buffer and set `image-dired-thumbnail-mode'." + (let ((buf (get-buffer-create image-dired-thumbnail-buffer))) + (with-current-buffer buf + (setq buffer-read-only t) + (if (not (eq major-mode 'image-dired-thumbnail-mode)) + (image-dired-thumbnail-mode))) + buf)) + +(defvar image-dired-display-image-buffer "*image-dired-display-image*" + "Where larger versions of the images are display.") + +(defvar image-dired-saved-window-configuration nil + "Saved window configuration.") + +;;;###autoload +(defun image-dired-dired-with-window-configuration (dir &optional arg) + "Open directory DIR and create a default window configuration. + +Convenience command that: + + - Opens Dired in folder DIR + - Splits windows in most useful (?) way + - Sets `truncate-lines' to t + +After the command has finished, you would typically mark some +image files in Dired and type +\\[image-dired-display-thumbs] (`image-dired-display-thumbs'). + +If called with prefix argument ARG, skip splitting of windows. + +The current window configuration is saved and can be restored by +calling `image-dired-restore-window-configuration'." + (interactive "DDirectory: \nP") + (let ((buf (image-dired-create-thumbnail-buffer)) + (buf2 (get-buffer-create image-dired-display-image-buffer))) + (setq image-dired-saved-window-configuration + (current-window-configuration)) + (dired dir) + (delete-other-windows) + (when (not arg) + (split-window-right) + (setq truncate-lines t) + (save-excursion + (other-window 1) + (pop-to-buffer-same-window buf) + (select-window (split-window-below)) + (pop-to-buffer-same-window buf2) + (other-window -2))))) + +(defun image-dired-restore-window-configuration () + "Restore window configuration. +Restore any changes to the window configuration made by calling +`image-dired-dired-with-window-configuration'." + (interactive nil image-dired-thumbnail-mode) + (if image-dired-saved-window-configuration + (set-window-configuration image-dired-saved-window-configuration) + (message "No saved window configuration"))) + +(defun image-dired--line-up-with-method () + "Line up thumbnails according to `image-dired-line-up-method'." + (cond ((eq 'dynamic image-dired-line-up-method) + (image-dired-line-up-dynamic)) + ((eq 'fixed image-dired-line-up-method) + (image-dired-line-up)) + ((eq 'interactive image-dired-line-up-method) + (image-dired-line-up-interactive)) + ((eq 'none image-dired-line-up-method) + nil) + (t + (image-dired-line-up-dynamic)))) + +;;;###autoload +(defun image-dired-display-thumbs (&optional arg append do-not-pop) + "Display thumbnails of all marked files, in `image-dired-thumbnail-buffer'. +If a thumbnail image does not exist for a file, it is created on the +fly. With prefix argument ARG, display only thumbnail for file at +point (this is useful if you have marked some files but want to show +another one). + +Recommended usage is to split the current frame horizontally so that +you have the Dired buffer in the left window and the +`image-dired-thumbnail-buffer' buffer in the right window. + +With optional argument APPEND, append thumbnail to thumbnail buffer +instead of erasing it first. + +Optional argument DO-NOT-POP controls if `pop-to-buffer' should be +used or not. If non-nil, use `display-buffer' instead of +`pop-to-buffer'. This is used from functions like +`image-dired-next-line-and-display' and +`image-dired-previous-line-and-display' where we do not want the +thumbnail buffer to be selected." + (interactive "P") + (setq image-dired--generate-thumbs-start (current-time)) + (let ((buf (image-dired-create-thumbnail-buffer)) + thumb-name files dired-buf) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (setq dired-buf (current-buffer)) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (if (not append) + (erase-buffer) + (goto-char (point-max))) + (dolist (curr-file files) + (setq thumb-name (image-dired-thumb-name curr-file)) + (when (not (file-exists-p thumb-name)) + (image-dired-create-thumb curr-file thumb-name)) + (image-dired-insert-thumbnail thumb-name curr-file dired-buf))) + (if do-not-pop + (display-buffer buf) + (pop-to-buffer buf)) + (image-dired--line-up-with-method)))) + +;;;###autoload +(defun image-dired-show-all-from-dir (dir) + "Make a thumbnail buffer for all images in DIR and display it. +Any file matching `image-file-name-regexp' is considered an image +file. + +If the number of image files in DIR exceeds +`image-dired-show-all-from-dir-max-files', ask for confirmation +before creating the thumbnail buffer. If that variable is nil, +never ask for confirmation." + (interactive "DImage-Dired: ") + (dired dir) + (dired-mark-files-regexp (image-file-name-regexp)) + (let ((files (dired-get-marked-files nil nil nil t))) + (cond ((and (null (cdr files))) + (message "No image files in directory")) + ((or (not image-dired-show-all-from-dir-max-files) + (<= (length (cdr files)) image-dired-show-all-from-dir-max-files) + (and (> (length (cdr files)) image-dired-show-all-from-dir-max-files) + (y-or-n-p + (format + "Directory contains more than %d image files. Proceed?" + image-dired-show-all-from-dir-max-files)))) + (image-dired-display-thumbs) + (pop-to-buffer image-dired-thumbnail-buffer) + (setq default-directory dir) + (image-dired-unmark-all-marks)) + (t (message "Image-Dired canceled"))))) + +;;;###autoload +(defalias 'image-dired 'image-dired-show-all-from-dir) + + +;;; Tags + +(defun image-dired-sane-db-file () + "Check if `image-dired-db-file' exists. +If not, try to create it (including any parent directories). +Signal error if there are problems creating it." + (or (file-exists-p image-dired-db-file) + (let (dir buf) + (unless (file-directory-p (setq dir (file-name-directory + image-dired-db-file))) + (with-file-modes #o700 + (make-directory dir t))) + (with-current-buffer (setq buf (create-file-buffer + image-dired-db-file)) + (with-file-modes #o600 + (write-file image-dired-db-file))) + (kill-buffer buf) + (file-exists-p image-dired-db-file)) + (error "Could not create %s" image-dired-db-file))) + +(defvar image-dired-tag-history nil "Variable holding the tag history.") + +(defun image-dired-write-tags (file-tags) + "Write file tags to database. +Write each file and tag in FILE-TAGS to the database. +FILE-TAGS is an alist in the following form: + ((FILE . TAG) ... )" + (image-dired-sane-db-file) + (let (end file tag) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (dolist (elt file-tags) + (setq file (car elt) + tag (cdr elt)) + (goto-char (point-min)) + (if (search-forward-regexp (format "^%s.*$" file) nil t) + (progn + (setq end (point)) + (beginning-of-line) + (when (not (search-forward (format ";%s" tag) end t)) + (end-of-line) + (insert (format ";%s" tag)))) + (goto-char (point-max)) + (insert (format "%s;%s\n" file tag)))) + (save-buffer)))) + +(defun image-dired-remove-tag (files tag) + "For all FILES, remove TAG from the image database." + (image-dired-sane-db-file) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (let (end) + (unless (listp files) + (if (stringp files) + (setq files (list files)) + (error "Files must be a string or a list of strings!"))) + (dolist (file files) + (goto-char (point-min)) + (when (search-forward-regexp (format "^%s;" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (search-forward-regexp + (format "\\(;%s\\)\\($\\|;\\)" tag) end t) + (delete-region (match-beginning 1) (match-end 1)) + ;; Check if file should still be in the database. If + ;; it has no tags or comments, it will be removed. + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (not (search-forward ";" end t)) + (kill-line 1)))))) + (save-buffer))) + +(defun image-dired-list-tags (file) + "Read all tags for image FILE from the image database." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end (tags "")) + (when (search-forward-regexp (format "^%s" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (if (search-forward ";" end t) + (if (search-forward "comment:" end t) + (if (search-forward ";" end t) + (setq tags (buffer-substring (point) end))) + (setq tags (buffer-substring (point) end))))) + (split-string tags ";")))) + +;;;###autoload +(defun image-dired-tag-files (arg) + "Tag marked file(s) in Dired. With prefix ARG, tag file at point." + (interactive "P") + (let ((tag (completing-read + "Tags to add (separate tags with a semicolon): " + image-dired-tag-history nil nil nil 'image-dired-tag-history)) + files) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (image-dired-write-tags + (mapcar + (lambda (x) + (cons x tag)) + files)))) + +(defun image-dired-tag-thumbnail () + "Tag current or marked thumbnails." + (interactive) + (let ((tag (completing-read + "Tags to add (separate tags with a semicolon): " + image-dired-tag-history nil nil nil 'image-dired-tag-history))) + (image-dired--with-marked + (image-dired-write-tags + (list (cons (image-dired-original-file-name) tag))) + (image-dired-update-property + 'tags (image-dired-list-tags (image-dired-original-file-name)))))) + +;;;###autoload +(defun image-dired-delete-tag (arg) + "Remove tag for selected file(s). +With prefix argument ARG, remove tag from file at point." + (interactive "P") + (let ((tag (completing-read "Tag to remove: " image-dired-tag-history + nil nil nil 'image-dired-tag-history)) + files) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (image-dired-remove-tag files tag))) + +(defun image-dired-tag-thumbnail-remove () + "Remove tag from current or marked thumbnails." + (interactive) + (let ((tag (completing-read "Tag to remove: " image-dired-tag-history + nil nil nil 'image-dired-tag-history))) + (image-dired--with-marked + (image-dired-remove-tag (image-dired-original-file-name) tag) + (image-dired-update-property + 'tags (image-dired-list-tags (image-dired-original-file-name)))))) + + +;;; Thumbnail mode (cont.) + +(defun image-dired-original-file-name () + "Get original file name for thumbnail or display image at point." + (get-text-property (point) 'original-file-name)) + +(defun image-dired-file-name-at-point () + "Get abbreviated file name for thumbnail or display image at point." + (let ((f (image-dired-original-file-name))) + (when f + (abbreviate-file-name f)))) + +(defun image-dired-associated-dired-buffer () + "Get associated Dired buffer at point." + (get-text-property (point) 'associated-dired-buffer)) + +(defun image-dired-get-buffer-window (buf) + "Return window where buffer BUF is." + (get-window-with-predicate + (lambda (window) + (equal (window-buffer window) buf)) + nil t)) + +(defun image-dired-track-original-file () + "Track the original file in the associated Dired buffer. +See documentation for `image-dired-toggle-movement-tracking'. +Interactive use only useful if `image-dired-track-movement' is nil." + (interactive) + (let* ((dired-buf (image-dired-associated-dired-buffer)) + (file-name (image-dired-original-file-name)) + (window (image-dired-get-buffer-window dired-buf))) + (and (buffer-live-p dired-buf) file-name + (with-current-buffer dired-buf + (if (not (dired-goto-file file-name)) + (message "Could not track file") + (if window (set-window-point window (point)))))))) + +(defun image-dired-toggle-movement-tracking () + "Turn on and off `image-dired-track-movement'. +Tracking of the movements between thumbnail and Dired buffer so that +they are \"mirrored\" in the dired buffer. When this is on, moving +around in the thumbnail or dired buffer will find the matching +position in the other buffer." + (interactive) + (setq image-dired-track-movement (not image-dired-track-movement)) + (message "Movement tracking %s" (if image-dired-track-movement "on" "off"))) + +(defun image-dired-track-thumbnail () + "Track current Dired file's thumb in `image-dired-thumbnail-buffer'. +This is almost the same as what `image-dired-track-original-file' does, +but the other way around." + (let ((file (dired-get-filename)) + prop-val found window) + (when (get-buffer image-dired-thumbnail-buffer) + (with-current-buffer image-dired-thumbnail-buffer + (goto-char (point-min)) + (while (and (not (eobp)) + (not found)) + (if (and (setq prop-val + (get-text-property (point) 'original-file-name)) + (string= prop-val file)) + (setq found t)) + (if (not found) + (forward-char 1))) + (when found + (if (setq window (image-dired-thumbnail-window)) + (set-window-point window (point))) + (image-dired-update-header-line)))))) + +(defun image-dired-dired-next-line (&optional arg) + "Call `dired-next-line', then track thumbnail. +This can safely replace `dired-next-line'. +With prefix argument, move ARG lines." + (interactive "P") + (dired-next-line (or arg 1)) + (if image-dired-track-movement + (image-dired-track-thumbnail))) + +(defun image-dired-dired-previous-line (&optional arg) + "Call `dired-previous-line', then track thumbnail. +This can safely replace `dired-previous-line'. +With prefix argument, move ARG lines." + (interactive "P") + (dired-previous-line (or arg 1)) + (if image-dired-track-movement + (image-dired-track-thumbnail))) + +(defun image-dired--display-thumb-properties-fun () + (let ((old-buf (current-buffer)) + (old-point (point))) + (lambda () + (when (and (equal (current-buffer) old-buf) + (= (point) old-point)) + (ignore-errors + (image-dired-update-header-line)))))) + +(defun image-dired-forward-image (&optional arg wrap-around) + "Move to next image and display properties. +Optional prefix ARG says how many images to move; the default is +one image. Negative means move backwards. +On reaching end or beginning of buffer, stop and show a message. + +If optional argument WRAP-AROUND is non-nil, wrap around: if +point is on the last image, move to the last one and vice versa." + (interactive "p") + (setq arg (or arg 1)) + (let (pos) + (dotimes (_ (abs arg)) + (if (and (not (if (> arg 0) (eobp) (bobp))) + (save-excursion + (forward-char (if (> arg 0) 1 -1)) + (while (and (not (if (> arg 0) (eobp) (bobp))) + (not (image-dired-image-at-point-p))) + (forward-char (if (> arg 0) 1 -1))) + (setq pos (point)) + (image-dired-image-at-point-p))) + (progn (goto-char pos) + (image-dired-update-header-line)) + (if wrap-around + (progn (goto-char (if (> arg 0) + (point-min) + ;; There are two spaces after the last image. + (- (point-max) 2))) + (image-dired-update-header-line)) + (message "At %s image" (if (> arg 0) "last" "first")) + (run-at-time 1 nil (image-dired--display-thumb-properties-fun)))))) + (when image-dired-track-movement + (image-dired-track-original-file))) + +(defun image-dired-backward-image (&optional arg) + "Move to previous image and display properties. +Optional prefix ARG says how many images to move; the default is +one image. Negative means move forward. +On reaching end or beginning of buffer, stop and show a message." + (interactive "p") + (image-dired-forward-image (- (or arg 1)))) + +(defun image-dired-next-line () + "Move to next line and display properties." + (interactive nil image-dired-thumbnail-mode) + (let ((goal-column (current-column))) + (forward-line 1) + (move-to-column goal-column)) + ;; If we end up in an empty spot, back up to the next thumbnail. + (if (not (image-dired-image-at-point-p)) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + + +(defun image-dired-previous-line () + "Move to previous line and display properties." + (interactive nil image-dired-thumbnail-mode) + (let ((goal-column (current-column))) + (forward-line -1) + (move-to-column goal-column)) + ;; If we end up in an empty spot, back up to the next + ;; thumbnail. This should only happen if the user deleted a + ;; thumbnail and did not refresh, so it is not very common. But we + ;; can handle it in a good manner, so why not? + (if (not (image-dired-image-at-point-p)) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-beginning-of-buffer () + "Move to the first image in the buffer and display properties." + (interactive nil image-dired-thumbnail-mode) + (goto-char (point-min)) + (while (and (not (image-at-point-p)) + (not (eobp))) + (forward-char 1)) + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-end-of-buffer () + "Move to the last image in the buffer and display properties." + (interactive nil image-dired-thumbnail-mode) + (goto-char (point-max)) + (while (and (not (image-at-point-p)) + (not (bobp))) + (forward-char -1)) + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-format-properties-string (buf file props comment) + "Format display properties. +BUF is the associated Dired buffer, FILE is the original image file +name, PROPS is a stringified list of tags and COMMENT is the image file's +comment." + (format-spec + image-dired-display-properties-format + (list + (cons ?b (or buf "")) + (cons ?f file) + (cons ?t (or props "")) + (cons ?c (or comment ""))))) + +(defun image-dired-update-header-line () + "Update image information in the header line." + (when (and (not (eobp)) + (memq major-mode '(image-dired-thumbnail-mode + image-dired-display-image-mode))) + (let ((file-name (file-name-nondirectory (image-dired-original-file-name))) + (dired-buf (buffer-name (image-dired-associated-dired-buffer))) + (props (mapconcat #'identity (get-text-property (point) 'tags) ", ")) + (comment (get-text-property (point) 'comment)) + (message-log-max nil)) + (if file-name + (setq header-line-format + (image-dired-format-properties-string + dired-buf + file-name + props + comment)))))) + +(defun image-dired-dired-file-marked-p (&optional marker) + "In Dired, return t if file on current line is marked. +If optional argument MARKER is non-nil, it is a character to look +for. The default is to look for `dired-marker-char'." + (setq marker (or marker dired-marker-char)) + (save-excursion + (beginning-of-line) + (and (looking-at dired-re-mark) + (= (aref (match-string 0) 0) marker)))) + +(defun image-dired-dired-file-flagged-p () + "In Dired, return t if file on current line is flagged for deletion." + (image-dired-dired-file-marked-p dired-del-marker)) + +(defmacro image-dired--with-thumbnail-buffer (&rest body) + (declare (indent defun) (debug t)) + `(if-let ((buf (get-buffer image-dired-thumbnail-buffer))) + (with-current-buffer buf + (if-let ((win (get-buffer-window buf))) + (with-selected-window win + ,@body) + ,@body)) + (user-error "No such buffer: %s" image-dired-thumbnail-buffer))) + +(defmacro image-dired--on-file-in-dired-buffer (&rest body) + "Run BODY with point on file at point in Dired buffer. +Should be called from commands in `image-dired-thumbnail-mode'." + (declare (indent defun) (debug t)) + `(let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (if (not (and dired-buf file-name)) + (message "No image, or image with correct properties, at point") + (with-current-buffer dired-buf + (when (dired-goto-file file-name) + ,@body + (image-dired-thumb-update-marks)))))) + +(defmacro image-dired--do-mark-command (maybe-next &rest body) + "Helper macro for the mark, unmark and flag commands. +Run BODY in Dired buffer. +If optional argument MAYBE-NEXT is non-nil, show next image +according to `image-dired-marking-shows-next'." + (declare (indent defun) (debug t)) + `(image-dired--with-thumbnail-buffer + (image-dired--on-file-in-dired-buffer + ,@body) + ,(when maybe-next + '(if image-dired-marking-shows-next + (image-dired-display-next-thumbnail-original) + (image-dired-next-line))))) + +(defun image-dired-mark-thumb-original-file () + "Mark original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-mark 1))) + +(defun image-dired-unmark-thumb-original-file () + "Unmark original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-unmark 1))) + +(defun image-dired-flag-thumb-original-file () + "Flag original image file for deletion in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-flag-file-deletion 1))) + +(defun image-dired-toggle-mark-thumb-original-file () + "Toggle mark on original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command nil + (if (image-dired-dired-file-marked-p) + (dired-unmark 1) + (dired-mark 1)))) + +(defun image-dired-unmark-all-marks () + "Remove all marks from all files in associated Dired buffer. +Also update the marks in the thumbnail buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command nil + (dired-unmark-all-marks)) + (image-dired--with-thumbnail-buffer + (image-dired-thumb-update-marks))) + +(defun image-dired-jump-original-dired-buffer () + "Jump to the Dired buffer associated with the current image file. +You probably want to use this together with +`image-dired-track-original-file'." + (interactive nil image-dired-thumbnail-mode) + (let ((buf (image-dired-associated-dired-buffer)) + window frame) + (setq window (image-dired-get-buffer-window buf)) + (if window + (progn + (if (not (equal (selected-frame) (setq frame (window-frame window)))) + (select-frame-set-input-focus frame)) + (select-window window)) + (message "Associated dired buffer not visible")))) + +;;;###autoload +(defun image-dired-jump-thumbnail-buffer () + "Jump to thumbnail buffer." + (interactive) + (let ((window (image-dired-thumbnail-window)) + frame) + (if window + (progn + (if (not (equal (selected-frame) (setq frame (window-frame window)))) + (select-frame-set-input-focus frame)) + (select-window window)) + (message "Thumbnail buffer not visible")))) + +(defvar image-dired-thumbnail-mode-line-up-map + (let ((map (make-sparse-keymap))) + ;; map it to "g" so that the user can press it more quickly + (define-key map "g" #'image-dired-line-up-dynamic) + ;; "f" for "fixed" number of thumbs per row + (define-key map "f" #'image-dired-line-up) + ;; "i" for "interactive" + (define-key map "i" #'image-dired-line-up-interactive) + map) + "Keymap for line-up commands in `image-dired-thumbnail-mode'.") + +(defvar image-dired-thumbnail-mode-tag-map + (let ((map (make-sparse-keymap))) + ;; map it to "t" so that the user can press it more quickly + (define-key map "t" #'image-dired-tag-thumbnail) + ;; "r" for "remove" + (define-key map "r" #'image-dired-tag-thumbnail-remove) + map) + "Keymap for tag commands in `image-dired-thumbnail-mode'.") + +(defvar image-dired-thumbnail-mode-map + (let ((map (make-sparse-keymap))) + (define-key map [right] #'image-dired-forward-image) + (define-key map [left] #'image-dired-backward-image) + (define-key map [up] #'image-dired-previous-line) + (define-key map [down] #'image-dired-next-line) + (define-key map "\C-f" #'image-dired-forward-image) + (define-key map "\C-b" #'image-dired-backward-image) + (define-key map "\C-p" #'image-dired-previous-line) + (define-key map "\C-n" #'image-dired-next-line) + + (define-key map "<" #'image-dired-beginning-of-buffer) + (define-key map ">" #'image-dired-end-of-buffer) + (define-key map (kbd "M-<") #'image-dired-beginning-of-buffer) + (define-key map (kbd "M->") #'image-dired-end-of-buffer) + + (define-key map "d" #'image-dired-flag-thumb-original-file) + (define-key map [delete] #'image-dired-flag-thumb-original-file) + (define-key map "m" #'image-dired-mark-thumb-original-file) + (define-key map "u" #'image-dired-unmark-thumb-original-file) + (define-key map "U" #'image-dired-unmark-all-marks) + (define-key map "." #'image-dired-track-original-file) + (define-key map [tab] #'image-dired-jump-original-dired-buffer) + + ;; add line-up map + (define-key map "g" image-dired-thumbnail-mode-line-up-map) + ;; add tag map + (define-key map "t" image-dired-thumbnail-mode-tag-map) + + (define-key map "\C-m" #'image-dired-display-thumbnail-original-image) + (define-key map [C-return] #'image-dired-thumbnail-display-external) + + (define-key map "L" #'image-dired-rotate-original-left) + (define-key map "R" #'image-dired-rotate-original-right) + + (define-key map "D" #'image-dired-thumbnail-set-image-description) + (define-key map "S" #'image-dired-slideshow-start) + (define-key map "\C-d" #'image-dired-delete-char) + (define-key map " " #'image-dired-display-next-thumbnail-original) + (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) + (define-key map "c" #'image-dired-comment-thumbnail) + + ;; Mouse + (define-key map [mouse-2] #'image-dired-mouse-display-image) + (define-key map [mouse-1] #'image-dired-mouse-select-thumbnail) + (define-key map [mouse-3] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-1] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-2] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-3] #'image-dired-mouse-select-thumbnail) + ;; Seems I must first set C-down-mouse-1 to undefined, or else it + ;; will trigger the buffer menu. If I try to instead bind + ;; C-down-mouse-1 to `image-dired-mouse-toggle-mark', I get a message + ;; about C-mouse-1 not being defined afterwards. Annoying, but I + ;; probably do not completely understand mouse events. + (define-key map [C-down-mouse-1] #'undefined) + (define-key map [C-mouse-1] #'image-dired-mouse-toggle-mark) + map) + "Keymap for `image-dired-thumbnail-mode'.") + +(easy-menu-define image-dired-thumbnail-mode-menu image-dired-thumbnail-mode-map + "Menu for `image-dired-thumbnail-mode'." + '("Image-Dired" + ["Display image" image-dired-display-thumbnail-original-image] + ["Display in external viewer" image-dired-thumbnail-display-external] + ["Jump to Dired buffer" image-dired-jump-original-dired-buffer] + "---" + ["Mark image" image-dired-mark-thumb-original-file] + ["Unmark image" image-dired-unmark-thumb-original-file] + ["Unmark all images" image-dired-unmark-all-marks] + ["Flag for deletion" image-dired-flag-thumb-original-file] + ["Delete marked images" image-dired-delete-marked] + "---" + ["Rotate original right" image-dired-rotate-original-right] + ["Rotate original left" image-dired-rotate-original-left] + "---" + ["Comment thumbnail" image-dired-comment-thumbnail] + ["Tag current or marked thumbnails" image-dired-tag-thumbnail] + ["Remove tag from current or marked thumbnails" + image-dired-tag-thumbnail-remove] + ["Start slideshow" image-dired-slideshow-start] + "---" + ("View Options" + ["Toggle movement tracking" image-dired-toggle-movement-tracking + :style toggle + :selected image-dired-track-movement] + "---" + ["Line up thumbnails" image-dired-line-up] + ["Dynamic line up" image-dired-line-up-dynamic] + ["Refresh thumb" image-dired-refresh-thumb]) + ["Quit" quit-window])) + +(defvar image-dired-display-image-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "S" #'image-dired-slideshow-start) + (define-key map (kbd "SPC") #'image-dired-display-next-thumbnail-original) + (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) + (define-key map "n" #'image-dired-display-next-thumbnail-original) + (define-key map "p" #'image-dired-display-previous-thumbnail-original) + (define-key map "m" #'image-dired-mark-thumb-original-file) + (define-key map "d" #'image-dired-flag-thumb-original-file) + (define-key map "u" #'image-dired-unmark-thumb-original-file) + (define-key map "U" #'image-dired-unmark-all-marks) + ;; Disable keybindings from `image-mode-map' that doesn't make sense here. + (define-key map "o" nil) ; image-save + map) + "Keymap for `image-dired-display-image-mode'.") + +(define-derived-mode image-dired-thumbnail-mode + special-mode "image-dired-thumbnail" + "Browse and manipulate thumbnail images using Dired. +Use `image-dired-minor-mode' to get a nice setup." + :interactive nil + (buffer-disable-undo) + (add-hook 'file-name-at-point-functions 'image-dired-file-name-at-point nil t) + (setq-local window-resize-pixelwise t) + (setq-local bookmark-make-record-function #'image-dired-bookmark-make-record) + ;; Use approximately as much vertical spacing as horizontal. + (setq-local line-spacing (frame-char-width))) + + +;;; Display image mode + +(define-derived-mode image-dired-display-image-mode + image-mode "image-dired-image-display" + "Mode for displaying and manipulating original image. +Resized or in full-size." + :interactive nil + (add-hook 'file-name-at-point-functions #'image-dired-file-name-at-point nil t)) + +(defvar image-dired-minor-mode-map + (let ((map (make-sparse-keymap))) + ;; (set-keymap-parent map dired-mode-map) + ;; Hijack previous and next line movement. Let C-p and C-b be + ;; though... + (define-key map "p" #'image-dired-dired-previous-line) + (define-key map "n" #'image-dired-dired-next-line) + (define-key map [up] #'image-dired-dired-previous-line) + (define-key map [down] #'image-dired-dired-next-line) + + (define-key map (kbd "C-S-n") #'image-dired-next-line-and-display) + (define-key map (kbd "C-S-p") #'image-dired-previous-line-and-display) + (define-key map (kbd "C-S-m") #'image-dired-mark-and-display-next) + + (define-key map "\C-td" #'image-dired-display-thumbs) + (define-key map [tab] #'image-dired-jump-thumbnail-buffer) + (define-key map "\C-ti" #'image-dired-dired-display-image) + (define-key map "\C-tx" #'image-dired-dired-display-external) + (define-key map "\C-ta" #'image-dired-display-thumbs-append) + (define-key map "\C-t." #'image-dired-display-thumb) + (define-key map "\C-tc" #'image-dired-dired-comment-files) + (define-key map "\C-tf" #'image-dired-mark-tagged-files) + map) + "Keymap for `image-dired-minor-mode'.") + +(easy-menu-define image-dired-minor-mode-menu image-dired-minor-mode-map + "Menu for `image-dired-minor-mode'." + '("Image-dired" + ["Display thumb for next file" image-dired-next-line-and-display] + ["Display thumb for previous file" image-dired-previous-line-and-display] + ["Mark and display next" image-dired-mark-and-display-next] + "---" + ["Create thumbnails for marked files" image-dired-create-thumbs] + "---" + ["Display thumbnails append" image-dired-display-thumbs-append] + ["Display this thumbnail" image-dired-display-thumb] + ["Display image" image-dired-dired-display-image] + ["Display in external viewer" image-dired-dired-display-external] + "---" + ["Toggle display properties" image-dired-toggle-dired-display-properties + :style toggle + :selected image-dired-dired-disp-props] + ["Toggle append browsing" image-dired-toggle-append-browsing + :style toggle + :selected image-dired-append-when-browsing] + ["Toggle movement tracking" image-dired-toggle-movement-tracking + :style toggle + :selected image-dired-track-movement] + "---" + ["Jump to thumbnail buffer" image-dired-jump-thumbnail-buffer] + ["Mark tagged files" image-dired-mark-tagged-files] + ["Comment files" image-dired-dired-comment-files] + ["Copy with EXIF file name" image-dired-copy-with-exif-file-name])) + +;;;###autoload +(define-minor-mode image-dired-minor-mode + "Setup easy-to-use keybindings for the commands to be used in Dired mode. +Note that n, p and and will be hijacked and bound to +`image-dired-dired-next-line' and `image-dired-dired-previous-line'." + :keymap image-dired-minor-mode-map) + +(declare-function clear-image-cache "image.c" (&optional filter)) + +(defun image-dired-create-thumbs (&optional arg) + "Create thumbnail images for all marked files in Dired. +With prefix argument ARG, create thumbnails even if they already exist +\(i.e. use this to refresh your thumbnails)." + (interactive "P") + (let (thumb-name) + (dolist (curr-file (dired-get-marked-files)) + (setq thumb-name (image-dired-thumb-name curr-file)) + ;; If the user overrides the exist check, we must clear the + ;; image cache so that if the user wants to display the + ;; thumbnail, it is not fetched from cache. + (when arg + (clear-image-cache (expand-file-name thumb-name))) + (when (or (not (file-exists-p thumb-name)) + arg) + (image-dired-create-thumb curr-file thumb-name))))) + + +;;; Slideshow + +(defcustom image-dired-slideshow-delay 5.0 + "Seconds to wait before showing the next image in a slideshow. +This is used by `image-dired-slideshow-start'." + :type 'float + :version "29.1") + +(define-obsolete-variable-alias 'image-dired-slideshow-timer + 'image-dired--slideshow-timer "29.1") +(defvar image-dired--slideshow-timer nil + "Slideshow timer.") + +(defvar image-dired--slideshow-initial nil) + +(defun image-dired-slideshow-step () + "Step to next image in a slideshow." + (if-let ((buf (get-buffer image-dired-thumbnail-buffer))) + (with-current-buffer buf + (image-dired-display-next-thumbnail-original)) + (image-dired-slideshow-stop))) + +(defun image-dired-slideshow-start (&optional arg) + "Start a slideshow, waiting `image-dired-slideshow-delay' between images. + +With prefix argument ARG, wait that many seconds before going to +the next image. + +With a negative prefix argument, prompt user for the delay." + (interactive "P" image-dired-thumbnail-mode image-dired-display-image-mode) + (let ((delay (if (not arg) + image-dired-slideshow-delay + (if (> arg 0) + arg + (string-to-number + (let ((delay (number-to-string image-dired-slideshow-delay))) + (read-string + (format-prompt "Delay, in seconds. Decimals are accepted" delay)) + delay)))))) + (setq image-dired--slideshow-timer + (run-with-timer + 0 delay + 'image-dired-slideshow-step)) + (add-hook 'post-command-hook 'image-dired-slideshow-stop) + (setq image-dired--slideshow-initial t) + (message "Running slideshow; use any command to stop"))) + +(defun image-dired-slideshow-stop () + "Cancel slideshow." + ;; Make sure we don't immediately stop after + ;; `image-dired-slideshow-start'. + (unless image-dired--slideshow-initial + (remove-hook 'post-command-hook 'image-dired-slideshow-stop) + (cancel-timer image-dired--slideshow-timer)) + (setq image-dired--slideshow-initial nil)) + + +;;; Thumbnail mode (cont. 3) + +(defun image-dired-delete-char () + "Remove current thumbnail from thumbnail buffer and line up." + (interactive nil image-dired-thumbnail-mode) + (let ((inhibit-read-only t)) + (delete-char 1) + (when (= (following-char) ?\s) + (delete-char 1)))) + +;;;###autoload +(defun image-dired-display-thumbs-append () + "Append thumbnails to `image-dired-thumbnail-buffer'." + (interactive) + (image-dired-display-thumbs nil t t)) + +;;;###autoload +(defun image-dired-display-thumb () + "Shorthand for `image-dired-display-thumbs' with prefix argument." + (interactive) + (image-dired-display-thumbs t nil t)) + +(defun image-dired-line-up () + "Line up thumbnails according to `image-dired-thumbs-per-row'. +See also `image-dired-line-up-dynamic'." + (interactive) + (let ((inhibit-read-only t)) + (goto-char (point-min)) + (while (and (not (image-dired-image-at-point-p)) + (not (eobp))) + (delete-char 1)) + (while (not (eobp)) + (forward-char) + (while (and (not (image-dired-image-at-point-p)) + (not (eobp))) + (delete-char 1))) + (goto-char (point-min)) + (let ((seen 0) + (thumb-prev-pos 0) + (thumb-width-chars + (ceiling (/ (+ (* 2 image-dired-thumb-relief) + (* 2 image-dired-thumb-margin) + (image-dired-thumb-size 'width)) + (float (frame-char-width)))))) + (while (not (eobp)) + (forward-char) + (if (= image-dired-thumbs-per-row 1) + (insert "\n") + (cl-incf thumb-prev-pos thumb-width-chars) + (insert (propertize " " 'display `(space :align-to ,thumb-prev-pos))) + (cl-incf seen) + (when (and (= seen (- image-dired-thumbs-per-row 1)) + (not (eobp))) + (forward-char) + (insert "\n") + (setq seen 0) + (setq thumb-prev-pos 0))))) + (goto-char (point-min)))) + +(defun image-dired-line-up-dynamic () + "Line up thumbnails images dynamically. +Calculate how many thumbnails fit." + (interactive) + (let* ((char-width (frame-char-width)) + (width (image-dired-window-width-pixels (image-dired-thumbnail-window))) + (image-dired-thumbs-per-row + (/ width + (+ (* 2 image-dired-thumb-relief) + (* 2 image-dired-thumb-margin) + (image-dired-thumb-size 'width) + char-width)))) + (image-dired-line-up))) + +(defun image-dired-line-up-interactive () + "Line up thumbnails interactively. +Ask user how many thumbnails should be displayed per row." + (interactive) + (let ((image-dired-thumbs-per-row + (string-to-number (read-string "How many thumbs per row: ")))) + (if (not (> image-dired-thumbs-per-row 0)) + (message "Number must be greater than 0") + (image-dired-line-up)))) + +(defun image-dired-thumbnail-display-external () + "Display original image for thumbnail at point using external viewer." + (interactive) + (let ((file (image-dired-original-file-name))) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (if (not file) + (message "No original file name found") + (start-process "image-dired-thumb-external" nil + image-dired-external-viewer file))))) + +;;;###autoload +(defun image-dired-dired-display-external () + "Display file at point using an external viewer." + (interactive) + (let ((file (dired-get-filename))) + (start-process "image-dired-external" nil + image-dired-external-viewer file))) + +(defun image-dired-window-width-pixels (window) + "Calculate WINDOW width in pixels." + (* (window-width window) (frame-char-width))) + +(defun image-dired-display-window () + "Return window where `image-dired-display-image-buffer' is visible." + (get-window-with-predicate + (lambda (window) + (equal (buffer-name (window-buffer window)) image-dired-display-image-buffer)) + nil t)) + +(defun image-dired-thumbnail-window () + "Return window where `image-dired-thumbnail-buffer' is visible." + (get-window-with-predicate + (lambda (window) + (equal (buffer-name (window-buffer window)) image-dired-thumbnail-buffer)) + nil t)) + +(defun image-dired-associated-dired-buffer-window () + "Return window where associated Dired buffer is visible." + (let (buf) + (if (image-dired-image-at-point-p) + (progn + (setq buf (image-dired-associated-dired-buffer)) + (get-window-with-predicate + (lambda (window) + (equal (window-buffer window) buf)))) + (error "No thumbnail image at point")))) + +(defun image-dired-display-image (file &optional _ignored) + "Display image FILE in image buffer. +Use this when you want to display the image, in a new window. +The window will use `image-dired-display-image-mode' which is +based on `image-mode'." + (declare (advertised-calling-convention (file) "29.1")) + (setq file (expand-file-name file)) + (when (not (file-exists-p file)) + (error "No such file: %s" file)) + (let ((buf (get-buffer image-dired-display-image-buffer)) + (cur-win (selected-window))) + (when buf + (kill-buffer buf)) + (when-let ((buf (find-file-noselect file nil t))) + (pop-to-buffer buf) + (rename-buffer image-dired-display-image-buffer) + (image-dired-display-image-mode) + (select-window cur-win)))) + +(defun image-dired-display-thumbnail-original-image (&optional arg) + "Display current thumbnail's original image in display buffer. +See documentation for `image-dired-display-image' for more information. +With prefix argument ARG, display image in its original size." + (interactive "P") + (let ((file (image-dired-original-file-name))) + (if (not (string-equal major-mode "image-dired-thumbnail-mode")) + (message "Not in image-dired-thumbnail-mode") + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (if (not file) + (message "No original file name found") + (image-dired-display-image file arg)))))) + + +;;;###autoload +(defun image-dired-dired-display-image (&optional arg) + "Display current image file. +See documentation for `image-dired-display-image' for more information. +With prefix argument ARG, display image in its original size." + (interactive "P") + (image-dired-display-image (dired-get-filename) arg)) + +(defun image-dired-image-at-point-p () + "Return non-nil if there is an `image-dired' thumbnail at point." + (get-text-property (point) 'image-dired-thumbnail)) + +(defun image-dired-refresh-thumb () + "Force creation of new image for current thumbnail." + (interactive nil image-dired-thumbnail-mode) + (let* ((file (image-dired-original-file-name)) + (thumb (expand-file-name (image-dired-thumb-name file)))) + (clear-image-cache (expand-file-name thumb)) + (image-dired-create-thumb file thumb))) + +(defun image-dired-rotate-original (degrees) + "Rotate original image DEGREES degrees." + (image-dired--check-executable-exists + 'image-dired-cmd-rotate-original-program) + (if (not (image-dired-image-at-point-p)) + (message "No image at point") + (let* ((file (image-dired-original-file-name)) + (spec + (list + (cons ?d degrees) + (cons ?o (expand-file-name file)) + (cons ?t image-dired-temp-rotate-image-file)))) + (unless (eq 'jpeg (image-type file)) + (user-error "Only JPEG images can be rotated")) + (if (not (= 0 (apply #'call-process image-dired-cmd-rotate-original-program + nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-rotate-original-options)))) + (error "Could not rotate image") + (image-dired-display-image image-dired-temp-rotate-image-file) + (if (or (and image-dired-rotate-original-ask-before-overwrite + (y-or-n-p + "Rotate to temp file OK. Overwrite original image? ")) + (not image-dired-rotate-original-ask-before-overwrite)) + (progn + (copy-file image-dired-temp-rotate-image-file file t) + (image-dired-refresh-thumb)) + (image-dired-display-image file)))))) + +(defun image-dired-rotate-original-left () + "Rotate original image left (counter clockwise) 90 degrees. +The result of the rotation is displayed in the image display area +and a confirmation is needed before the original image files is +overwritten. This confirmation can be turned off using +`image-dired-rotate-original-ask-before-overwrite'." + (interactive) + (image-dired-rotate-original "270")) + +(defun image-dired-rotate-original-right () + "Rotate original image right (clockwise) 90 degrees. +The result of the rotation is displayed in the image display area +and a confirmation is needed before the original image files is +overwritten. This confirmation can be turned off using +`image-dired-rotate-original-ask-before-overwrite'." + (interactive) + (image-dired-rotate-original "90")) + + +;;; EXIF support + +(defun image-dired-get-exif-file-name (file) + "Use the image's EXIF information to return a unique file name. +The file name should be unique as long as you do not take more than +one picture per second. The original file name is suffixed at the end +for traceability. The format of the returned file name is +YYYY_MM_DD_HH_MM_DD_ORIG_FILE_NAME.jpg. Used from +`image-dired-copy-with-exif-file-name'." + (let (data no-exif-data-found) + (if (not (eq 'jpeg (image-type (expand-file-name file)))) + (setq no-exif-data-found t + data (format-time-string + "%Y:%m:%d %H:%M:%S" + (file-attribute-modification-time + (file-attributes (expand-file-name file))))) + (setq data (exif-field 'date-time (exif-parse-file + (expand-file-name file))))) + (while (string-match "[ :]" data) + (setq data (replace-match "_" nil nil data))) + (format "%s%s%s" data + (if no-exif-data-found + "_noexif_" + "_") + (file-name-nondirectory file)))) + +(defun image-dired-thumbnail-set-image-description () + "Set the ImageDescription EXIF tag for the original image. +If the image already has a value for this tag, it is used as the +default value at the prompt." + (interactive) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (let* ((file (image-dired-original-file-name)) + (old-value (or (exif-field 'description (exif-parse-file file)) ""))) + (if (eq 0 + (image-dired-set-exif-data file "ImageDescription" + (read-string "Value of ImageDescription: " + old-value))) + (message "Successfully wrote ImageDescription tag") + (error "Could not write ImageDescription tag"))))) + +(defun image-dired-set-exif-data (file tag-name tag-value) + "In FILE, set EXIF tag TAG-NAME to value TAG-VALUE." + (image-dired--check-executable-exists + 'image-dired-cmd-write-exif-data-program) + (let ((spec + (list + (cons ?f (expand-file-name file)) + (cons ?t tag-name) + (cons ?v tag-value)))) + (apply #'call-process image-dired-cmd-write-exif-data-program nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-write-exif-data-options)))) + +(defun image-dired-copy-with-exif-file-name () + "Copy file with unique name to main image directory. +Copy current or all marked files in Dired to a new file in your +main image directory, using a file name generated by +`image-dired-get-exif-file-name'. A typical usage for this if when +copying images from a digital camera into the image directory. + + Typically, you would open up the folder with the incoming +digital images, mark the files to be copied, and execute this +function. The result is a couple of new files in +`image-dired-main-image-directory' called +2005_05_08_12_52_00_dscn0319.jpg, +2005_05_08_14_27_45_dscn0320.jpg etc." + (interactive) + (let (new-name + (files (dired-get-marked-files))) + (mapc + (lambda (curr-file) + (setq new-name + (format "%s/%s" + (file-name-as-directory + (expand-file-name image-dired-main-image-directory)) + (image-dired-get-exif-file-name curr-file))) + (message "Copying %s to %s" curr-file new-name) + (copy-file curr-file new-name)) + files))) + +;;; Thumbnail mode (cont.) + +(defun image-dired-display-next-thumbnail-original (&optional arg) + "Move to the next image in the thumbnail buffer and display it. +With prefix ARG, move that many thumbnails." + (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--with-thumbnail-buffer + (image-dired-forward-image arg t) + (image-dired-display-thumbnail-original-image))) + +(defun image-dired-display-previous-thumbnail-original (arg) + "Move to the previous image in the thumbnail buffer and display it. +With prefix ARG, move that many thumbnails." + (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired-display-next-thumbnail-original (- arg))) + + +;;; Image Comments + +(defun image-dired-write-comments (file-comments) + "Write file comments to database. +Write file comments to one or more files. +FILE-COMMENTS is an alist on the following form: + ((FILE . COMMENT) ... )" + (image-dired-sane-db-file) + (let (end comment-beg-pos comment-end-pos file comment) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (dolist (elt file-comments) + (setq file (car elt) + comment (cdr elt)) + (goto-char (point-min)) + (if (search-forward-regexp (format "^%s.*$" file) nil t) + (progn + (setq end (point)) + (beginning-of-line) + ;; Delete old comment, if any + (when (search-forward ";comment:" end t) + (setq comment-beg-pos (match-beginning 0)) + ;; Any tags after the comment? + (if (search-forward ";" end t) + (setq comment-end-pos (- (point) 1)) + (setq comment-end-pos end)) + ;; Delete comment tag and comment + (delete-region comment-beg-pos comment-end-pos)) + ;; Insert new comment + (beginning-of-line) + (unless (search-forward ";" end t) + (end-of-line) + (insert ";")) + (insert (format "comment:%s;" comment))) + ;; File does not exist in database - add it. + (goto-char (point-max)) + (insert (format "%s;comment:%s\n" file comment)))) + (save-buffer)))) + +(defun image-dired-update-property (prop value) + "Update text property PROP with value VALUE at point." + (let ((inhibit-read-only t)) + (put-text-property + (point) (1+ (point)) + prop + value))) + +;;;###autoload +(defun image-dired-dired-comment-files () + "Add comment to current or marked files in Dired." + (interactive) + (let ((comment (image-dired-read-comment))) + (image-dired-write-comments + (mapcar + (lambda (curr-file) + (cons curr-file comment)) + (dired-get-marked-files))))) + +(defun image-dired-comment-thumbnail () + "Add comment to current thumbnail in thumbnail buffer." + (interactive) + (let* ((file (image-dired-original-file-name)) + (comment (image-dired-read-comment file))) + (image-dired-write-comments (list (cons file comment))) + (image-dired-update-property 'comment comment)) + (image-dired-update-header-line)) + +(defun image-dired-read-comment (&optional file) + "Read comment for an image. +Optionally use old comment from FILE as initial value." + (let ((comment + (read-string + "Comment: " + (if file (image-dired-get-comment file))))) + comment)) + +(defun image-dired-get-comment (file) + "Get comment for file FILE." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end comment-beg-pos comment-end-pos comment) + (when (search-forward-regexp (format "^%s" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (search-forward ";comment:" end t) + (setq comment-beg-pos (point)) + (if (search-forward ";" end t) + (setq comment-end-pos (- (point) 1)) + (setq comment-end-pos end)) + (setq comment (buffer-substring + comment-beg-pos comment-end-pos)))) + comment))) + +;;;###autoload +(defun image-dired-mark-tagged-files (regexp) + "Use REGEXP to mark files with matching tag. +A `tag' is a keyword, a piece of meta data, associated with an +image file and stored in image-dired's database file. This command +lets you input a regexp and this will be matched against all tags +on all image files in the database file. The files that have a +matching tag will be marked in the Dired buffer." + (interactive "sMark tagged files (regexp): ") + (image-dired-sane-db-file) + (let ((hits 0) + files) + (image-dired--with-db-file + ;; Collect matches + (while (search-forward-regexp "\\(^[^;\n]+\\);\\(.*\\)" nil t) + (let ((file (match-string 1)) + (tags (split-string (match-string 2) ";"))) + (when (seq-find (lambda (tag) + (string-match-p regexp tag)) + tags) + (push file files))))) + ;; Mark files + (dolist (curr-file files) + ;; I tried using `dired-mark-files-regexp' but it was waaaay to + ;; slow. Don't bother about hits found in other directories + ;; than the current one. + (when (string= (file-name-as-directory + (expand-file-name default-directory)) + (file-name-as-directory + (file-name-directory curr-file))) + (setq curr-file (file-name-nondirectory curr-file)) + (goto-char (point-min)) + (when (search-forward-regexp (format "\\s %s$" curr-file) nil t) + (setq hits (+ hits 1)) + (dired-mark 1)))) + (message "%d files with matching tag marked" hits))) + + + +;;; Mouse support + +(defun image-dired-mouse-display-image (event) + "Use mouse EVENT, call `image-dired-display-image' to display image. +Track this in associated Dired buffer if `image-dired-track-movement' is +non-nil." + (interactive "e") + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (unless (image-at-point-p) + (image-dired-backward-image)) + (let ((file (image-dired-original-file-name))) + (when file + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-display-image file)))) + +(defun image-dired-mouse-select-thumbnail (event) + "Use mouse EVENT to select thumbnail image. +Track this in associated Dired buffer if `image-dired-track-movement' is +non-nil." + (interactive "e") + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (unless (image-at-point-p) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + + + +;;; Dired marks and tags + +(defun image-dired-thumb-file-marked-p (&optional flagged) + "Check if file is marked in associated Dired buffer. +If optional argument FLAGGED is non-nil, check if file is flagged +for deletion instead." + (let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (when (and dired-buf file-name) + (with-current-buffer dired-buf + (save-excursion + (when (dired-goto-file file-name) + (if flagged + (image-dired-dired-file-flagged-p) + (image-dired-dired-file-marked-p)))))))) + +(defun image-dired-thumb-file-flagged-p () + "Check if file is flagged for deletion in associated Dired buffer." + (image-dired-thumb-file-marked-p t)) + +(defun image-dired-delete-marked () + "Delete current or marked thumbnails and associated images." + (interactive) + (image-dired--with-marked + (image-dired-delete-char) + (unless (bobp) + (backward-char))) + (image-dired--line-up-with-method) + (with-current-buffer (image-dired-associated-dired-buffer) + (dired-do-delete))) + +(defun image-dired-thumb-update-marks () + "Update the marks in the thumbnail buffer." + (when image-dired-thumb-visible-marks + (with-current-buffer image-dired-thumbnail-buffer + (save-mark-and-excursion + (goto-char (point-min)) + (let ((inhibit-read-only t)) + (while (not (eobp)) + (with-silent-modifications + (cond ((image-dired-thumb-file-marked-p) + (add-face-text-property (point) (1+ (point)) + 'image-dired-thumb-mark)) + ((image-dired-thumb-file-flagged-p) + (add-face-text-property (point) (1+ (point)) + 'image-dired-thumb-flagged)) + (t (remove-text-properties (point) (1+ (point)) + '(face image-dired-thumb-mark))))) + (forward-char))))))) + +(defun image-dired-mouse-toggle-mark-1 () + "Toggle Dired mark for current thumbnail. +Track this in associated Dired buffer if +`image-dired-track-movement' is non-nil." + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-toggle-mark-thumb-original-file)) + +(defun image-dired-mouse-toggle-mark (event) + "Use mouse EVENT to toggle Dired mark for thumbnail. +Toggle marks of all thumbnails in region, if it's active. +Track this in associated Dired buffer if +`image-dired-track-movement' is non-nil." + (interactive "e") + (if (use-region-p) + (let ((end (region-end))) + (save-excursion + (goto-char (region-beginning)) + (while (<= (point) end) + (when (image-dired-image-at-point-p) + (image-dired-mouse-toggle-mark-1)) + (forward-char)))) + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (image-dired-mouse-toggle-mark-1)) + (image-dired-thumb-update-marks)) + +(defun image-dired-dired-display-properties () + "Display properties for Dired file in the echo area." + (interactive) + (let* ((file (dired-get-filename)) + (file-name (file-name-nondirectory file)) + (dired-buf (buffer-name (current-buffer))) + (props (mapconcat #'identity (image-dired-list-tags file) ", ")) + (comment (image-dired-get-comment file)) + (message-log-max nil)) + (if file-name + (message "%s" + (image-dired-format-properties-string + dired-buf + file-name + props + comment))))) + + + +;;; Gallery support + +;; TODO: +;; * Support gallery creation when using per-directory thumbnail +;; storage. +;; * Enhanced gallery creation with basic CSS-support and pagination +;; of tag pages with many pictures. + +(defgroup image-dired-gallery nil + "Image-Dired support for generating a HTML gallery." + :prefix "image-dired-" + :group 'image-dired + :version "29.1") + +(defcustom image-dired-gallery-dir + (expand-file-name ".image-dired_gallery" image-dired-dir) + "Directory to store generated gallery html pages. +The name of this directory needs to be \"shared\" to the public +so that it can access the index.html page that image-dired creates." + :type 'directory) + +(defcustom image-dired-gallery-image-root-url + "https://example.org/image-diredpics" + "URL where the full size images are to be found on your web server. +Note that this URL has to be configured on your web server. +Image-Dired expects to find pictures in this directory. +This is used by `image-dired-gallery-generate'." + :type 'string + :version "29.1") + +(defcustom image-dired-gallery-thumb-image-root-url + "https://example.org/image-diredthumbs" + "URL where the thumbnail images are to be found on your web server. +Note that URL path has to be configured on your web server. +Image-Dired expects to find pictures in this directory. +This is used by `image-dired-gallery-generate'." + :type 'string + :version "29.1") + +(defcustom image-dired-gallery-hidden-tags + (list "private" "hidden" "pending") + "List of \"hidden\" tags. +Used by `image-dired-gallery-generate' to leave out \"hidden\" images." + :type '(repeat string)) + +(defvar image-dired-tag-file-list nil + "List to store tag-file structure.") + +(defvar image-dired-file-tag-list nil + "List to store file-tag structure.") + +(defvar image-dired-file-comment-list nil + "List to store file comments.") + +(defun image-dired--add-to-tag-file-lists (tag file) + "Helper function used from `image-dired--create-gallery-lists'. + +Add TAG to FILE in one list and FILE to TAG in the other. + +Lisp structures look like the following: + +image-dired-file-tag-list: + + ((\"filename1\" \"tag1\" \"tag2\" \"tag3\" ...) + (\"filename2\" \"tag1\" \"tag2\" \"tag3\" ...) + ...) + +image-dired-tag-file-list: + + ((\"tag1\" \"filename1\" \"filename2\" \"filename3\" ...) + (\"tag2\" \"filename1\" \"filename2\" \"filename3\" ...) + ...)" + ;; Add tag to file list + (let (curr) + (if image-dired-file-tag-list + (if (setq curr (assoc file image-dired-file-tag-list)) + (setcdr curr (cons tag (cdr curr))) + (setcdr image-dired-file-tag-list + (cons (list file tag) (cdr image-dired-file-tag-list)))) + (setq image-dired-file-tag-list (list (list file tag)))) + ;; Add file to tag list + (if image-dired-tag-file-list + (if (setq curr (assoc tag image-dired-tag-file-list)) + (if (not (member file curr)) + (setcdr curr (cons file (cdr curr)))) + (setcdr image-dired-tag-file-list + (cons (list tag file) (cdr image-dired-tag-file-list)))) + (setq image-dired-tag-file-list (list (list tag file)))))) + +(defun image-dired--add-to-file-comment-list (file comment) + "Helper function used from `image-dired--create-gallery-lists'. + +For FILE, add COMMENT to list. + +Lisp structure looks like the following: + +image-dired-file-comment-list: + + ((\"filename1\" . \"comment1\") + (\"filename2\" . \"comment2\") + ...)" + (if image-dired-file-comment-list + (if (not (assoc file image-dired-file-comment-list)) + (setcdr image-dired-file-comment-list + (cons (cons file comment) + (cdr image-dired-file-comment-list)))) + (setq image-dired-file-comment-list (list (cons file comment))))) + +(defun image-dired--create-gallery-lists () + "Create temporary lists used by `image-dired-gallery-generate'." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end beg file row-tags) + (setq image-dired-tag-file-list nil) + (setq image-dired-file-tag-list nil) + (setq image-dired-file-comment-list nil) + (goto-char (point-min)) + (while (search-forward-regexp "^." nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (setq beg (point)) + (unless (search-forward ";" end nil) + (error "Something is really wrong, check format of database")) + (setq row-tags (split-string + (buffer-substring beg end) ";")) + (setq file (car row-tags)) + (dolist (x (cdr row-tags)) + (if (not (string-match "^comment:\\(.*\\)" x)) + (image-dired--add-to-tag-file-lists x file) + (image-dired--add-to-file-comment-list file (match-string 1 x))))))) + ;; Sort tag-file list + (setq image-dired-tag-file-list + (sort image-dired-tag-file-list + (lambda (x y) + (string< (car x) (car y)))))) + +(defun image-dired--hidden-p (file) + "Return t if image FILE has a \"hidden\" tag." + (cl-loop for tag in (cdr (assoc file image-dired-file-tag-list)) + if (member tag image-dired-gallery-hidden-tags) return t)) + +(defun image-dired-gallery-generate () + "Generate gallery pages. +First we create a couple of Lisp structures from the database to make +it easier to generate, then HTML-files are created in +`image-dired-gallery-dir'." + (interactive) + (if (eq 'per-directory image-dired-thumbnail-storage) + (error "Currently, gallery generation is not supported \ +when using per-directory thumbnail file storage")) + (image-dired--create-gallery-lists) + (let ((tags image-dired-tag-file-list) + (index-file (format "%s/index.html" image-dired-gallery-dir)) + count tag tag-file + comment file-tags tag-link tag-link-list) + ;; Make sure gallery root exist + (if (file-exists-p image-dired-gallery-dir) + (if (not (file-directory-p image-dired-gallery-dir)) + (error "Variable image-dired-gallery-dir is not a directory")) + ;; FIXME: Should we set umask to 077 here, as we do for thumbnails? + (make-directory image-dired-gallery-dir)) + ;; Open index file + (with-temp-file index-file + (if (file-exists-p index-file) + (insert-file-contents index-file)) + (insert "\n") + (insert " \n") + (insert "

Image-Dired Gallery

\n") + (insert (format "

\n Gallery generated %s\n

\n" + (current-time-string))) + (insert "

Tag index

\n") + (setq count 1) + ;; Pre-generate list of all tag links + (dolist (curr tags) + (setq tag (car curr)) + (when (not (member tag image-dired-gallery-hidden-tags)) + (setq tag-link (format "%s" count tag)) + (if tag-link-list + (setq tag-link-list + (append tag-link-list (list (cons tag tag-link)))) + (setq tag-link-list (list (cons tag tag-link)))) + (setq count (1+ count)))) + (setq count 1) + ;; Main loop where we generated thumbnail pages per tag + (dolist (curr tags) + (setq tag (car curr)) + ;; Don't display hidden tags + (when (not (member tag image-dired-gallery-hidden-tags)) + ;; Insert link to tag page in index + (insert (format " %s
\n" (cdr (assoc tag tag-link-list)))) + ;; Open per-tag file + (setq tag-file (format "%s/%s.html" image-dired-gallery-dir count)) + (with-temp-file tag-file + (if (file-exists-p tag-file) + (insert-file-contents tag-file)) + (erase-buffer) + (insert "\n") + (insert " \n") + (insert "

Index

\n") + (insert (format "

Images with tag "%s"

" tag)) + ;; Main loop for files per tag page + (dolist (file (cdr curr)) + (unless (image-dired-hidden-p file) + ;; Insert thumbnail with link to full image + (insert + (format "\n" + image-dired-gallery-image-root-url + (file-name-nondirectory file) + image-dired-gallery-thumb-image-root-url + (file-name-nondirectory (image-dired-thumb-name file)) file)) + ;; Insert comment, if any + (if (setq comment (cdr (assoc file image-dired-file-comment-list))) + (insert (format "
\n%s
\n" comment)) + (insert "
\n")) + ;; Insert links to other tags, if any + (when (> (length + (setq file-tags (assoc file image-dired-file-tag-list))) 2) + (insert "[ ") + (dolist (extra-tag file-tags) + ;; Only insert if not file name or the main tag + (if (and (not (equal extra-tag tag)) + (not (equal extra-tag file))) + (insert + (format "%s " (cdr (assoc extra-tag tag-link-list)))))) + (insert "]
\n")))) + (insert "

Index

\n") + (insert " \n") + (insert "\n")) + (setq count (1+ count)))) + (insert " \n") + (insert "")))) + + +;;; Tag support + +(defvar image-dired-widget-list nil + "List to keep track of meta data in edit buffer.") + +(declare-function widget-forward "wid-edit" (arg)) + +;;;###autoload +(defun image-dired-dired-edit-comment-and-tags () + "Edit comment and tags of current or marked image files. +Edit comment and tags for all marked image files in an +easy-to-use form." + (interactive) + (setq image-dired-widget-list nil) + ;; Setup buffer. + (let ((files (dired-get-marked-files))) + (pop-to-buffer-same-window "*Image-Dired Edit Meta Data*") + (kill-all-local-variables) + (let ((inhibit-read-only t)) + (erase-buffer)) + (remove-overlays) + ;; Some help for the user. + (widget-insert +"\nEdit comments and tags for each image. Separate multiple tags +with a comma. Move forward between fields using TAB or RET. +Move to the previous field using backtab (S-TAB). Save by +activating the Save button at the bottom of the form or cancel +the operation by activating the Cancel button.\n\n") + ;; Here comes all images and a comment and tag field for each + ;; image. + (let (thumb-file img comment-widget tag-widget) + + (dolist (file files) + + (setq thumb-file (image-dired-thumb-name file) + img (create-image thumb-file)) + + (insert-image img) + (widget-insert "\n\nComment: ") + (setq comment-widget + (widget-create 'editable-field + :size 60 + :format "%v " + :value (or (image-dired-get-comment file) ""))) + (widget-insert "\nTags: ") + (setq tag-widget + (widget-create 'editable-field + :size 60 + :format "%v " + :value (or (mapconcat + #'identity + (image-dired-list-tags file) + ",") ""))) + ;; Save information in all widgets so that we can use it when + ;; the user saves the form. + (setq image-dired-widget-list + (append image-dired-widget-list + (list (list file comment-widget tag-widget)))) + (widget-insert "\n\n"))) + + ;; Footer with Save and Cancel button. + (widget-insert "\n") + (widget-create 'push-button + :notify + (lambda (&rest _ignore) + (image-dired-save-information-from-widgets) + (bury-buffer) + (message "Done")) + "Save") + (widget-insert " ") + (widget-create 'push-button + :notify + (lambda (&rest _ignore) + (bury-buffer) + (message "Operation canceled")) + "Cancel") + (widget-insert "\n") + (use-local-map widget-keymap) + (widget-setup) + ;; Jump to the first widget. + (widget-forward 1))) + +(defun image-dired-save-information-from-widgets () + "Save information found in `image-dired-widget-list'. +Use the information in `image-dired-widget-list' to save comments and +tags to their respective image file. Internal function used by +`image-dired-dired-edit-comment-and-tags'." + (let (file comment tag-string tag-list lst) + (image-dired-write-comments + (mapcar + (lambda (widget) + (setq file (car widget) + comment (widget-value (cadr widget))) + (cons file comment)) + image-dired-widget-list)) + (image-dired-write-tags + (dolist (widget image-dired-widget-list lst) + (setq file (car widget) + tag-string (widget-value (car (cddr widget))) + tag-list (split-string tag-string ",")) + (dolist (tag tag-list) + (push (cons file tag) lst)))))) + + +;;; bookmark.el support + +(declare-function bookmark-make-record-default + "bookmark" (&optional no-file no-context posn)) +(declare-function bookmark-prop-get "bookmark" (bookmark prop)) + +(defun image-dired-bookmark-name () + "Create a default bookmark name for the current EWW buffer." + (file-name-nondirectory + (directory-file-name + (file-name-directory (image-dired-original-file-name))))) + +(defun image-dired-bookmark-make-record () + "Create a bookmark for the current EWW buffer." + `(,(image-dired-bookmark-name) + ,@(bookmark-make-record-default t) + (location . ,(file-name-directory (image-dired-original-file-name))) + (image-dired-file . ,(file-name-nondirectory (image-dired-original-file-name))) + (handler . image-dired-bookmark-jump))) + +;;;###autoload +(defun image-dired-bookmark-jump (bookmark) + "Default bookmark handler for Image-Dired buffers." + ;; User already cached thumbnails, so disable any checking. + (let ((image-dired-show-all-from-dir-max-files nil)) + (image-dired (bookmark-prop-get bookmark 'location)) + ;; TODO: Go to the bookmarked file, if it exists. + ;; (bookmark-prop-get bookmark 'image-dired-file) + (goto-char (point-min)))) + +(put 'image-dired-bookmark-jump 'bookmark-handler-type "Image-Dired") + +;;; Obsolete + +;;;###autoload +(define-obsolete-function-alias 'tumme #'image-dired "24.4") + +;;;###autoload +(define-obsolete-function-alias 'image-dired-setup-dired-keybindings + #'image-dired-minor-mode "26.1") + +(defcustom image-dired-temp-image-file + (expand-file-name ".image-dired_temp" image-dired-dir) + "Name of temporary image file used by various commands." + :type 'file) +(make-obsolete-variable 'image-dired-temp-image-file + "no longer used." "29.1") + +(defcustom image-dired-cmd-create-temp-image-program + (if (executable-find "gm") "gm" "convert") + "Executable used to create temporary image. +Used together with `image-dired-cmd-create-temp-image-options'." + :type 'file + :version "29.1") +(make-obsolete-variable 'image-dired-cmd-create-temp-image-program + "no longer used." "29.1") + +(defcustom image-dired-cmd-create-temp-image-options + (let ((opts '("-size" "%wx%h" "%f[0]" + "-resize" "%wx%h>" + "-strip" "jpeg:%t"))) + (if (executable-find "gm") (cons "convert" opts) opts)) + "Options of command used to create temporary image for display window. +Used together with `image-dired-cmd-create-temp-image-program', +Available format specifiers are: %w and %h which are replaced by +the calculated max size for width and height in the image display window, +%f which is replaced by the file name of the original image and %t which +is replaced by the file name of the temporary file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-create-temp-image-options + "no longer used." "29.1") + +(defcustom image-dired-display-window-width-correction 1 + "Number to be used to correct image display window width. +Change if the default (1) does not work (i.e. if the image does not +completely fit)." + :type 'integer) +(make-obsolete-variable 'image-dired-display-window-width-correction + "no longer used." "29.1") + +(defcustom image-dired-display-window-height-correction 0 + "Number to be used to correct image display window height. +Change if the default (0) does not work (i.e. if the image does not +completely fit)." + :type 'integer) +(make-obsolete-variable 'image-dired-display-window-height-correction + "no longer used." "29.1") + +(defun image-dired-display-window-width (window) + "Return width, in pixels, of WINDOW." + (declare (obsolete nil "29.1")) + (- (image-dired-window-width-pixels window) + image-dired-display-window-width-correction)) + +(defun image-dired-display-window-height (window) + "Return height, in pixels, of WINDOW." + (declare (obsolete nil "29.1")) + (- (image-dired-window-height-pixels window) + image-dired-display-window-height-correction)) + +(defun image-dired-window-height-pixels (window) + "Calculate WINDOW height in pixels." + (declare (obsolete nil "29.1")) + ;; Note: The mode-line consumes one line + (* (- (window-height window) 1) (frame-char-height))) + +(defcustom image-dired-cmd-read-exif-data-program "exiftool" + "Program used to read EXIF data to image. +Used together with `image-dired-cmd-read-exif-data-options'." + :type 'file) +(make-obsolete-variable 'image-dired-cmd-read-exif-data-program + "use `exif-parse-file' and `exif-field' instead." "29.1") + +(defcustom image-dired-cmd-read-exif-data-options '("-s" "-s" "-s" "-%t" "%f") + "Arguments of command used to read EXIF data. +Used with `image-dired-cmd-read-exif-data-program'. +Available format specifiers are: %f which is replaced +by the image file name and %t which is replaced by the tag name." + :version "26.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-read-exif-data-options + "use `exif-parse-file' and `exif-field' instead." "29.1") + +(defun image-dired-get-exif-data (file tag-name) + "From FILE, return EXIF tag TAG-NAME." + (declare (obsolete "use `exif-parse-file' and `exif-field' instead." "29.1")) + (image-dired--check-executable-exists + 'image-dired-cmd-read-exif-data-program) + (let ((buf (get-buffer-create "*image-dired-get-exif-data*")) + (spec (list (cons ?f file) (cons ?t tag-name))) + tag-value) + (with-current-buffer buf + (delete-region (point-min) (point-max)) + (if (not (eq (apply #'call-process image-dired-cmd-read-exif-data-program + nil t nil + (mapcar + (lambda (arg) (format-spec arg spec)) + image-dired-cmd-read-exif-data-options)) + 0)) + (error "Could not get EXIF tag") + (goto-char (point-min)) + ;; Clean buffer from newlines and carriage returns before + ;; getting final info + (while (search-forward-regexp "[\n\r]" nil t) + (replace-match "" nil t)) + (setq tag-value (buffer-substring (point-min) (point-max))))) + tag-value)) + +(defcustom image-dired-cmd-rotate-thumbnail-program + (if (executable-find "gm") "gm" "mogrify") + "Executable used to rotate thumbnail. +Used together with `image-dired-cmd-rotate-thumbnail-options'." + :type 'file + :version "29.1") +(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-program nil "29.1") + +(defcustom image-dired-cmd-rotate-thumbnail-options + (let ((opts '("-rotate" "%d" "%t"))) + (if (executable-find "gm") (cons "mogrify" opts) opts)) + "Arguments of command used to rotate thumbnail image. +Used with `image-dired-cmd-rotate-thumbnail-program'. +Available format specifiers are: %d which is replaced by the +number of (positive) degrees to rotate the image, normally 90 or 270 +\(for 90 degrees right and left), %t which is replaced by the file name +of the thumbnail file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-options nil "29.1") + +(defun image-dired-rotate-thumbnail (degrees) + "Rotate thumbnail DEGREES degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (image-dired--check-executable-exists + 'image-dired-cmd-rotate-thumbnail-program) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (let* ((file (image-dired-thumb-name (image-dired-original-file-name))) + (thumb (expand-file-name file)) + (spec (list (cons ?d degrees) (cons ?t thumb)))) + (apply #'call-process image-dired-cmd-rotate-thumbnail-program nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-rotate-thumbnail-options)) + (clear-image-cache thumb)))) + +(defun image-dired-rotate-thumbnail-left () + "Rotate thumbnail left (counter clockwise) 90 degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (interactive) + (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) + (image-dired-rotate-thumbnail "270"))) + +(defun image-dired-rotate-thumbnail-right () + "Rotate thumbnail counter right (clockwise) 90 degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (interactive) + (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) + (image-dired-rotate-thumbnail "90"))) + +(defun image-dired-modify-mark-on-thumb-original-file (command) + "Modify mark in Dired buffer. +COMMAND is one of `mark' for marking file in Dired, `unmark' for +unmarking file in Dired or `flag' for flagging file for delete in +Dired." + (declare (obsolete image-dired--on-file-in-dired-buffer "29.1")) + (let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (if (not (and dired-buf file-name)) + (message "No image, or image with correct properties, at point") + (with-current-buffer dired-buf + (message "%s" file-name) + (when (dired-goto-file file-name) + (cond ((eq command 'mark) (dired-mark 1)) + ((eq command 'unmark) (dired-unmark 1)) + ((eq command 'toggle) + (if (image-dired-dired-file-marked-p) + (dired-unmark 1) + (dired-mark 1))) + ((eq command 'flag) (dired-flag-file-deletion 1))) + (image-dired-thumb-update-marks)))))) + +(defun image-dired-display-current-image-full () + "Display current image in full size." + (declare (obsolete image-transform-original "29.1")) + (interactive nil image-dired-thumbnail-mode) + (let ((file (image-dired-original-file-name))) + (if file + (progn + (image-dired-display-image file) + (with-current-buffer image-dired-display-image-buffer + (image-transform-original))) + (error "No original file name at point")))) + +(defun image-dired-display-current-image-sized () + "Display current image in sized to fit window dimensions." + (declare (obsolete image-mode-fit-frame "29.1")) + (interactive nil image-dired-thumbnail-mode) + (let ((file (image-dired-original-file-name))) + (if file + (progn + (image-dired-display-image file)) + (error "No original file name at point")))) + +(defun image-dired-add-to-tag-file-list (tag file) + "Add relation between TAG and FILE." + (declare (obsolete nil "29.1")) + (let (curr) + (if image-dired-tag-file-list + (if (setq curr (assoc tag image-dired-tag-file-list)) + (if (not (member file curr)) + (setcdr curr (cons file (cdr curr)))) + (setcdr image-dired-tag-file-list + (cons (list tag file) (cdr image-dired-tag-file-list)))) + (setq image-dired-tag-file-list (list (list tag file)))))) + +(defun image-dired-display-thumb-properties () + "Display thumbnail properties in the echo area." + (declare (obsolete image-dired-update-header-line "29.1")) + (image-dired-update-header-line)) + +(defvar image-dired-slideshow-count 0 + "Keeping track on number of images in slideshow.") +(make-obsolete-variable 'image-dired-slideshow-count "no longer used." "29.1") + +(defvar image-dired-slideshow-times 0 + "Number of pictures to display in slideshow.") +(make-obsolete-variable 'image-dired-slideshow-times "no longer used." "29.1") + +(define-obsolete-function-alias 'image-dired-create-display-image-buffer + #'ignore "29.1") +(define-obsolete-function-alias 'image-dired-create-gallery-lists + #'image-dired--create-gallery-lists "29.1") +(define-obsolete-function-alias 'image-dired-add-to-file-comment-list + #'image-dired--add-to-file-comment-list "29.1") +(define-obsolete-function-alias 'image-dired-add-to-tag-file-lists + #'image-dired--add-to-tag-file-lists "29.1") +(define-obsolete-function-alias 'image-dired-hidden-p + #'image-dired--hidden-p "29.1") + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;; TEST-SECTION ;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; (defvar image-dired-dir-max-size 12300000) + +;; (defun image-dired-test-clean-old-files () +;; "Clean `image-dired-dir' from old thumbnail files. +;; \"Oldness\" measured using last access time. If the total size of all +;; thumbnail files in `image-dired-dir' is larger than 'image-dired-dir-max-size', +;; old files are deleted until the max size is reached." +;; (let* ((files +;; (sort +;; (mapcar +;; (lambda (f) +;; (let ((fattribs (file-attributes f))) +;; `(,(file-attribute-access-time fattribs) +;; ,(file-attribute-size fattribs) ,f))) +;; (directory-files (image-dired-dir) t ".+\\.thumb\\..+$")) +;; ;; Sort function. Compare time between two files. +;; (lambda (l1 l2) +;; (time-less-p (car l1) (car l2))))) +;; (dirsize (apply '+ (mapcar (lambda (x) (cadr x)) files)))) +;; (while (> dirsize image-dired-dir-max-size) +;; (y-or-n-p +;; (format "Size of thumbnail directory: %d, delete old file %s? " +;; dirsize (cadr (cdar files)))) +;; (delete-file (cadr (cdar files))) +;; (setq dirsize (- dirsize (car (cdar files)))) +;; (setq files (cdr files))))) + +(provide 'image-dired) + +;;; image-dired.el ends here diff --git a/lisp/image/image-dired-util.el b/lisp/image/image-dired-util.el new file mode 100644 index 0000000000..9f12354111 --- /dev/null +++ b/lisp/image/image-dired-util.el @@ -0,0 +1,3080 @@ +;;; image-dired.el --- use dired to browse and manipulate your images -*- lexical-binding: t -*- + +;; Copyright (C) 2005-2022 Free Software Foundation, Inc. + +;; Version: 0.4.11 +;; Keywords: multimedia +;; Author: Mathias Dahl + +;; 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: + +;; BACKGROUND +;; ========== +;; +;; I needed a program to browse, organize and tag my pictures. I got +;; tired of the old gallery program I used as it did not allow +;; multi-file operations easily. Also, it put things out of my +;; control. Image viewing programs I tested did not allow multi-file +;; operations or did not do what I wanted it to. +;; +;; So, I got the idea to use the wonderful functionality of Emacs and +;; `dired' to do it. It would allow me to do almost anything I wanted, +;; which is basically just to browse all my pictures in an easy way, +;; letting me manipulate and tag them in various ways. `dired' already +;; provide all the file handling and navigation facilities; I only +;; needed to add some functions to display the images. +;; +;; I briefly tried out thumbs.el, and although it seemed more +;; powerful than this package, it did not work the way I wanted to. It +;; was too slow to create thumbnails of all files in a directory (I +;; currently keep all my 2000+ images in the same directory) and +;; browsing the thumbnail buffer was slow too. image-dired.el will not +;; create thumbnails until they are needed and the browsing is done +;; quickly and easily in Dired. I copied a great deal of ideas and +;; code from there though... :) +;; +;; `image-dired' stores the thumbnail files in `image-dired-dir' +;; using the file name format ORIGNAME.thumb.ORIGEXT. For example +;; ~/.emacs.d/image-dired/myimage01.thumb.jpg. The "database" is for +;; now just a plain text file with the following format: +;; +;; file-name-non-directory;comment:comment-text;tag1;tag2;tag3;...;tagN +;; +;; +;; PREREQUISITES +;; ============= +;; +;; * The GraphicsMagick or ImageMagick package; Image-Dired uses +;; whichever is available. +;; +;; A) For GraphicsMagick, `gm' is used. +;; Find it here: http://www.graphicsmagick.org/ +;; +;; B) For ImageMagick, `convert' and `mogrify' are used. +;; Find it here: https://www.imagemagick.org. +;; +;; * For non-lossy rotation of JPEG images, the JpegTRAN program is +;; needed. +;; +;; * For `image-dired-set-exif-data' to work, the command line tool `exiftool' is +;; needed. It can be found here: https://exiftool.org/. This +;; function is, among other things, used for writing comments to +;; image files using `image-dired-thumbnail-set-image-description'. +;; +;; +;; USAGE +;; ===== +;; +;; This information has been moved to the manual. Type `C-h r' to open +;; the Emacs manual and go to the node Thumbnails by typing `g +;; Image-Dired RET'. +;; +;; Quickstart: M-x image-dired RET DIRNAME RET +;; +;; where DIRNAME is a directory containing image files. +;; +;; LIMITATIONS +;; =========== +;; +;; * Supports all image formats that Emacs and convert supports, but +;; the thumbnails are hard-coded to JPEG or PNG format. It uses +;; JPEG by default, but can optionally follow the Thumbnail Managing +;; Standard (v0.9.0, Dec 2020), which mandates PNG. See the user +;; option `image-dired-thumbnail-storage'. +;; +;; * WARNING: The "database" format used might be changed so keep a +;; backup of `image-dired-db-file' when testing new versions. +;; +;; TODO +;; ==== +;; +;; * Investigate if it is possible to also write the tags to the image +;; files. +;; +;; * From thumbs.el: Add an option for clean-up/max-size functionality +;; for thumbnail directory. +;; +;; * From thumbs.el: Add setroot function. +;; +;; * Add `image-dired-display-thumbs-ring' and functions to cycle that. Find out +;; which is best, saving old batch just before inserting new, or +;; saving the current batch in the ring when inserting it. Adding +;; it probably needs rewriting `image-dired-display-thumbs' to be more general. +;; +;; * Find some way of toggling on and off really nice keybindings in +;; Dired (for example, using C-n or instead of C-S-n). +;; Richard suggested that we could keep C-t as prefix for +;; image-dired commands as it is currently not used in Dired. He +;; also suggested that `dired-next-line' and `dired-previous-line' +;; figure out if image-dired is enabled in the current buffer and, +;; if it is, call `image-dired-dired-next-line' and `image-dired-dired-previous-line', +;; respectively. Update: This is partly done; some bindings have +;; now been added to Dired. +;; +;; * In some way keep track of buffers and windows and stuff so that +;; it works as the user expects. +;; +;; * More/better documentation. + +;;; Code: + +(require 'dired) +(require 'exif) +(require 'image-mode) +(require 'widget) +(require 'xdg) + +(eval-when-compile + (require 'cl-lib) + (require 'wid-edit)) + + +;;; Customizable variables + +(defgroup image-dired nil + "Use Dired to browse your images as thumbnails, and more." + :prefix "image-dired-" + :link '(info-link "(emacs) Image-Dired") + :group 'multimedia) + +(defcustom image-dired-dir (locate-user-emacs-file "image-dired/") + "Directory where thumbnail images are stored. + +The value of this option will be ignored if Image-Dired is +customized to use the Thumbnail Managing Standard; they will be +saved in \"$XDG_CACHE_HOME/thumbnails/\" instead. See +`image-dired-thumbnail-storage'." + :type 'directory) + +(defcustom image-dired-thumbnail-storage 'use-image-dired-dir + "How `image-dired' stores thumbnail files. +There are two ways that Image-Dired can store and generate +thumbnails. If you set this variable to one of the two following +values, they will be stored in the JPEG format: + +- `use-image-dired-dir' means that the thumbnails are stored in a + central directory. + +- `per-directory' means that each thumbnail is stored in a + subdirectory called \".image-dired\" in the same directory + where the image file is. + +It can also use the \"Thumbnail Managing Standard\", which allows +sharing of thumbnails across different programs. Thumbnails will +be stored in \"$XDG_CACHE_HOME/thumbnails/\" instead of in +`image-dired-dir'. Thumbnails are saved in the PNG format, and +can be one of the following sizes: + +- `standard' means use thumbnails sized 128x128. +- `standard-large' means use thumbnails sized 256x256. +- `standard-x-large' means use thumbnails sized 512x512. +- `standard-xx-large' means use thumbnails sized 1024x1024. + +For more information on the Thumbnail Managing Standard, see: +https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html" + :type '(choice :tag "How to store thumbnail files" + (const :tag "Use image-dired-dir" use-image-dired-dir) + (const :tag "Thumbnail Managing Standard (normal 128x128)" + standard) + (const :tag "Thumbnail Managing Standard (large 256x256)" + standard-large) + (const :tag "Thumbnail Managing Standard (larger 512x512)" + standard-x-large) + (const :tag "Thumbnail Managing Standard (extra large 1024x1024)" + standard-xx-large) + (const :tag "Per-directory" per-directory)) + :version "29.1") + +(defconst image-dired--thumbnail-standard-sizes + '( standard standard-large + standard-x-large standard-xx-large) + "List of symbols representing thumbnail sizes in Thumbnail Managing Standard.") + +(defcustom image-dired-db-file + (expand-file-name ".image-dired_db" image-dired-dir) + "Database file where file names and their associated tags are stored." + :type 'file) + +(defcustom image-dired-cmd-create-thumbnail-program + (if (executable-find "gm") "gm" "convert") + "Executable used to create thumbnail. +Used together with `image-dired-cmd-create-thumbnail-options'." + :type 'file + :version "29.1") + +(defcustom image-dired-cmd-create-thumbnail-options + (let ((opts '("-size" "%wx%h" "%f[0]" + "-resize" "%wx%h>" + "-strip" "jpeg:%t"))) + (if (executable-find "gm") (cons "convert" opts) opts)) + "Options of command used to create thumbnail image. +Used with `image-dired-cmd-create-thumbnail-program'. +Available format specifiers are: %w which is replaced by +`image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', +%f which is replaced by the file name of the original image and %t +which is replaced by the file name of the thumbnail file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-pngnq-program + ;; Prefer pngquant to pngnq-s9 as it is faster on my machine. + ;; The project also seems more active than the alternatives. + ;; Prefer pngnq-s9 to pngnq as it fixes bugs in pngnq. + ;; The pngnq project seems dead (?) since 2011 or so. + (or (executable-find "pngquant") + (executable-find "pngnq-s9") + (executable-find "pngnq")) + "The file name of the `pngquant' or `pngnq' program. +It quantizes colors of PNG images down to 256 colors or fewer +using the NeuQuant algorithm." + :version "29.1" + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-pngnq-options + (if (executable-find "pngquant") + '("--ext" "-nq8.png" "%t") ; same extension as "pngnq" + '("-f" "%t")) + "Arguments to pass `image-dired-cmd-pngnq-program'. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options'." + :type '(repeat (string :tag "Argument")) + :version "29.1") + +(defcustom image-dired-cmd-pngcrush-program (executable-find "pngcrush") + "The file name of the `pngcrush' program. +It optimizes the compression of PNG images. Also it adds PNG textual chunks +with the information required by the Thumbnail Managing Standard." + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-pngcrush-options + `("-q" + "-text" "b" "Description" "Thumbnail of file://%f" + "-text" "b" "Software" ,(emacs-version) + ;; "-text b \"Thumb::Image::Height\" \"%oh\" " + ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" " + ;; "-text b \"Thumb::Image::Width\" \"%ow\" " + "-text" "b" "Thumb::MTime" "%m" + ;; "-text b \"Thumb::Size\" \"%b\" " + "-text" "b" "Thumb::URI" "file://%f" + "%q" "%t") + "Arguments for `image-dired-cmd-pngcrush-program'. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options', with %q for a +temporary file name (typically generated by pnqnq)." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-optipng-program (executable-find "optipng") + "The file name of the `optipng' program." + :version "26.1" + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-optipng-options '("-o5" "%t") + "Arguments passed to `image-dired-cmd-optipng-program'. +Available format specifiers are described in +`image-dired-cmd-create-thumbnail-options'." + :version "26.1" + :type '(repeat (string :tag "Argument")) + :link '(url-link "man:optipng(1)")) + +(defcustom image-dired-cmd-create-standard-thumbnail-options + (append '("-size" "%wx%h" "%f[0]") + (unless (or image-dired-cmd-pngcrush-program + image-dired-cmd-pngnq-program) + (list + "-set" "Thumb::MTime" "%m" + "-set" "Thumb::URI" "file://%f" + "-set" "Description" "Thumbnail of file://%f" + "-set" "Software" (emacs-version))) + '("-thumbnail" "%wx%h>" "png:%t")) + "Options for creating thumbnails according to the Thumbnail Managing Standard. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options', with %m for file modification time." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-rotate-original-program + "jpegtran" + "Executable used to rotate original image. +Used together with `image-dired-cmd-rotate-original-options'." + :type 'file) + +(defcustom image-dired-cmd-rotate-original-options + '("-rotate" "%d" "-copy" "all" "-outfile" "%t" "%o") + "Arguments of command used to rotate original image. +Used with `image-dired-cmd-rotate-original-program'. +Available format specifiers are: %d which is replaced by the +number of (positive) degrees to rotate the image, normally 90 or +270 \(for 90 degrees right and left), %o which is replaced by the +original image file name and %t which is replaced by +`image-dired-temp-image-file'." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-temp-rotate-image-file + (expand-file-name ".image-dired_rotate_temp" image-dired-dir) + "Temporary file for rotate operations." + :type 'file) + +(defcustom image-dired-rotate-original-ask-before-overwrite t + "Confirm overwrite of original file after rotate operation. +If non-nil, ask user for confirmation before overwriting the +original file with `image-dired-temp-rotate-image-file'." + :type 'boolean) + +(defcustom image-dired-cmd-write-exif-data-program + "exiftool" + "Program used to write EXIF data to image. +Used together with `image-dired-cmd-write-exif-data-options'." + :type 'file) + +(defcustom image-dired-cmd-write-exif-data-options + '("-%t=%v" "%f") + "Arguments of command used to write EXIF data. +Used with `image-dired-cmd-write-exif-data-program'. +Available format specifiers are: %f which is replaced by +the image file name, %t which is replaced by the tag name and %v +which is replaced by the tag value." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-thumb-size + (cond + ((eq 'standard image-dired-thumbnail-storage) 128) + ((eq 'standard-large image-dired-thumbnail-storage) 256) + ((eq 'standard-x-large image-dired-thumbnail-storage) 512) + ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) + (t 100)) + "Size of thumbnails, in pixels. +This is the default size for both `image-dired-thumb-width' +and `image-dired-thumb-height'. + +The value of this option will be ignored if Image-Dired is +customized to use the Thumbnail Managing Standard; the standard +sizes will be used instead. See `image-dired-thumbnail-storage'." + :type 'integer) + +(defcustom image-dired-thumb-width image-dired-thumb-size + "Width of thumbnails, in pixels." + :type 'integer) + +(defcustom image-dired-thumb-height image-dired-thumb-size + "Height of thumbnails, in pixels." + :type 'integer) + +(defcustom image-dired-thumb-relief 2 + "Size of button-like border around thumbnails." + :type 'integer) + +(defcustom image-dired-thumb-margin 2 + "Size of the margin around thumbnails. +This is where you see the cursor." + :type 'integer) + +(defcustom image-dired-thumb-visible-marks t + "Make marks and flags visible in thumbnail buffer. +If non-nil, apply the `image-dired-thumb-mark' face to marked +images and `image-dired-thumb-flagged' to images flagged for +deletion." + :type 'boolean + :version "28.1") + +(defface image-dired-thumb-mark + '((((class color) (min-colors 16)) :background "DarkOrange") + (((class color)) :foreground "yellow")) + "Face for marked images in thumbnail buffer." + :version "29.1") + +(defface image-dired-thumb-flagged + '((((class color) (min-colors 88) (background light)) :background "Red3") + (((class color) (min-colors 88) (background dark)) :background "Pink") + (((class color) (min-colors 16) (background light)) :background "Red3") + (((class color) (min-colors 16) (background dark)) :background "Pink") + (((class color) (min-colors 8)) :background "red") + (t :inverse-video t)) + "Face for images flagged for deletion in thumbnail buffer." + :version "29.1") + +(defcustom image-dired-line-up-method 'dynamic + "Default method for line-up of thumbnails in thumbnail buffer. +Used by `image-dired-display-thumbs' and other functions that needs +to line-up thumbnails. Dynamic means to use the available width of +the window containing the thumbnail buffer, Fixed means to use +`image-dired-thumbs-per-row', Interactive is for asking the user, +and No line-up means that no automatic line-up will be done." + :type '(choice :tag "Default line-up method" + (const :tag "Dynamic" dynamic) + (const :tag "Fixed" fixed) + (const :tag "Interactive" interactive) + (const :tag "No line-up" none))) + +(defcustom image-dired-thumbs-per-row 3 + "Number of thumbnails to display per row in thumb buffer." + :type 'integer) + +(defcustom image-dired-track-movement t + "The current state of the tracking and mirroring. +For more information, see the documentation for +`image-dired-toggle-movement-tracking'." + :type 'boolean) + +(defcustom image-dired-append-when-browsing nil + "Append thumbnails in thumbnail buffer when browsing. +If non-nil, using `image-dired-next-line-and-display' and +`image-dired-previous-line-and-display' will leave a trail of thumbnail +images in the thumbnail buffer. If you enable this and want to clean +the thumbnail buffer because it is filled with too many thumbnails, +just call `image-dired-display-thumb' to display only the image at point. +This value can be toggled using `image-dired-toggle-append-browsing'." + :type 'boolean) + +(defcustom image-dired-dired-disp-props t + "If non-nil, display properties for Dired file when browsing. +Used by `image-dired-next-line-and-display', +`image-dired-previous-line-and-display' and `image-dired-mark-and-display-next'. +If the database file is large, this can slow down image browsing in +Dired and you might want to turn it off." + :type 'boolean) + +(defcustom image-dired-display-properties-format "%b: %f (%t): %c" + "Display format for thumbnail properties. +%b is replaced with associated Dired buffer name, %f with file +name (without path) of original image file, %t with the list of +tags and %c with the comment." + :type 'string) + +(defcustom image-dired-external-viewer + ;; TODO: Use mailcap, dired-guess-shell-alist-default, + ;; dired-view-command-alist. + (cond ((executable-find "display")) + ((executable-find "xli")) + ((executable-find "qiv") "qiv -t") + ((executable-find "feh") "feh")) + "Name of external viewer. +Including parameters. Used when displaying original image from +`image-dired-thumbnail-mode'." + :version "28.1" + :type '(choice string + (const :tag "Not Set" nil))) + +(defcustom image-dired-main-image-directory + (or (xdg-user-dir "PICTURES") "~/pics/") + "Name of main image directory, if any. +Used by `image-dired-copy-with-exif-file-name'." + :type 'string + :version "29.1") + +(defcustom image-dired-show-all-from-dir-max-files 500 + "Maximum number of files in directory before prompting. + +If there are more image files than this in a selected directory, +the `image-dired-show-all-from-dir' command will ask for +confirmation before creating the thumbnail buffer. If this +variable is nil, it will never ask." + :type '(choice integer + (const :tag "Disable warning" nil)) + :version "29.1") + +(defcustom image-dired-marking-shows-next t + "If non-nil, marking, unmarking or flagging an image shows the next image. + +This affects the following commands: +\\ + `image-dired-flag-thumb-original-file' (bound to \\[image-dired-flag-thumb-original-file]) + `image-dired-mark-thumb-original-file' (bound to \\[image-dired-mark-thumb-original-file]) + `image-dired-unmark-thumb-original-file' (bound to \\[image-dired-unmark-thumb-original-file])" + :type 'boolean + :version "29.1") + + +;;; Util functions + +(defvar image-dired-debug nil + "Non-nil means enable debug messages.") + +(defun image-dired-debug-message (&rest args) + "Display debug message ARGS when `image-dired-debug' is non-nil." + (when image-dired-debug + (apply #'message args))) + +(defmacro image-dired--with-db-file (&rest body) + "Run BODY in a temp buffer containing `image-dired-db-file'. +Return the last form in BODY." + (declare (indent 0) (debug t)) + `(with-temp-buffer + (if (file-exists-p image-dired-db-file) + (insert-file-contents image-dired-db-file)) + ,@body)) + +(defun image-dired-dir () + "Return the current thumbnail directory (from variable `image-dired-dir'). +Create the thumbnail directory if it does not exist." + (let ((image-dired-dir (file-name-as-directory + (expand-file-name image-dired-dir)))) + (unless (file-directory-p image-dired-dir) + (with-file-modes #o700 + (make-directory image-dired-dir t)) + (message "Thumbnail directory created: %s" image-dired-dir)) + image-dired-dir)) + +(defun image-dired-insert-image (file type relief margin) + "Insert image FILE of image TYPE, using RELIEF and MARGIN, at point." + (let ((i `(image :type ,type + :file ,file + :relief ,relief + :margin ,margin))) + (insert-image i))) + +(defun image-dired-get-thumbnail-image (file) + "Return the image descriptor for a thumbnail of image file FILE." + (unless (string-match-p (image-file-name-regexp) file) + (error "%s is not a valid image file" file)) + (let* ((thumb-file (image-dired-thumb-name file)) + (thumb-attr (file-attributes thumb-file))) + (when (or (not thumb-attr) + (time-less-p (file-attribute-modification-time thumb-attr) + (file-attribute-modification-time + (file-attributes file)))) + (image-dired-create-thumb file thumb-file)) + (create-image thumb-file))) + +(defun image-dired-insert-thumbnail (file original-file-name + associated-dired-buffer) + "Insert thumbnail image FILE. +Add text properties ORIGINAL-FILE-NAME and ASSOCIATED-DIRED-BUFFER." + (let (beg end) + (setq beg (point)) + (image-dired-insert-image + file + ;; Thumbnails are created asynchronously, so we might not yet + ;; have a file. But if it exists, it might have been cached from + ;; before and we should use it instead of our current settings. + (or (and (file-exists-p file) + (image-type-from-file-header file)) + (and (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + 'png) + 'jpeg) + image-dired-thumb-relief + image-dired-thumb-margin) + (setq end (point)) + (add-text-properties + beg end + (list 'image-dired-thumbnail t + 'original-file-name original-file-name + 'associated-dired-buffer associated-dired-buffer + 'tags (image-dired-list-tags original-file-name) + 'mouse-face 'highlight + 'comment (image-dired-get-comment original-file-name))))) + +(defun image-dired-thumb-name (file) + "Return absolute file name for thumbnail FILE. +Depending on the value of `image-dired-thumbnail-storage', the +file name of the thumbnail will vary: +- For `use-image-dired-dir', make a SHA1-hash of the image file's + directory name and add that to make the thumbnail file name + unique. +- For `per-directory' storage, just add a subdirectory. +- For `standard' storage, produce the file name according to the + Thumbnail Managing Standard. Among other things, an MD5-hash + of the image file's directory name will be added to the + filename. +See also `image-dired-thumbnail-storage'." + (cond ((memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + (let ((thumbdir (cl-case image-dired-thumbnail-storage + (standard "thumbnails/normal") + (standard-large "thumbnails/large") + (standard-x-large "thumbnails/x-large") + (standard-xx-large "thumbnails/xx-large")))) + (expand-file-name + ;; MD5 is mandated by the Thumbnail Managing Standard. + (concat (md5 (concat "file://" (expand-file-name file))) ".png") + (expand-file-name thumbdir (xdg-cache-home))))) + ((eq 'use-image-dired-dir image-dired-thumbnail-storage) + (let* ((f (expand-file-name file)) + (hash + (md5 (file-name-as-directory (file-name-directory f))))) + (format "%s%s%s.thumb.%s" + (file-name-as-directory (expand-file-name (image-dired-dir))) + (file-name-base f) + (if hash (concat "_" hash) "") + (file-name-extension f)))) + ((eq 'per-directory image-dired-thumbnail-storage) + (let ((f (expand-file-name file))) + (format "%s.image-dired/%s.thumb.%s" + (file-name-directory f) + (file-name-base f) + (file-name-extension f)))))) + +(defun image-dired--check-executable-exists (executable) + (unless (executable-find (symbol-value executable)) + (error "Executable %S not found" executable))) + + +;;; Creating thumbnails + +(defun image-dired-thumb-size (dimension) + "Return thumb size depending on `image-dired-thumbnail-storage'. +DIMENSION should be either the symbol `width' or `height'." + (cond + ((eq 'standard image-dired-thumbnail-storage) 128) + ((eq 'standard-large image-dired-thumbnail-storage) 256) + ((eq 'standard-x-large image-dired-thumbnail-storage) 512) + ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) + (t (cl-ecase dimension + (width image-dired-thumb-width) + (height image-dired-thumb-height))))) + +(defvar image-dired--generate-thumbs-start nil + "Time when `display-thumbs' was called.") + +(defvar image-dired-queue nil + "List of items in the queue. +Each item has the form (ORIGINAL-FILE TARGET-FILE).") + +(defvar image-dired-queue-active-jobs 0 + "Number of active jobs in `image-dired-queue'.") + +(defvar image-dired-queue-active-limit (min 4 (max 2 (/ (num-processors) 2))) + "Maximum number of concurrent jobs permitted for generating images. +Increase at own risk. If you want to experiment with this, +consider setting `image-dired-debug' to a non-nil value to see +the time spent on generating thumbnails. Run `image-clear-cache' +and remove the cached thumbnail files between each trial run.") + +(defun image-dired-pngnq-thumb (spec) + "Quantize thumbnail described by format SPEC with pngnq(1)." + (let ((process + (apply #'start-process "image-dired-pngnq" nil + image-dired-cmd-pngnq-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-pngnq-options)))) + (setf (process-sentinel process) + (lambda (process status) + (if (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + ;; Pass off to pngcrush, or just rename the + ;; THUMB-nq8.png file back to THUMB.png + (if (and image-dired-cmd-pngcrush-program + (executable-find image-dired-cmd-pngcrush-program)) + (image-dired-pngcrush-thumb spec) + (let ((nq8 (cdr (assq ?q spec))) + (thumb (cdr (assq ?t spec)))) + (rename-file nq8 thumb t))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))))) + process)) + +(defun image-dired-pngcrush-thumb (spec) + "Optimize thumbnail described by format SPEC with pngcrush(1)." + ;; If pngnq wasn't run, then the THUMB-nq8.png file does not exist. + ;; pngcrush needs an infile and outfile, so we just copy THUMB to + ;; THUMB-nq8.png and use the latter as a temp file. + (when (not image-dired-cmd-pngnq-program) + (let ((temp (cdr (assq ?q spec))) + (thumb (cdr (assq ?t spec)))) + (copy-file thumb temp))) + (let ((process + (apply #'start-process "image-dired-pngcrush" nil + image-dired-cmd-pngcrush-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-pngcrush-options)))) + (setf (process-sentinel process) + (lambda (process status) + (unless (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))) + (when (memq (process-status process) '(exit signal)) + (let ((temp (cdr (assq ?q spec)))) + (delete-file temp))))) + process)) + +(defun image-dired-optipng-thumb (spec) + "Optimize thumbnail described by format SPEC with optipng(1)." + (let ((process + (apply #'start-process "image-dired-optipng" nil + image-dired-cmd-optipng-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-optipng-options)))) + (setf (process-sentinel process) + (lambda (process status) + (unless (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))))) + process)) + +(defun image-dired-create-thumb-1 (original-file thumbnail-file) + "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE." + (image-dired--check-executable-exists + 'image-dired-cmd-create-thumbnail-program) + (let* ((width (int-to-string (image-dired-thumb-size 'width))) + (height (int-to-string (image-dired-thumb-size 'height))) + (modif-time (format-time-string + "%s" (file-attribute-modification-time + (file-attributes original-file)))) + (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png" + thumbnail-file)) + (spec + (list + (cons ?w width) + (cons ?h height) + (cons ?m modif-time) + (cons ?f original-file) + (cons ?q thumbnail-nq8-file) + (cons ?t thumbnail-file))) + (thumbnail-dir (file-name-directory thumbnail-file)) + process) + (when (not (file-exists-p thumbnail-dir)) + (with-file-modes #o700 + (make-directory thumbnail-dir t)) + (message "Thumbnail directory created: %s" thumbnail-dir)) + + ;; Thumbnail file creation processes begin here and are marshaled + ;; in a queue by `image-dired-create-thumb'. + (setq process + (apply #'start-process "image-dired-create-thumbnail" nil + image-dired-cmd-create-thumbnail-program + (mapcar + (lambda (arg) (format-spec arg spec)) + (if (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + image-dired-cmd-create-standard-thumbnail-options + image-dired-cmd-create-thumbnail-options)))) + + (setf (process-sentinel process) + (lambda (process status) + ;; Trigger next in queue once a thumbnail has been created + (cl-decf image-dired-queue-active-jobs) + (image-dired-thumb-queue-run) + (when (= image-dired-queue-active-jobs 0) + (image-dired-debug-message + (format-time-string + "Generated thumbnails in %s.%3N seconds" + (time-subtract nil + image-dired--generate-thumbs-start)))) + (if (not (and (eq (process-status process) 'exit) + (zerop (process-exit-status process)))) + (message "Thumb could not be created for %s: %s" + (abbreviate-file-name original-file) + (string-replace "\n" "" status)) + (set-file-modes thumbnail-file #o600) + (clear-image-cache thumbnail-file) + ;; PNG thumbnail has been created since we are + ;; following the XDG thumbnail spec, so try to optimize + (when (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + (cond + ((and image-dired-cmd-pngnq-program + (executable-find image-dired-cmd-pngnq-program)) + (image-dired-pngnq-thumb spec)) + ((and image-dired-cmd-pngcrush-program + (executable-find image-dired-cmd-pngcrush-program)) + (image-dired-pngcrush-thumb spec)) + ((and image-dired-cmd-optipng-program + (executable-find image-dired-cmd-optipng-program)) + (image-dired-optipng-thumb spec))))))) + process)) + +(defun image-dired-thumb-queue-run () + "Run a queued job if one exists and not too many jobs are running. +Queued items live in `image-dired-queue'." + (while (and image-dired-queue + (< image-dired-queue-active-jobs + image-dired-queue-active-limit)) + (cl-incf image-dired-queue-active-jobs) + (apply #'image-dired-create-thumb-1 (pop image-dired-queue)))) + +(defun image-dired-create-thumb (original-file thumbnail-file) + "Add a job for generating ORIGINAL-FILE thumbnail to `image-dired-queue'. +The new file will be named THUMBNAIL-FILE." + (setq image-dired-queue + (nconc image-dired-queue + (list (list original-file thumbnail-file)))) + (run-at-time 0 nil #'image-dired-thumb-queue-run)) + +(defmacro image-dired--with-marked (&rest body) + "Eval BODY with point on each marked thumbnail. +If no marked file could be found, execute BODY on the current +thumbnail." + `(with-current-buffer image-dired-thumbnail-buffer + (let (found) + (save-mark-and-excursion + (goto-char (point-min)) + (while (not (eobp)) + (when (image-dired-thumb-file-marked-p) + (setq found t) + ,@body) + (forward-char))) + (unless found + ,@body)))) + +;;;###autoload +(defun image-dired-dired-toggle-marked-thumbs (&optional arg) + "Toggle thumbnails in front of file names in the Dired buffer. +If no marked file could be found, insert or hide thumbnails on the +current line. ARG, if non-nil, specifies the files to use instead +of the marked files. If ARG is an integer, use the next ARG (or +previous -ARG, if ARG<0) files." + (interactive "P") + (dired-map-over-marks + (let ((image-pos (dired-move-to-filename)) + (image-file (dired-get-filename nil t)) + thumb-file + overlay) + (when (and image-file + (string-match-p (image-file-name-regexp) image-file)) + (setq thumb-file (image-dired-get-thumbnail-image image-file)) + ;; If image is not already added, then add it. + (let ((thumb-ov (cl-loop for ov in (overlays-in (point) (1+ (point))) + if (overlay-get ov 'thumb-file) return ov))) + (if thumb-ov + (delete-overlay thumb-ov) + (put-image thumb-file image-pos) + (setq overlay + (cl-loop for ov in (overlays-in (point) (1+ (point))) + if (overlay-get ov 'put-image) return ov)) + (overlay-put overlay 'image-file image-file) + (overlay-put overlay 'thumb-file thumb-file))))) + arg ; Show or hide image on ARG next files. + 'show-progress) ; Update dired display after each image is updated. + (add-hook 'dired-after-readin-hook + 'image-dired-dired-after-readin-hook nil t)) + +(defun image-dired-dired-after-readin-hook () + "Relocate existing thumbnail overlays in Dired buffer after reverting. +Move them to their corresponding files if they still exist. +Otherwise, delete overlays." + (mapc (lambda (overlay) + (when (overlay-get overlay 'put-image) + (let* ((image-file (overlay-get overlay 'image-file)) + (image-pos (dired-goto-file image-file))) + (if image-pos + (move-overlay overlay image-pos image-pos) + (delete-overlay overlay))))) + (overlays-in (point-min) (point-max)))) + +(defun image-dired-next-line-and-display () + "Move to next Dired line and display thumbnail image." + (interactive) + (dired-next-line 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-previous-line-and-display () + "Move to previous Dired line and display thumbnail image." + (interactive) + (dired-previous-line 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-toggle-append-browsing () + "Toggle `image-dired-append-when-browsing'." + (interactive) + (setq image-dired-append-when-browsing + (not image-dired-append-when-browsing)) + (message "Append browsing %s" + (if image-dired-append-when-browsing + "on" + "off"))) + +(defun image-dired-mark-and-display-next () + "Mark current file in Dired and display next thumbnail image." + (interactive) + (dired-mark 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-toggle-dired-display-properties () + "Toggle `image-dired-dired-disp-props'." + (interactive) + (setq image-dired-dired-disp-props + (not image-dired-dired-disp-props)) + (message "Dired display properties %s" + (if image-dired-dired-disp-props + "on" + "off"))) + +(defvar image-dired-thumbnail-buffer "*image-dired*" + "Image-Dired's thumbnail buffer.") + +(defun image-dired-create-thumbnail-buffer () + "Create thumb buffer and set `image-dired-thumbnail-mode'." + (let ((buf (get-buffer-create image-dired-thumbnail-buffer))) + (with-current-buffer buf + (setq buffer-read-only t) + (if (not (eq major-mode 'image-dired-thumbnail-mode)) + (image-dired-thumbnail-mode))) + buf)) + +(defvar image-dired-display-image-buffer "*image-dired-display-image*" + "Where larger versions of the images are display.") + +(defvar image-dired-saved-window-configuration nil + "Saved window configuration.") + +;;;###autoload +(defun image-dired-dired-with-window-configuration (dir &optional arg) + "Open directory DIR and create a default window configuration. + +Convenience command that: + + - Opens Dired in folder DIR + - Splits windows in most useful (?) way + - Sets `truncate-lines' to t + +After the command has finished, you would typically mark some +image files in Dired and type +\\[image-dired-display-thumbs] (`image-dired-display-thumbs'). + +If called with prefix argument ARG, skip splitting of windows. + +The current window configuration is saved and can be restored by +calling `image-dired-restore-window-configuration'." + (interactive "DDirectory: \nP") + (let ((buf (image-dired-create-thumbnail-buffer)) + (buf2 (get-buffer-create image-dired-display-image-buffer))) + (setq image-dired-saved-window-configuration + (current-window-configuration)) + (dired dir) + (delete-other-windows) + (when (not arg) + (split-window-right) + (setq truncate-lines t) + (save-excursion + (other-window 1) + (pop-to-buffer-same-window buf) + (select-window (split-window-below)) + (pop-to-buffer-same-window buf2) + (other-window -2))))) + +(defun image-dired-restore-window-configuration () + "Restore window configuration. +Restore any changes to the window configuration made by calling +`image-dired-dired-with-window-configuration'." + (interactive nil image-dired-thumbnail-mode) + (if image-dired-saved-window-configuration + (set-window-configuration image-dired-saved-window-configuration) + (message "No saved window configuration"))) + +(defun image-dired--line-up-with-method () + "Line up thumbnails according to `image-dired-line-up-method'." + (cond ((eq 'dynamic image-dired-line-up-method) + (image-dired-line-up-dynamic)) + ((eq 'fixed image-dired-line-up-method) + (image-dired-line-up)) + ((eq 'interactive image-dired-line-up-method) + (image-dired-line-up-interactive)) + ((eq 'none image-dired-line-up-method) + nil) + (t + (image-dired-line-up-dynamic)))) + +;;;###autoload +(defun image-dired-display-thumbs (&optional arg append do-not-pop) + "Display thumbnails of all marked files, in `image-dired-thumbnail-buffer'. +If a thumbnail image does not exist for a file, it is created on the +fly. With prefix argument ARG, display only thumbnail for file at +point (this is useful if you have marked some files but want to show +another one). + +Recommended usage is to split the current frame horizontally so that +you have the Dired buffer in the left window and the +`image-dired-thumbnail-buffer' buffer in the right window. + +With optional argument APPEND, append thumbnail to thumbnail buffer +instead of erasing it first. + +Optional argument DO-NOT-POP controls if `pop-to-buffer' should be +used or not. If non-nil, use `display-buffer' instead of +`pop-to-buffer'. This is used from functions like +`image-dired-next-line-and-display' and +`image-dired-previous-line-and-display' where we do not want the +thumbnail buffer to be selected." + (interactive "P") + (setq image-dired--generate-thumbs-start (current-time)) + (let ((buf (image-dired-create-thumbnail-buffer)) + thumb-name files dired-buf) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (setq dired-buf (current-buffer)) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (if (not append) + (erase-buffer) + (goto-char (point-max))) + (dolist (curr-file files) + (setq thumb-name (image-dired-thumb-name curr-file)) + (when (not (file-exists-p thumb-name)) + (image-dired-create-thumb curr-file thumb-name)) + (image-dired-insert-thumbnail thumb-name curr-file dired-buf))) + (if do-not-pop + (display-buffer buf) + (pop-to-buffer buf)) + (image-dired--line-up-with-method)))) + +;;;###autoload +(defun image-dired-show-all-from-dir (dir) + "Make a thumbnail buffer for all images in DIR and display it. +Any file matching `image-file-name-regexp' is considered an image +file. + +If the number of image files in DIR exceeds +`image-dired-show-all-from-dir-max-files', ask for confirmation +before creating the thumbnail buffer. If that variable is nil, +never ask for confirmation." + (interactive "DImage-Dired: ") + (dired dir) + (dired-mark-files-regexp (image-file-name-regexp)) + (let ((files (dired-get-marked-files nil nil nil t))) + (cond ((and (null (cdr files))) + (message "No image files in directory")) + ((or (not image-dired-show-all-from-dir-max-files) + (<= (length (cdr files)) image-dired-show-all-from-dir-max-files) + (and (> (length (cdr files)) image-dired-show-all-from-dir-max-files) + (y-or-n-p + (format + "Directory contains more than %d image files. Proceed?" + image-dired-show-all-from-dir-max-files)))) + (image-dired-display-thumbs) + (pop-to-buffer image-dired-thumbnail-buffer) + (setq default-directory dir) + (image-dired-unmark-all-marks)) + (t (message "Image-Dired canceled"))))) + +;;;###autoload +(defalias 'image-dired 'image-dired-show-all-from-dir) + + +;;; Tags + +(defun image-dired-sane-db-file () + "Check if `image-dired-db-file' exists. +If not, try to create it (including any parent directories). +Signal error if there are problems creating it." + (or (file-exists-p image-dired-db-file) + (let (dir buf) + (unless (file-directory-p (setq dir (file-name-directory + image-dired-db-file))) + (with-file-modes #o700 + (make-directory dir t))) + (with-current-buffer (setq buf (create-file-buffer + image-dired-db-file)) + (with-file-modes #o600 + (write-file image-dired-db-file))) + (kill-buffer buf) + (file-exists-p image-dired-db-file)) + (error "Could not create %s" image-dired-db-file))) + +(defvar image-dired-tag-history nil "Variable holding the tag history.") + +(defun image-dired-write-tags (file-tags) + "Write file tags to database. +Write each file and tag in FILE-TAGS to the database. +FILE-TAGS is an alist in the following form: + ((FILE . TAG) ... )" + (image-dired-sane-db-file) + (let (end file tag) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (dolist (elt file-tags) + (setq file (car elt) + tag (cdr elt)) + (goto-char (point-min)) + (if (search-forward-regexp (format "^%s.*$" file) nil t) + (progn + (setq end (point)) + (beginning-of-line) + (when (not (search-forward (format ";%s" tag) end t)) + (end-of-line) + (insert (format ";%s" tag)))) + (goto-char (point-max)) + (insert (format "%s;%s\n" file tag)))) + (save-buffer)))) + +(defun image-dired-remove-tag (files tag) + "For all FILES, remove TAG from the image database." + (image-dired-sane-db-file) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (let (end) + (unless (listp files) + (if (stringp files) + (setq files (list files)) + (error "Files must be a string or a list of strings!"))) + (dolist (file files) + (goto-char (point-min)) + (when (search-forward-regexp (format "^%s;" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (search-forward-regexp + (format "\\(;%s\\)\\($\\|;\\)" tag) end t) + (delete-region (match-beginning 1) (match-end 1)) + ;; Check if file should still be in the database. If + ;; it has no tags or comments, it will be removed. + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (not (search-forward ";" end t)) + (kill-line 1)))))) + (save-buffer))) + +(defun image-dired-list-tags (file) + "Read all tags for image FILE from the image database." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end (tags "")) + (when (search-forward-regexp (format "^%s" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (if (search-forward ";" end t) + (if (search-forward "comment:" end t) + (if (search-forward ";" end t) + (setq tags (buffer-substring (point) end))) + (setq tags (buffer-substring (point) end))))) + (split-string tags ";")))) + +;;;###autoload +(defun image-dired-tag-files (arg) + "Tag marked file(s) in Dired. With prefix ARG, tag file at point." + (interactive "P") + (let ((tag (completing-read + "Tags to add (separate tags with a semicolon): " + image-dired-tag-history nil nil nil 'image-dired-tag-history)) + files) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (image-dired-write-tags + (mapcar + (lambda (x) + (cons x tag)) + files)))) + +(defun image-dired-tag-thumbnail () + "Tag current or marked thumbnails." + (interactive) + (let ((tag (completing-read + "Tags to add (separate tags with a semicolon): " + image-dired-tag-history nil nil nil 'image-dired-tag-history))) + (image-dired--with-marked + (image-dired-write-tags + (list (cons (image-dired-original-file-name) tag))) + (image-dired-update-property + 'tags (image-dired-list-tags (image-dired-original-file-name)))))) + +;;;###autoload +(defun image-dired-delete-tag (arg) + "Remove tag for selected file(s). +With prefix argument ARG, remove tag from file at point." + (interactive "P") + (let ((tag (completing-read "Tag to remove: " image-dired-tag-history + nil nil nil 'image-dired-tag-history)) + files) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (image-dired-remove-tag files tag))) + +(defun image-dired-tag-thumbnail-remove () + "Remove tag from current or marked thumbnails." + (interactive) + (let ((tag (completing-read "Tag to remove: " image-dired-tag-history + nil nil nil 'image-dired-tag-history))) + (image-dired--with-marked + (image-dired-remove-tag (image-dired-original-file-name) tag) + (image-dired-update-property + 'tags (image-dired-list-tags (image-dired-original-file-name)))))) + + +;;; Thumbnail mode (cont.) + +(defun image-dired-original-file-name () + "Get original file name for thumbnail or display image at point." + (get-text-property (point) 'original-file-name)) + +(defun image-dired-file-name-at-point () + "Get abbreviated file name for thumbnail or display image at point." + (let ((f (image-dired-original-file-name))) + (when f + (abbreviate-file-name f)))) + +(defun image-dired-associated-dired-buffer () + "Get associated Dired buffer at point." + (get-text-property (point) 'associated-dired-buffer)) + +(defun image-dired-get-buffer-window (buf) + "Return window where buffer BUF is." + (get-window-with-predicate + (lambda (window) + (equal (window-buffer window) buf)) + nil t)) + +(defun image-dired-track-original-file () + "Track the original file in the associated Dired buffer. +See documentation for `image-dired-toggle-movement-tracking'. +Interactive use only useful if `image-dired-track-movement' is nil." + (interactive) + (let* ((dired-buf (image-dired-associated-dired-buffer)) + (file-name (image-dired-original-file-name)) + (window (image-dired-get-buffer-window dired-buf))) + (and (buffer-live-p dired-buf) file-name + (with-current-buffer dired-buf + (if (not (dired-goto-file file-name)) + (message "Could not track file") + (if window (set-window-point window (point)))))))) + +(defun image-dired-toggle-movement-tracking () + "Turn on and off `image-dired-track-movement'. +Tracking of the movements between thumbnail and Dired buffer so that +they are \"mirrored\" in the dired buffer. When this is on, moving +around in the thumbnail or dired buffer will find the matching +position in the other buffer." + (interactive) + (setq image-dired-track-movement (not image-dired-track-movement)) + (message "Movement tracking %s" (if image-dired-track-movement "on" "off"))) + +(defun image-dired-track-thumbnail () + "Track current Dired file's thumb in `image-dired-thumbnail-buffer'. +This is almost the same as what `image-dired-track-original-file' does, +but the other way around." + (let ((file (dired-get-filename)) + prop-val found window) + (when (get-buffer image-dired-thumbnail-buffer) + (with-current-buffer image-dired-thumbnail-buffer + (goto-char (point-min)) + (while (and (not (eobp)) + (not found)) + (if (and (setq prop-val + (get-text-property (point) 'original-file-name)) + (string= prop-val file)) + (setq found t)) + (if (not found) + (forward-char 1))) + (when found + (if (setq window (image-dired-thumbnail-window)) + (set-window-point window (point))) + (image-dired-update-header-line)))))) + +(defun image-dired-dired-next-line (&optional arg) + "Call `dired-next-line', then track thumbnail. +This can safely replace `dired-next-line'. +With prefix argument, move ARG lines." + (interactive "P") + (dired-next-line (or arg 1)) + (if image-dired-track-movement + (image-dired-track-thumbnail))) + +(defun image-dired-dired-previous-line (&optional arg) + "Call `dired-previous-line', then track thumbnail. +This can safely replace `dired-previous-line'. +With prefix argument, move ARG lines." + (interactive "P") + (dired-previous-line (or arg 1)) + (if image-dired-track-movement + (image-dired-track-thumbnail))) + +(defun image-dired--display-thumb-properties-fun () + (let ((old-buf (current-buffer)) + (old-point (point))) + (lambda () + (when (and (equal (current-buffer) old-buf) + (= (point) old-point)) + (ignore-errors + (image-dired-update-header-line)))))) + +(defun image-dired-forward-image (&optional arg wrap-around) + "Move to next image and display properties. +Optional prefix ARG says how many images to move; the default is +one image. Negative means move backwards. +On reaching end or beginning of buffer, stop and show a message. + +If optional argument WRAP-AROUND is non-nil, wrap around: if +point is on the last image, move to the last one and vice versa." + (interactive "p") + (setq arg (or arg 1)) + (let (pos) + (dotimes (_ (abs arg)) + (if (and (not (if (> arg 0) (eobp) (bobp))) + (save-excursion + (forward-char (if (> arg 0) 1 -1)) + (while (and (not (if (> arg 0) (eobp) (bobp))) + (not (image-dired-image-at-point-p))) + (forward-char (if (> arg 0) 1 -1))) + (setq pos (point)) + (image-dired-image-at-point-p))) + (progn (goto-char pos) + (image-dired-update-header-line)) + (if wrap-around + (progn (goto-char (if (> arg 0) + (point-min) + ;; There are two spaces after the last image. + (- (point-max) 2))) + (image-dired-update-header-line)) + (message "At %s image" (if (> arg 0) "last" "first")) + (run-at-time 1 nil (image-dired--display-thumb-properties-fun)))))) + (when image-dired-track-movement + (image-dired-track-original-file))) + +(defun image-dired-backward-image (&optional arg) + "Move to previous image and display properties. +Optional prefix ARG says how many images to move; the default is +one image. Negative means move forward. +On reaching end or beginning of buffer, stop and show a message." + (interactive "p") + (image-dired-forward-image (- (or arg 1)))) + +(defun image-dired-next-line () + "Move to next line and display properties." + (interactive nil image-dired-thumbnail-mode) + (let ((goal-column (current-column))) + (forward-line 1) + (move-to-column goal-column)) + ;; If we end up in an empty spot, back up to the next thumbnail. + (if (not (image-dired-image-at-point-p)) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + + +(defun image-dired-previous-line () + "Move to previous line and display properties." + (interactive nil image-dired-thumbnail-mode) + (let ((goal-column (current-column))) + (forward-line -1) + (move-to-column goal-column)) + ;; If we end up in an empty spot, back up to the next + ;; thumbnail. This should only happen if the user deleted a + ;; thumbnail and did not refresh, so it is not very common. But we + ;; can handle it in a good manner, so why not? + (if (not (image-dired-image-at-point-p)) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-beginning-of-buffer () + "Move to the first image in the buffer and display properties." + (interactive nil image-dired-thumbnail-mode) + (goto-char (point-min)) + (while (and (not (image-at-point-p)) + (not (eobp))) + (forward-char 1)) + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-end-of-buffer () + "Move to the last image in the buffer and display properties." + (interactive nil image-dired-thumbnail-mode) + (goto-char (point-max)) + (while (and (not (image-at-point-p)) + (not (bobp))) + (forward-char -1)) + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-format-properties-string (buf file props comment) + "Format display properties. +BUF is the associated Dired buffer, FILE is the original image file +name, PROPS is a stringified list of tags and COMMENT is the image file's +comment." + (format-spec + image-dired-display-properties-format + (list + (cons ?b (or buf "")) + (cons ?f file) + (cons ?t (or props "")) + (cons ?c (or comment ""))))) + +(defun image-dired-update-header-line () + "Update image information in the header line." + (when (and (not (eobp)) + (memq major-mode '(image-dired-thumbnail-mode + image-dired-display-image-mode))) + (let ((file-name (file-name-nondirectory (image-dired-original-file-name))) + (dired-buf (buffer-name (image-dired-associated-dired-buffer))) + (props (mapconcat #'identity (get-text-property (point) 'tags) ", ")) + (comment (get-text-property (point) 'comment)) + (message-log-max nil)) + (if file-name + (setq header-line-format + (image-dired-format-properties-string + dired-buf + file-name + props + comment)))))) + +(defun image-dired-dired-file-marked-p (&optional marker) + "In Dired, return t if file on current line is marked. +If optional argument MARKER is non-nil, it is a character to look +for. The default is to look for `dired-marker-char'." + (setq marker (or marker dired-marker-char)) + (save-excursion + (beginning-of-line) + (and (looking-at dired-re-mark) + (= (aref (match-string 0) 0) marker)))) + +(defun image-dired-dired-file-flagged-p () + "In Dired, return t if file on current line is flagged for deletion." + (image-dired-dired-file-marked-p dired-del-marker)) + +(defmacro image-dired--with-thumbnail-buffer (&rest body) + (declare (indent defun) (debug t)) + `(if-let ((buf (get-buffer image-dired-thumbnail-buffer))) + (with-current-buffer buf + (if-let ((win (get-buffer-window buf))) + (with-selected-window win + ,@body) + ,@body)) + (user-error "No such buffer: %s" image-dired-thumbnail-buffer))) + +(defmacro image-dired--on-file-in-dired-buffer (&rest body) + "Run BODY with point on file at point in Dired buffer. +Should be called from commands in `image-dired-thumbnail-mode'." + (declare (indent defun) (debug t)) + `(let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (if (not (and dired-buf file-name)) + (message "No image, or image with correct properties, at point") + (with-current-buffer dired-buf + (when (dired-goto-file file-name) + ,@body + (image-dired-thumb-update-marks)))))) + +(defmacro image-dired--do-mark-command (maybe-next &rest body) + "Helper macro for the mark, unmark and flag commands. +Run BODY in Dired buffer. +If optional argument MAYBE-NEXT is non-nil, show next image +according to `image-dired-marking-shows-next'." + (declare (indent defun) (debug t)) + `(image-dired--with-thumbnail-buffer + (image-dired--on-file-in-dired-buffer + ,@body) + ,(when maybe-next + '(if image-dired-marking-shows-next + (image-dired-display-next-thumbnail-original) + (image-dired-next-line))))) + +(defun image-dired-mark-thumb-original-file () + "Mark original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-mark 1))) + +(defun image-dired-unmark-thumb-original-file () + "Unmark original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-unmark 1))) + +(defun image-dired-flag-thumb-original-file () + "Flag original image file for deletion in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-flag-file-deletion 1))) + +(defun image-dired-toggle-mark-thumb-original-file () + "Toggle mark on original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command nil + (if (image-dired-dired-file-marked-p) + (dired-unmark 1) + (dired-mark 1)))) + +(defun image-dired-unmark-all-marks () + "Remove all marks from all files in associated Dired buffer. +Also update the marks in the thumbnail buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command nil + (dired-unmark-all-marks)) + (image-dired--with-thumbnail-buffer + (image-dired-thumb-update-marks))) + +(defun image-dired-jump-original-dired-buffer () + "Jump to the Dired buffer associated with the current image file. +You probably want to use this together with +`image-dired-track-original-file'." + (interactive nil image-dired-thumbnail-mode) + (let ((buf (image-dired-associated-dired-buffer)) + window frame) + (setq window (image-dired-get-buffer-window buf)) + (if window + (progn + (if (not (equal (selected-frame) (setq frame (window-frame window)))) + (select-frame-set-input-focus frame)) + (select-window window)) + (message "Associated dired buffer not visible")))) + +;;;###autoload +(defun image-dired-jump-thumbnail-buffer () + "Jump to thumbnail buffer." + (interactive) + (let ((window (image-dired-thumbnail-window)) + frame) + (if window + (progn + (if (not (equal (selected-frame) (setq frame (window-frame window)))) + (select-frame-set-input-focus frame)) + (select-window window)) + (message "Thumbnail buffer not visible")))) + +(defvar image-dired-thumbnail-mode-line-up-map + (let ((map (make-sparse-keymap))) + ;; map it to "g" so that the user can press it more quickly + (define-key map "g" #'image-dired-line-up-dynamic) + ;; "f" for "fixed" number of thumbs per row + (define-key map "f" #'image-dired-line-up) + ;; "i" for "interactive" + (define-key map "i" #'image-dired-line-up-interactive) + map) + "Keymap for line-up commands in `image-dired-thumbnail-mode'.") + +(defvar image-dired-thumbnail-mode-tag-map + (let ((map (make-sparse-keymap))) + ;; map it to "t" so that the user can press it more quickly + (define-key map "t" #'image-dired-tag-thumbnail) + ;; "r" for "remove" + (define-key map "r" #'image-dired-tag-thumbnail-remove) + map) + "Keymap for tag commands in `image-dired-thumbnail-mode'.") + +(defvar image-dired-thumbnail-mode-map + (let ((map (make-sparse-keymap))) + (define-key map [right] #'image-dired-forward-image) + (define-key map [left] #'image-dired-backward-image) + (define-key map [up] #'image-dired-previous-line) + (define-key map [down] #'image-dired-next-line) + (define-key map "\C-f" #'image-dired-forward-image) + (define-key map "\C-b" #'image-dired-backward-image) + (define-key map "\C-p" #'image-dired-previous-line) + (define-key map "\C-n" #'image-dired-next-line) + + (define-key map "<" #'image-dired-beginning-of-buffer) + (define-key map ">" #'image-dired-end-of-buffer) + (define-key map (kbd "M-<") #'image-dired-beginning-of-buffer) + (define-key map (kbd "M->") #'image-dired-end-of-buffer) + + (define-key map "d" #'image-dired-flag-thumb-original-file) + (define-key map [delete] #'image-dired-flag-thumb-original-file) + (define-key map "m" #'image-dired-mark-thumb-original-file) + (define-key map "u" #'image-dired-unmark-thumb-original-file) + (define-key map "U" #'image-dired-unmark-all-marks) + (define-key map "." #'image-dired-track-original-file) + (define-key map [tab] #'image-dired-jump-original-dired-buffer) + + ;; add line-up map + (define-key map "g" image-dired-thumbnail-mode-line-up-map) + ;; add tag map + (define-key map "t" image-dired-thumbnail-mode-tag-map) + + (define-key map "\C-m" #'image-dired-display-thumbnail-original-image) + (define-key map [C-return] #'image-dired-thumbnail-display-external) + + (define-key map "L" #'image-dired-rotate-original-left) + (define-key map "R" #'image-dired-rotate-original-right) + + (define-key map "D" #'image-dired-thumbnail-set-image-description) + (define-key map "S" #'image-dired-slideshow-start) + (define-key map "\C-d" #'image-dired-delete-char) + (define-key map " " #'image-dired-display-next-thumbnail-original) + (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) + (define-key map "c" #'image-dired-comment-thumbnail) + + ;; Mouse + (define-key map [mouse-2] #'image-dired-mouse-display-image) + (define-key map [mouse-1] #'image-dired-mouse-select-thumbnail) + (define-key map [mouse-3] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-1] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-2] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-3] #'image-dired-mouse-select-thumbnail) + ;; Seems I must first set C-down-mouse-1 to undefined, or else it + ;; will trigger the buffer menu. If I try to instead bind + ;; C-down-mouse-1 to `image-dired-mouse-toggle-mark', I get a message + ;; about C-mouse-1 not being defined afterwards. Annoying, but I + ;; probably do not completely understand mouse events. + (define-key map [C-down-mouse-1] #'undefined) + (define-key map [C-mouse-1] #'image-dired-mouse-toggle-mark) + map) + "Keymap for `image-dired-thumbnail-mode'.") + +(easy-menu-define image-dired-thumbnail-mode-menu image-dired-thumbnail-mode-map + "Menu for `image-dired-thumbnail-mode'." + '("Image-Dired" + ["Display image" image-dired-display-thumbnail-original-image] + ["Display in external viewer" image-dired-thumbnail-display-external] + ["Jump to Dired buffer" image-dired-jump-original-dired-buffer] + "---" + ["Mark image" image-dired-mark-thumb-original-file] + ["Unmark image" image-dired-unmark-thumb-original-file] + ["Unmark all images" image-dired-unmark-all-marks] + ["Flag for deletion" image-dired-flag-thumb-original-file] + ["Delete marked images" image-dired-delete-marked] + "---" + ["Rotate original right" image-dired-rotate-original-right] + ["Rotate original left" image-dired-rotate-original-left] + "---" + ["Comment thumbnail" image-dired-comment-thumbnail] + ["Tag current or marked thumbnails" image-dired-tag-thumbnail] + ["Remove tag from current or marked thumbnails" + image-dired-tag-thumbnail-remove] + ["Start slideshow" image-dired-slideshow-start] + "---" + ("View Options" + ["Toggle movement tracking" image-dired-toggle-movement-tracking + :style toggle + :selected image-dired-track-movement] + "---" + ["Line up thumbnails" image-dired-line-up] + ["Dynamic line up" image-dired-line-up-dynamic] + ["Refresh thumb" image-dired-refresh-thumb]) + ["Quit" quit-window])) + +(defvar image-dired-display-image-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "S" #'image-dired-slideshow-start) + (define-key map (kbd "SPC") #'image-dired-display-next-thumbnail-original) + (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) + (define-key map "n" #'image-dired-display-next-thumbnail-original) + (define-key map "p" #'image-dired-display-previous-thumbnail-original) + (define-key map "m" #'image-dired-mark-thumb-original-file) + (define-key map "d" #'image-dired-flag-thumb-original-file) + (define-key map "u" #'image-dired-unmark-thumb-original-file) + (define-key map "U" #'image-dired-unmark-all-marks) + ;; Disable keybindings from `image-mode-map' that doesn't make sense here. + (define-key map "o" nil) ; image-save + map) + "Keymap for `image-dired-display-image-mode'.") + +(define-derived-mode image-dired-thumbnail-mode + special-mode "image-dired-thumbnail" + "Browse and manipulate thumbnail images using Dired. +Use `image-dired-minor-mode' to get a nice setup." + :interactive nil + (buffer-disable-undo) + (add-hook 'file-name-at-point-functions 'image-dired-file-name-at-point nil t) + (setq-local window-resize-pixelwise t) + (setq-local bookmark-make-record-function #'image-dired-bookmark-make-record) + ;; Use approximately as much vertical spacing as horizontal. + (setq-local line-spacing (frame-char-width))) + + +;;; Display image mode + +(define-derived-mode image-dired-display-image-mode + image-mode "image-dired-image-display" + "Mode for displaying and manipulating original image. +Resized or in full-size." + :interactive nil + (add-hook 'file-name-at-point-functions #'image-dired-file-name-at-point nil t)) + +(defvar image-dired-minor-mode-map + (let ((map (make-sparse-keymap))) + ;; (set-keymap-parent map dired-mode-map) + ;; Hijack previous and next line movement. Let C-p and C-b be + ;; though... + (define-key map "p" #'image-dired-dired-previous-line) + (define-key map "n" #'image-dired-dired-next-line) + (define-key map [up] #'image-dired-dired-previous-line) + (define-key map [down] #'image-dired-dired-next-line) + + (define-key map (kbd "C-S-n") #'image-dired-next-line-and-display) + (define-key map (kbd "C-S-p") #'image-dired-previous-line-and-display) + (define-key map (kbd "C-S-m") #'image-dired-mark-and-display-next) + + (define-key map "\C-td" #'image-dired-display-thumbs) + (define-key map [tab] #'image-dired-jump-thumbnail-buffer) + (define-key map "\C-ti" #'image-dired-dired-display-image) + (define-key map "\C-tx" #'image-dired-dired-display-external) + (define-key map "\C-ta" #'image-dired-display-thumbs-append) + (define-key map "\C-t." #'image-dired-display-thumb) + (define-key map "\C-tc" #'image-dired-dired-comment-files) + (define-key map "\C-tf" #'image-dired-mark-tagged-files) + map) + "Keymap for `image-dired-minor-mode'.") + +(easy-menu-define image-dired-minor-mode-menu image-dired-minor-mode-map + "Menu for `image-dired-minor-mode'." + '("Image-dired" + ["Display thumb for next file" image-dired-next-line-and-display] + ["Display thumb for previous file" image-dired-previous-line-and-display] + ["Mark and display next" image-dired-mark-and-display-next] + "---" + ["Create thumbnails for marked files" image-dired-create-thumbs] + "---" + ["Display thumbnails append" image-dired-display-thumbs-append] + ["Display this thumbnail" image-dired-display-thumb] + ["Display image" image-dired-dired-display-image] + ["Display in external viewer" image-dired-dired-display-external] + "---" + ["Toggle display properties" image-dired-toggle-dired-display-properties + :style toggle + :selected image-dired-dired-disp-props] + ["Toggle append browsing" image-dired-toggle-append-browsing + :style toggle + :selected image-dired-append-when-browsing] + ["Toggle movement tracking" image-dired-toggle-movement-tracking + :style toggle + :selected image-dired-track-movement] + "---" + ["Jump to thumbnail buffer" image-dired-jump-thumbnail-buffer] + ["Mark tagged files" image-dired-mark-tagged-files] + ["Comment files" image-dired-dired-comment-files] + ["Copy with EXIF file name" image-dired-copy-with-exif-file-name])) + +;;;###autoload +(define-minor-mode image-dired-minor-mode + "Setup easy-to-use keybindings for the commands to be used in Dired mode. +Note that n, p and and will be hijacked and bound to +`image-dired-dired-next-line' and `image-dired-dired-previous-line'." + :keymap image-dired-minor-mode-map) + +(declare-function clear-image-cache "image.c" (&optional filter)) + +(defun image-dired-create-thumbs (&optional arg) + "Create thumbnail images for all marked files in Dired. +With prefix argument ARG, create thumbnails even if they already exist +\(i.e. use this to refresh your thumbnails)." + (interactive "P") + (let (thumb-name) + (dolist (curr-file (dired-get-marked-files)) + (setq thumb-name (image-dired-thumb-name curr-file)) + ;; If the user overrides the exist check, we must clear the + ;; image cache so that if the user wants to display the + ;; thumbnail, it is not fetched from cache. + (when arg + (clear-image-cache (expand-file-name thumb-name))) + (when (or (not (file-exists-p thumb-name)) + arg) + (image-dired-create-thumb curr-file thumb-name))))) + + +;;; Slideshow + +(defcustom image-dired-slideshow-delay 5.0 + "Seconds to wait before showing the next image in a slideshow. +This is used by `image-dired-slideshow-start'." + :type 'float + :version "29.1") + +(define-obsolete-variable-alias 'image-dired-slideshow-timer + 'image-dired--slideshow-timer "29.1") +(defvar image-dired--slideshow-timer nil + "Slideshow timer.") + +(defvar image-dired--slideshow-initial nil) + +(defun image-dired-slideshow-step () + "Step to next image in a slideshow." + (if-let ((buf (get-buffer image-dired-thumbnail-buffer))) + (with-current-buffer buf + (image-dired-display-next-thumbnail-original)) + (image-dired-slideshow-stop))) + +(defun image-dired-slideshow-start (&optional arg) + "Start a slideshow, waiting `image-dired-slideshow-delay' between images. + +With prefix argument ARG, wait that many seconds before going to +the next image. + +With a negative prefix argument, prompt user for the delay." + (interactive "P" image-dired-thumbnail-mode image-dired-display-image-mode) + (let ((delay (if (not arg) + image-dired-slideshow-delay + (if (> arg 0) + arg + (string-to-number + (let ((delay (number-to-string image-dired-slideshow-delay))) + (read-string + (format-prompt "Delay, in seconds. Decimals are accepted" delay)) + delay)))))) + (setq image-dired--slideshow-timer + (run-with-timer + 0 delay + 'image-dired-slideshow-step)) + (add-hook 'post-command-hook 'image-dired-slideshow-stop) + (setq image-dired--slideshow-initial t) + (message "Running slideshow; use any command to stop"))) + +(defun image-dired-slideshow-stop () + "Cancel slideshow." + ;; Make sure we don't immediately stop after + ;; `image-dired-slideshow-start'. + (unless image-dired--slideshow-initial + (remove-hook 'post-command-hook 'image-dired-slideshow-stop) + (cancel-timer image-dired--slideshow-timer)) + (setq image-dired--slideshow-initial nil)) + + +;;; Thumbnail mode (cont. 3) + +(defun image-dired-delete-char () + "Remove current thumbnail from thumbnail buffer and line up." + (interactive nil image-dired-thumbnail-mode) + (let ((inhibit-read-only t)) + (delete-char 1) + (when (= (following-char) ?\s) + (delete-char 1)))) + +;;;###autoload +(defun image-dired-display-thumbs-append () + "Append thumbnails to `image-dired-thumbnail-buffer'." + (interactive) + (image-dired-display-thumbs nil t t)) + +;;;###autoload +(defun image-dired-display-thumb () + "Shorthand for `image-dired-display-thumbs' with prefix argument." + (interactive) + (image-dired-display-thumbs t nil t)) + +(defun image-dired-line-up () + "Line up thumbnails according to `image-dired-thumbs-per-row'. +See also `image-dired-line-up-dynamic'." + (interactive) + (let ((inhibit-read-only t)) + (goto-char (point-min)) + (while (and (not (image-dired-image-at-point-p)) + (not (eobp))) + (delete-char 1)) + (while (not (eobp)) + (forward-char) + (while (and (not (image-dired-image-at-point-p)) + (not (eobp))) + (delete-char 1))) + (goto-char (point-min)) + (let ((seen 0) + (thumb-prev-pos 0) + (thumb-width-chars + (ceiling (/ (+ (* 2 image-dired-thumb-relief) + (* 2 image-dired-thumb-margin) + (image-dired-thumb-size 'width)) + (float (frame-char-width)))))) + (while (not (eobp)) + (forward-char) + (if (= image-dired-thumbs-per-row 1) + (insert "\n") + (cl-incf thumb-prev-pos thumb-width-chars) + (insert (propertize " " 'display `(space :align-to ,thumb-prev-pos))) + (cl-incf seen) + (when (and (= seen (- image-dired-thumbs-per-row 1)) + (not (eobp))) + (forward-char) + (insert "\n") + (setq seen 0) + (setq thumb-prev-pos 0))))) + (goto-char (point-min)))) + +(defun image-dired-line-up-dynamic () + "Line up thumbnails images dynamically. +Calculate how many thumbnails fit." + (interactive) + (let* ((char-width (frame-char-width)) + (width (image-dired-window-width-pixels (image-dired-thumbnail-window))) + (image-dired-thumbs-per-row + (/ width + (+ (* 2 image-dired-thumb-relief) + (* 2 image-dired-thumb-margin) + (image-dired-thumb-size 'width) + char-width)))) + (image-dired-line-up))) + +(defun image-dired-line-up-interactive () + "Line up thumbnails interactively. +Ask user how many thumbnails should be displayed per row." + (interactive) + (let ((image-dired-thumbs-per-row + (string-to-number (read-string "How many thumbs per row: ")))) + (if (not (> image-dired-thumbs-per-row 0)) + (message "Number must be greater than 0") + (image-dired-line-up)))) + +(defun image-dired-thumbnail-display-external () + "Display original image for thumbnail at point using external viewer." + (interactive) + (let ((file (image-dired-original-file-name))) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (if (not file) + (message "No original file name found") + (start-process "image-dired-thumb-external" nil + image-dired-external-viewer file))))) + +;;;###autoload +(defun image-dired-dired-display-external () + "Display file at point using an external viewer." + (interactive) + (let ((file (dired-get-filename))) + (start-process "image-dired-external" nil + image-dired-external-viewer file))) + +(defun image-dired-window-width-pixels (window) + "Calculate WINDOW width in pixels." + (* (window-width window) (frame-char-width))) + +(defun image-dired-display-window () + "Return window where `image-dired-display-image-buffer' is visible." + (get-window-with-predicate + (lambda (window) + (equal (buffer-name (window-buffer window)) image-dired-display-image-buffer)) + nil t)) + +(defun image-dired-thumbnail-window () + "Return window where `image-dired-thumbnail-buffer' is visible." + (get-window-with-predicate + (lambda (window) + (equal (buffer-name (window-buffer window)) image-dired-thumbnail-buffer)) + nil t)) + +(defun image-dired-associated-dired-buffer-window () + "Return window where associated Dired buffer is visible." + (let (buf) + (if (image-dired-image-at-point-p) + (progn + (setq buf (image-dired-associated-dired-buffer)) + (get-window-with-predicate + (lambda (window) + (equal (window-buffer window) buf)))) + (error "No thumbnail image at point")))) + +(defun image-dired-display-image (file &optional _ignored) + "Display image FILE in image buffer. +Use this when you want to display the image, in a new window. +The window will use `image-dired-display-image-mode' which is +based on `image-mode'." + (declare (advertised-calling-convention (file) "29.1")) + (setq file (expand-file-name file)) + (when (not (file-exists-p file)) + (error "No such file: %s" file)) + (let ((buf (get-buffer image-dired-display-image-buffer)) + (cur-win (selected-window))) + (when buf + (kill-buffer buf)) + (when-let ((buf (find-file-noselect file nil t))) + (pop-to-buffer buf) + (rename-buffer image-dired-display-image-buffer) + (image-dired-display-image-mode) + (select-window cur-win)))) + +(defun image-dired-display-thumbnail-original-image (&optional arg) + "Display current thumbnail's original image in display buffer. +See documentation for `image-dired-display-image' for more information. +With prefix argument ARG, display image in its original size." + (interactive "P") + (let ((file (image-dired-original-file-name))) + (if (not (string-equal major-mode "image-dired-thumbnail-mode")) + (message "Not in image-dired-thumbnail-mode") + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (if (not file) + (message "No original file name found") + (image-dired-display-image file arg)))))) + + +;;;###autoload +(defun image-dired-dired-display-image (&optional arg) + "Display current image file. +See documentation for `image-dired-display-image' for more information. +With prefix argument ARG, display image in its original size." + (interactive "P") + (image-dired-display-image (dired-get-filename) arg)) + +(defun image-dired-image-at-point-p () + "Return non-nil if there is an `image-dired' thumbnail at point." + (get-text-property (point) 'image-dired-thumbnail)) + +(defun image-dired-refresh-thumb () + "Force creation of new image for current thumbnail." + (interactive nil image-dired-thumbnail-mode) + (let* ((file (image-dired-original-file-name)) + (thumb (expand-file-name (image-dired-thumb-name file)))) + (clear-image-cache (expand-file-name thumb)) + (image-dired-create-thumb file thumb))) + +(defun image-dired-rotate-original (degrees) + "Rotate original image DEGREES degrees." + (image-dired--check-executable-exists + 'image-dired-cmd-rotate-original-program) + (if (not (image-dired-image-at-point-p)) + (message "No image at point") + (let* ((file (image-dired-original-file-name)) + (spec + (list + (cons ?d degrees) + (cons ?o (expand-file-name file)) + (cons ?t image-dired-temp-rotate-image-file)))) + (unless (eq 'jpeg (image-type file)) + (user-error "Only JPEG images can be rotated")) + (if (not (= 0 (apply #'call-process image-dired-cmd-rotate-original-program + nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-rotate-original-options)))) + (error "Could not rotate image") + (image-dired-display-image image-dired-temp-rotate-image-file) + (if (or (and image-dired-rotate-original-ask-before-overwrite + (y-or-n-p + "Rotate to temp file OK. Overwrite original image? ")) + (not image-dired-rotate-original-ask-before-overwrite)) + (progn + (copy-file image-dired-temp-rotate-image-file file t) + (image-dired-refresh-thumb)) + (image-dired-display-image file)))))) + +(defun image-dired-rotate-original-left () + "Rotate original image left (counter clockwise) 90 degrees. +The result of the rotation is displayed in the image display area +and a confirmation is needed before the original image files is +overwritten. This confirmation can be turned off using +`image-dired-rotate-original-ask-before-overwrite'." + (interactive) + (image-dired-rotate-original "270")) + +(defun image-dired-rotate-original-right () + "Rotate original image right (clockwise) 90 degrees. +The result of the rotation is displayed in the image display area +and a confirmation is needed before the original image files is +overwritten. This confirmation can be turned off using +`image-dired-rotate-original-ask-before-overwrite'." + (interactive) + (image-dired-rotate-original "90")) + + +;;; EXIF support + +(defun image-dired-get-exif-file-name (file) + "Use the image's EXIF information to return a unique file name. +The file name should be unique as long as you do not take more than +one picture per second. The original file name is suffixed at the end +for traceability. The format of the returned file name is +YYYY_MM_DD_HH_MM_DD_ORIG_FILE_NAME.jpg. Used from +`image-dired-copy-with-exif-file-name'." + (let (data no-exif-data-found) + (if (not (eq 'jpeg (image-type (expand-file-name file)))) + (setq no-exif-data-found t + data (format-time-string + "%Y:%m:%d %H:%M:%S" + (file-attribute-modification-time + (file-attributes (expand-file-name file))))) + (setq data (exif-field 'date-time (exif-parse-file + (expand-file-name file))))) + (while (string-match "[ :]" data) + (setq data (replace-match "_" nil nil data))) + (format "%s%s%s" data + (if no-exif-data-found + "_noexif_" + "_") + (file-name-nondirectory file)))) + +(defun image-dired-thumbnail-set-image-description () + "Set the ImageDescription EXIF tag for the original image. +If the image already has a value for this tag, it is used as the +default value at the prompt." + (interactive) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (let* ((file (image-dired-original-file-name)) + (old-value (or (exif-field 'description (exif-parse-file file)) ""))) + (if (eq 0 + (image-dired-set-exif-data file "ImageDescription" + (read-string "Value of ImageDescription: " + old-value))) + (message "Successfully wrote ImageDescription tag") + (error "Could not write ImageDescription tag"))))) + +(defun image-dired-set-exif-data (file tag-name tag-value) + "In FILE, set EXIF tag TAG-NAME to value TAG-VALUE." + (image-dired--check-executable-exists + 'image-dired-cmd-write-exif-data-program) + (let ((spec + (list + (cons ?f (expand-file-name file)) + (cons ?t tag-name) + (cons ?v tag-value)))) + (apply #'call-process image-dired-cmd-write-exif-data-program nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-write-exif-data-options)))) + +(defun image-dired-copy-with-exif-file-name () + "Copy file with unique name to main image directory. +Copy current or all marked files in Dired to a new file in your +main image directory, using a file name generated by +`image-dired-get-exif-file-name'. A typical usage for this if when +copying images from a digital camera into the image directory. + + Typically, you would open up the folder with the incoming +digital images, mark the files to be copied, and execute this +function. The result is a couple of new files in +`image-dired-main-image-directory' called +2005_05_08_12_52_00_dscn0319.jpg, +2005_05_08_14_27_45_dscn0320.jpg etc." + (interactive) + (let (new-name + (files (dired-get-marked-files))) + (mapc + (lambda (curr-file) + (setq new-name + (format "%s/%s" + (file-name-as-directory + (expand-file-name image-dired-main-image-directory)) + (image-dired-get-exif-file-name curr-file))) + (message "Copying %s to %s" curr-file new-name) + (copy-file curr-file new-name)) + files))) + +;;; Thumbnail mode (cont.) + +(defun image-dired-display-next-thumbnail-original (&optional arg) + "Move to the next image in the thumbnail buffer and display it. +With prefix ARG, move that many thumbnails." + (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--with-thumbnail-buffer + (image-dired-forward-image arg t) + (image-dired-display-thumbnail-original-image))) + +(defun image-dired-display-previous-thumbnail-original (arg) + "Move to the previous image in the thumbnail buffer and display it. +With prefix ARG, move that many thumbnails." + (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired-display-next-thumbnail-original (- arg))) + + +;;; Image Comments + +(defun image-dired-write-comments (file-comments) + "Write file comments to database. +Write file comments to one or more files. +FILE-COMMENTS is an alist on the following form: + ((FILE . COMMENT) ... )" + (image-dired-sane-db-file) + (let (end comment-beg-pos comment-end-pos file comment) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (dolist (elt file-comments) + (setq file (car elt) + comment (cdr elt)) + (goto-char (point-min)) + (if (search-forward-regexp (format "^%s.*$" file) nil t) + (progn + (setq end (point)) + (beginning-of-line) + ;; Delete old comment, if any + (when (search-forward ";comment:" end t) + (setq comment-beg-pos (match-beginning 0)) + ;; Any tags after the comment? + (if (search-forward ";" end t) + (setq comment-end-pos (- (point) 1)) + (setq comment-end-pos end)) + ;; Delete comment tag and comment + (delete-region comment-beg-pos comment-end-pos)) + ;; Insert new comment + (beginning-of-line) + (unless (search-forward ";" end t) + (end-of-line) + (insert ";")) + (insert (format "comment:%s;" comment))) + ;; File does not exist in database - add it. + (goto-char (point-max)) + (insert (format "%s;comment:%s\n" file comment)))) + (save-buffer)))) + +(defun image-dired-update-property (prop value) + "Update text property PROP with value VALUE at point." + (let ((inhibit-read-only t)) + (put-text-property + (point) (1+ (point)) + prop + value))) + +;;;###autoload +(defun image-dired-dired-comment-files () + "Add comment to current or marked files in Dired." + (interactive) + (let ((comment (image-dired-read-comment))) + (image-dired-write-comments + (mapcar + (lambda (curr-file) + (cons curr-file comment)) + (dired-get-marked-files))))) + +(defun image-dired-comment-thumbnail () + "Add comment to current thumbnail in thumbnail buffer." + (interactive) + (let* ((file (image-dired-original-file-name)) + (comment (image-dired-read-comment file))) + (image-dired-write-comments (list (cons file comment))) + (image-dired-update-property 'comment comment)) + (image-dired-update-header-line)) + +(defun image-dired-read-comment (&optional file) + "Read comment for an image. +Optionally use old comment from FILE as initial value." + (let ((comment + (read-string + "Comment: " + (if file (image-dired-get-comment file))))) + comment)) + +(defun image-dired-get-comment (file) + "Get comment for file FILE." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end comment-beg-pos comment-end-pos comment) + (when (search-forward-regexp (format "^%s" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (search-forward ";comment:" end t) + (setq comment-beg-pos (point)) + (if (search-forward ";" end t) + (setq comment-end-pos (- (point) 1)) + (setq comment-end-pos end)) + (setq comment (buffer-substring + comment-beg-pos comment-end-pos)))) + comment))) + +;;;###autoload +(defun image-dired-mark-tagged-files (regexp) + "Use REGEXP to mark files with matching tag. +A `tag' is a keyword, a piece of meta data, associated with an +image file and stored in image-dired's database file. This command +lets you input a regexp and this will be matched against all tags +on all image files in the database file. The files that have a +matching tag will be marked in the Dired buffer." + (interactive "sMark tagged files (regexp): ") + (image-dired-sane-db-file) + (let ((hits 0) + files) + (image-dired--with-db-file + ;; Collect matches + (while (search-forward-regexp "\\(^[^;\n]+\\);\\(.*\\)" nil t) + (let ((file (match-string 1)) + (tags (split-string (match-string 2) ";"))) + (when (seq-find (lambda (tag) + (string-match-p regexp tag)) + tags) + (push file files))))) + ;; Mark files + (dolist (curr-file files) + ;; I tried using `dired-mark-files-regexp' but it was waaaay to + ;; slow. Don't bother about hits found in other directories + ;; than the current one. + (when (string= (file-name-as-directory + (expand-file-name default-directory)) + (file-name-as-directory + (file-name-directory curr-file))) + (setq curr-file (file-name-nondirectory curr-file)) + (goto-char (point-min)) + (when (search-forward-regexp (format "\\s %s$" curr-file) nil t) + (setq hits (+ hits 1)) + (dired-mark 1)))) + (message "%d files with matching tag marked" hits))) + + + +;;; Mouse support + +(defun image-dired-mouse-display-image (event) + "Use mouse EVENT, call `image-dired-display-image' to display image. +Track this in associated Dired buffer if `image-dired-track-movement' is +non-nil." + (interactive "e") + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (unless (image-at-point-p) + (image-dired-backward-image)) + (let ((file (image-dired-original-file-name))) + (when file + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-display-image file)))) + +(defun image-dired-mouse-select-thumbnail (event) + "Use mouse EVENT to select thumbnail image. +Track this in associated Dired buffer if `image-dired-track-movement' is +non-nil." + (interactive "e") + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (unless (image-at-point-p) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + + + +;;; Dired marks and tags + +(defun image-dired-thumb-file-marked-p (&optional flagged) + "Check if file is marked in associated Dired buffer. +If optional argument FLAGGED is non-nil, check if file is flagged +for deletion instead." + (let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (when (and dired-buf file-name) + (with-current-buffer dired-buf + (save-excursion + (when (dired-goto-file file-name) + (if flagged + (image-dired-dired-file-flagged-p) + (image-dired-dired-file-marked-p)))))))) + +(defun image-dired-thumb-file-flagged-p () + "Check if file is flagged for deletion in associated Dired buffer." + (image-dired-thumb-file-marked-p t)) + +(defun image-dired-delete-marked () + "Delete current or marked thumbnails and associated images." + (interactive) + (image-dired--with-marked + (image-dired-delete-char) + (unless (bobp) + (backward-char))) + (image-dired--line-up-with-method) + (with-current-buffer (image-dired-associated-dired-buffer) + (dired-do-delete))) + +(defun image-dired-thumb-update-marks () + "Update the marks in the thumbnail buffer." + (when image-dired-thumb-visible-marks + (with-current-buffer image-dired-thumbnail-buffer + (save-mark-and-excursion + (goto-char (point-min)) + (let ((inhibit-read-only t)) + (while (not (eobp)) + (with-silent-modifications + (cond ((image-dired-thumb-file-marked-p) + (add-face-text-property (point) (1+ (point)) + 'image-dired-thumb-mark)) + ((image-dired-thumb-file-flagged-p) + (add-face-text-property (point) (1+ (point)) + 'image-dired-thumb-flagged)) + (t (remove-text-properties (point) (1+ (point)) + '(face image-dired-thumb-mark))))) + (forward-char))))))) + +(defun image-dired-mouse-toggle-mark-1 () + "Toggle Dired mark for current thumbnail. +Track this in associated Dired buffer if +`image-dired-track-movement' is non-nil." + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-toggle-mark-thumb-original-file)) + +(defun image-dired-mouse-toggle-mark (event) + "Use mouse EVENT to toggle Dired mark for thumbnail. +Toggle marks of all thumbnails in region, if it's active. +Track this in associated Dired buffer if +`image-dired-track-movement' is non-nil." + (interactive "e") + (if (use-region-p) + (let ((end (region-end))) + (save-excursion + (goto-char (region-beginning)) + (while (<= (point) end) + (when (image-dired-image-at-point-p) + (image-dired-mouse-toggle-mark-1)) + (forward-char)))) + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (image-dired-mouse-toggle-mark-1)) + (image-dired-thumb-update-marks)) + +(defun image-dired-dired-display-properties () + "Display properties for Dired file in the echo area." + (interactive) + (let* ((file (dired-get-filename)) + (file-name (file-name-nondirectory file)) + (dired-buf (buffer-name (current-buffer))) + (props (mapconcat #'identity (image-dired-list-tags file) ", ")) + (comment (image-dired-get-comment file)) + (message-log-max nil)) + (if file-name + (message "%s" + (image-dired-format-properties-string + dired-buf + file-name + props + comment))))) + + + +;;; Gallery support + +;; TODO: +;; * Support gallery creation when using per-directory thumbnail +;; storage. +;; * Enhanced gallery creation with basic CSS-support and pagination +;; of tag pages with many pictures. + +(defgroup image-dired-gallery nil + "Image-Dired support for generating a HTML gallery." + :prefix "image-dired-" + :group 'image-dired + :version "29.1") + +(defcustom image-dired-gallery-dir + (expand-file-name ".image-dired_gallery" image-dired-dir) + "Directory to store generated gallery html pages. +The name of this directory needs to be \"shared\" to the public +so that it can access the index.html page that image-dired creates." + :type 'directory) + +(defcustom image-dired-gallery-image-root-url + "https://example.org/image-diredpics" + "URL where the full size images are to be found on your web server. +Note that this URL has to be configured on your web server. +Image-Dired expects to find pictures in this directory. +This is used by `image-dired-gallery-generate'." + :type 'string + :version "29.1") + +(defcustom image-dired-gallery-thumb-image-root-url + "https://example.org/image-diredthumbs" + "URL where the thumbnail images are to be found on your web server. +Note that URL path has to be configured on your web server. +Image-Dired expects to find pictures in this directory. +This is used by `image-dired-gallery-generate'." + :type 'string + :version "29.1") + +(defcustom image-dired-gallery-hidden-tags + (list "private" "hidden" "pending") + "List of \"hidden\" tags. +Used by `image-dired-gallery-generate' to leave out \"hidden\" images." + :type '(repeat string)) + +(defvar image-dired-tag-file-list nil + "List to store tag-file structure.") + +(defvar image-dired-file-tag-list nil + "List to store file-tag structure.") + +(defvar image-dired-file-comment-list nil + "List to store file comments.") + +(defun image-dired--add-to-tag-file-lists (tag file) + "Helper function used from `image-dired--create-gallery-lists'. + +Add TAG to FILE in one list and FILE to TAG in the other. + +Lisp structures look like the following: + +image-dired-file-tag-list: + + ((\"filename1\" \"tag1\" \"tag2\" \"tag3\" ...) + (\"filename2\" \"tag1\" \"tag2\" \"tag3\" ...) + ...) + +image-dired-tag-file-list: + + ((\"tag1\" \"filename1\" \"filename2\" \"filename3\" ...) + (\"tag2\" \"filename1\" \"filename2\" \"filename3\" ...) + ...)" + ;; Add tag to file list + (let (curr) + (if image-dired-file-tag-list + (if (setq curr (assoc file image-dired-file-tag-list)) + (setcdr curr (cons tag (cdr curr))) + (setcdr image-dired-file-tag-list + (cons (list file tag) (cdr image-dired-file-tag-list)))) + (setq image-dired-file-tag-list (list (list file tag)))) + ;; Add file to tag list + (if image-dired-tag-file-list + (if (setq curr (assoc tag image-dired-tag-file-list)) + (if (not (member file curr)) + (setcdr curr (cons file (cdr curr)))) + (setcdr image-dired-tag-file-list + (cons (list tag file) (cdr image-dired-tag-file-list)))) + (setq image-dired-tag-file-list (list (list tag file)))))) + +(defun image-dired--add-to-file-comment-list (file comment) + "Helper function used from `image-dired--create-gallery-lists'. + +For FILE, add COMMENT to list. + +Lisp structure looks like the following: + +image-dired-file-comment-list: + + ((\"filename1\" . \"comment1\") + (\"filename2\" . \"comment2\") + ...)" + (if image-dired-file-comment-list + (if (not (assoc file image-dired-file-comment-list)) + (setcdr image-dired-file-comment-list + (cons (cons file comment) + (cdr image-dired-file-comment-list)))) + (setq image-dired-file-comment-list (list (cons file comment))))) + +(defun image-dired--create-gallery-lists () + "Create temporary lists used by `image-dired-gallery-generate'." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end beg file row-tags) + (setq image-dired-tag-file-list nil) + (setq image-dired-file-tag-list nil) + (setq image-dired-file-comment-list nil) + (goto-char (point-min)) + (while (search-forward-regexp "^." nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (setq beg (point)) + (unless (search-forward ";" end nil) + (error "Something is really wrong, check format of database")) + (setq row-tags (split-string + (buffer-substring beg end) ";")) + (setq file (car row-tags)) + (dolist (x (cdr row-tags)) + (if (not (string-match "^comment:\\(.*\\)" x)) + (image-dired--add-to-tag-file-lists x file) + (image-dired--add-to-file-comment-list file (match-string 1 x))))))) + ;; Sort tag-file list + (setq image-dired-tag-file-list + (sort image-dired-tag-file-list + (lambda (x y) + (string< (car x) (car y)))))) + +(defun image-dired--hidden-p (file) + "Return t if image FILE has a \"hidden\" tag." + (cl-loop for tag in (cdr (assoc file image-dired-file-tag-list)) + if (member tag image-dired-gallery-hidden-tags) return t)) + +(defun image-dired-gallery-generate () + "Generate gallery pages. +First we create a couple of Lisp structures from the database to make +it easier to generate, then HTML-files are created in +`image-dired-gallery-dir'." + (interactive) + (if (eq 'per-directory image-dired-thumbnail-storage) + (error "Currently, gallery generation is not supported \ +when using per-directory thumbnail file storage")) + (image-dired--create-gallery-lists) + (let ((tags image-dired-tag-file-list) + (index-file (format "%s/index.html" image-dired-gallery-dir)) + count tag tag-file + comment file-tags tag-link tag-link-list) + ;; Make sure gallery root exist + (if (file-exists-p image-dired-gallery-dir) + (if (not (file-directory-p image-dired-gallery-dir)) + (error "Variable image-dired-gallery-dir is not a directory")) + ;; FIXME: Should we set umask to 077 here, as we do for thumbnails? + (make-directory image-dired-gallery-dir)) + ;; Open index file + (with-temp-file index-file + (if (file-exists-p index-file) + (insert-file-contents index-file)) + (insert "\n") + (insert " \n") + (insert "

Image-Dired Gallery

\n") + (insert (format "

\n Gallery generated %s\n

\n" + (current-time-string))) + (insert "

Tag index

\n") + (setq count 1) + ;; Pre-generate list of all tag links + (dolist (curr tags) + (setq tag (car curr)) + (when (not (member tag image-dired-gallery-hidden-tags)) + (setq tag-link (format "%s" count tag)) + (if tag-link-list + (setq tag-link-list + (append tag-link-list (list (cons tag tag-link)))) + (setq tag-link-list (list (cons tag tag-link)))) + (setq count (1+ count)))) + (setq count 1) + ;; Main loop where we generated thumbnail pages per tag + (dolist (curr tags) + (setq tag (car curr)) + ;; Don't display hidden tags + (when (not (member tag image-dired-gallery-hidden-tags)) + ;; Insert link to tag page in index + (insert (format " %s
\n" (cdr (assoc tag tag-link-list)))) + ;; Open per-tag file + (setq tag-file (format "%s/%s.html" image-dired-gallery-dir count)) + (with-temp-file tag-file + (if (file-exists-p tag-file) + (insert-file-contents tag-file)) + (erase-buffer) + (insert "\n") + (insert " \n") + (insert "

Index

\n") + (insert (format "

Images with tag "%s"

" tag)) + ;; Main loop for files per tag page + (dolist (file (cdr curr)) + (unless (image-dired-hidden-p file) + ;; Insert thumbnail with link to full image + (insert + (format "\n" + image-dired-gallery-image-root-url + (file-name-nondirectory file) + image-dired-gallery-thumb-image-root-url + (file-name-nondirectory (image-dired-thumb-name file)) file)) + ;; Insert comment, if any + (if (setq comment (cdr (assoc file image-dired-file-comment-list))) + (insert (format "
\n%s
\n" comment)) + (insert "
\n")) + ;; Insert links to other tags, if any + (when (> (length + (setq file-tags (assoc file image-dired-file-tag-list))) 2) + (insert "[ ") + (dolist (extra-tag file-tags) + ;; Only insert if not file name or the main tag + (if (and (not (equal extra-tag tag)) + (not (equal extra-tag file))) + (insert + (format "%s " (cdr (assoc extra-tag tag-link-list)))))) + (insert "]
\n")))) + (insert "

Index

\n") + (insert " \n") + (insert "\n")) + (setq count (1+ count)))) + (insert " \n") + (insert "")))) + + +;;; Tag support + +(defvar image-dired-widget-list nil + "List to keep track of meta data in edit buffer.") + +(declare-function widget-forward "wid-edit" (arg)) + +;;;###autoload +(defun image-dired-dired-edit-comment-and-tags () + "Edit comment and tags of current or marked image files. +Edit comment and tags for all marked image files in an +easy-to-use form." + (interactive) + (setq image-dired-widget-list nil) + ;; Setup buffer. + (let ((files (dired-get-marked-files))) + (pop-to-buffer-same-window "*Image-Dired Edit Meta Data*") + (kill-all-local-variables) + (let ((inhibit-read-only t)) + (erase-buffer)) + (remove-overlays) + ;; Some help for the user. + (widget-insert +"\nEdit comments and tags for each image. Separate multiple tags +with a comma. Move forward between fields using TAB or RET. +Move to the previous field using backtab (S-TAB). Save by +activating the Save button at the bottom of the form or cancel +the operation by activating the Cancel button.\n\n") + ;; Here comes all images and a comment and tag field for each + ;; image. + (let (thumb-file img comment-widget tag-widget) + + (dolist (file files) + + (setq thumb-file (image-dired-thumb-name file) + img (create-image thumb-file)) + + (insert-image img) + (widget-insert "\n\nComment: ") + (setq comment-widget + (widget-create 'editable-field + :size 60 + :format "%v " + :value (or (image-dired-get-comment file) ""))) + (widget-insert "\nTags: ") + (setq tag-widget + (widget-create 'editable-field + :size 60 + :format "%v " + :value (or (mapconcat + #'identity + (image-dired-list-tags file) + ",") ""))) + ;; Save information in all widgets so that we can use it when + ;; the user saves the form. + (setq image-dired-widget-list + (append image-dired-widget-list + (list (list file comment-widget tag-widget)))) + (widget-insert "\n\n"))) + + ;; Footer with Save and Cancel button. + (widget-insert "\n") + (widget-create 'push-button + :notify + (lambda (&rest _ignore) + (image-dired-save-information-from-widgets) + (bury-buffer) + (message "Done")) + "Save") + (widget-insert " ") + (widget-create 'push-button + :notify + (lambda (&rest _ignore) + (bury-buffer) + (message "Operation canceled")) + "Cancel") + (widget-insert "\n") + (use-local-map widget-keymap) + (widget-setup) + ;; Jump to the first widget. + (widget-forward 1))) + +(defun image-dired-save-information-from-widgets () + "Save information found in `image-dired-widget-list'. +Use the information in `image-dired-widget-list' to save comments and +tags to their respective image file. Internal function used by +`image-dired-dired-edit-comment-and-tags'." + (let (file comment tag-string tag-list lst) + (image-dired-write-comments + (mapcar + (lambda (widget) + (setq file (car widget) + comment (widget-value (cadr widget))) + (cons file comment)) + image-dired-widget-list)) + (image-dired-write-tags + (dolist (widget image-dired-widget-list lst) + (setq file (car widget) + tag-string (widget-value (car (cddr widget))) + tag-list (split-string tag-string ",")) + (dolist (tag tag-list) + (push (cons file tag) lst)))))) + + +;;; bookmark.el support + +(declare-function bookmark-make-record-default + "bookmark" (&optional no-file no-context posn)) +(declare-function bookmark-prop-get "bookmark" (bookmark prop)) + +(defun image-dired-bookmark-name () + "Create a default bookmark name for the current EWW buffer." + (file-name-nondirectory + (directory-file-name + (file-name-directory (image-dired-original-file-name))))) + +(defun image-dired-bookmark-make-record () + "Create a bookmark for the current EWW buffer." + `(,(image-dired-bookmark-name) + ,@(bookmark-make-record-default t) + (location . ,(file-name-directory (image-dired-original-file-name))) + (image-dired-file . ,(file-name-nondirectory (image-dired-original-file-name))) + (handler . image-dired-bookmark-jump))) + +;;;###autoload +(defun image-dired-bookmark-jump (bookmark) + "Default bookmark handler for Image-Dired buffers." + ;; User already cached thumbnails, so disable any checking. + (let ((image-dired-show-all-from-dir-max-files nil)) + (image-dired (bookmark-prop-get bookmark 'location)) + ;; TODO: Go to the bookmarked file, if it exists. + ;; (bookmark-prop-get bookmark 'image-dired-file) + (goto-char (point-min)))) + +(put 'image-dired-bookmark-jump 'bookmark-handler-type "Image-Dired") + +;;; Obsolete + +;;;###autoload +(define-obsolete-function-alias 'tumme #'image-dired "24.4") + +;;;###autoload +(define-obsolete-function-alias 'image-dired-setup-dired-keybindings + #'image-dired-minor-mode "26.1") + +(defcustom image-dired-temp-image-file + (expand-file-name ".image-dired_temp" image-dired-dir) + "Name of temporary image file used by various commands." + :type 'file) +(make-obsolete-variable 'image-dired-temp-image-file + "no longer used." "29.1") + +(defcustom image-dired-cmd-create-temp-image-program + (if (executable-find "gm") "gm" "convert") + "Executable used to create temporary image. +Used together with `image-dired-cmd-create-temp-image-options'." + :type 'file + :version "29.1") +(make-obsolete-variable 'image-dired-cmd-create-temp-image-program + "no longer used." "29.1") + +(defcustom image-dired-cmd-create-temp-image-options + (let ((opts '("-size" "%wx%h" "%f[0]" + "-resize" "%wx%h>" + "-strip" "jpeg:%t"))) + (if (executable-find "gm") (cons "convert" opts) opts)) + "Options of command used to create temporary image for display window. +Used together with `image-dired-cmd-create-temp-image-program', +Available format specifiers are: %w and %h which are replaced by +the calculated max size for width and height in the image display window, +%f which is replaced by the file name of the original image and %t which +is replaced by the file name of the temporary file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-create-temp-image-options + "no longer used." "29.1") + +(defcustom image-dired-display-window-width-correction 1 + "Number to be used to correct image display window width. +Change if the default (1) does not work (i.e. if the image does not +completely fit)." + :type 'integer) +(make-obsolete-variable 'image-dired-display-window-width-correction + "no longer used." "29.1") + +(defcustom image-dired-display-window-height-correction 0 + "Number to be used to correct image display window height. +Change if the default (0) does not work (i.e. if the image does not +completely fit)." + :type 'integer) +(make-obsolete-variable 'image-dired-display-window-height-correction + "no longer used." "29.1") + +(defun image-dired-display-window-width (window) + "Return width, in pixels, of WINDOW." + (declare (obsolete nil "29.1")) + (- (image-dired-window-width-pixels window) + image-dired-display-window-width-correction)) + +(defun image-dired-display-window-height (window) + "Return height, in pixels, of WINDOW." + (declare (obsolete nil "29.1")) + (- (image-dired-window-height-pixels window) + image-dired-display-window-height-correction)) + +(defun image-dired-window-height-pixels (window) + "Calculate WINDOW height in pixels." + (declare (obsolete nil "29.1")) + ;; Note: The mode-line consumes one line + (* (- (window-height window) 1) (frame-char-height))) + +(defcustom image-dired-cmd-read-exif-data-program "exiftool" + "Program used to read EXIF data to image. +Used together with `image-dired-cmd-read-exif-data-options'." + :type 'file) +(make-obsolete-variable 'image-dired-cmd-read-exif-data-program + "use `exif-parse-file' and `exif-field' instead." "29.1") + +(defcustom image-dired-cmd-read-exif-data-options '("-s" "-s" "-s" "-%t" "%f") + "Arguments of command used to read EXIF data. +Used with `image-dired-cmd-read-exif-data-program'. +Available format specifiers are: %f which is replaced +by the image file name and %t which is replaced by the tag name." + :version "26.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-read-exif-data-options + "use `exif-parse-file' and `exif-field' instead." "29.1") + +(defun image-dired-get-exif-data (file tag-name) + "From FILE, return EXIF tag TAG-NAME." + (declare (obsolete "use `exif-parse-file' and `exif-field' instead." "29.1")) + (image-dired--check-executable-exists + 'image-dired-cmd-read-exif-data-program) + (let ((buf (get-buffer-create "*image-dired-get-exif-data*")) + (spec (list (cons ?f file) (cons ?t tag-name))) + tag-value) + (with-current-buffer buf + (delete-region (point-min) (point-max)) + (if (not (eq (apply #'call-process image-dired-cmd-read-exif-data-program + nil t nil + (mapcar + (lambda (arg) (format-spec arg spec)) + image-dired-cmd-read-exif-data-options)) + 0)) + (error "Could not get EXIF tag") + (goto-char (point-min)) + ;; Clean buffer from newlines and carriage returns before + ;; getting final info + (while (search-forward-regexp "[\n\r]" nil t) + (replace-match "" nil t)) + (setq tag-value (buffer-substring (point-min) (point-max))))) + tag-value)) + +(defcustom image-dired-cmd-rotate-thumbnail-program + (if (executable-find "gm") "gm" "mogrify") + "Executable used to rotate thumbnail. +Used together with `image-dired-cmd-rotate-thumbnail-options'." + :type 'file + :version "29.1") +(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-program nil "29.1") + +(defcustom image-dired-cmd-rotate-thumbnail-options + (let ((opts '("-rotate" "%d" "%t"))) + (if (executable-find "gm") (cons "mogrify" opts) opts)) + "Arguments of command used to rotate thumbnail image. +Used with `image-dired-cmd-rotate-thumbnail-program'. +Available format specifiers are: %d which is replaced by the +number of (positive) degrees to rotate the image, normally 90 or 270 +\(for 90 degrees right and left), %t which is replaced by the file name +of the thumbnail file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-options nil "29.1") + +(defun image-dired-rotate-thumbnail (degrees) + "Rotate thumbnail DEGREES degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (image-dired--check-executable-exists + 'image-dired-cmd-rotate-thumbnail-program) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (let* ((file (image-dired-thumb-name (image-dired-original-file-name))) + (thumb (expand-file-name file)) + (spec (list (cons ?d degrees) (cons ?t thumb)))) + (apply #'call-process image-dired-cmd-rotate-thumbnail-program nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-rotate-thumbnail-options)) + (clear-image-cache thumb)))) + +(defun image-dired-rotate-thumbnail-left () + "Rotate thumbnail left (counter clockwise) 90 degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (interactive) + (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) + (image-dired-rotate-thumbnail "270"))) + +(defun image-dired-rotate-thumbnail-right () + "Rotate thumbnail counter right (clockwise) 90 degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (interactive) + (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) + (image-dired-rotate-thumbnail "90"))) + +(defun image-dired-modify-mark-on-thumb-original-file (command) + "Modify mark in Dired buffer. +COMMAND is one of `mark' for marking file in Dired, `unmark' for +unmarking file in Dired or `flag' for flagging file for delete in +Dired." + (declare (obsolete image-dired--on-file-in-dired-buffer "29.1")) + (let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (if (not (and dired-buf file-name)) + (message "No image, or image with correct properties, at point") + (with-current-buffer dired-buf + (message "%s" file-name) + (when (dired-goto-file file-name) + (cond ((eq command 'mark) (dired-mark 1)) + ((eq command 'unmark) (dired-unmark 1)) + ((eq command 'toggle) + (if (image-dired-dired-file-marked-p) + (dired-unmark 1) + (dired-mark 1))) + ((eq command 'flag) (dired-flag-file-deletion 1))) + (image-dired-thumb-update-marks)))))) + +(defun image-dired-display-current-image-full () + "Display current image in full size." + (declare (obsolete image-transform-original "29.1")) + (interactive nil image-dired-thumbnail-mode) + (let ((file (image-dired-original-file-name))) + (if file + (progn + (image-dired-display-image file) + (with-current-buffer image-dired-display-image-buffer + (image-transform-original))) + (error "No original file name at point")))) + +(defun image-dired-display-current-image-sized () + "Display current image in sized to fit window dimensions." + (declare (obsolete image-mode-fit-frame "29.1")) + (interactive nil image-dired-thumbnail-mode) + (let ((file (image-dired-original-file-name))) + (if file + (progn + (image-dired-display-image file)) + (error "No original file name at point")))) + +(defun image-dired-add-to-tag-file-list (tag file) + "Add relation between TAG and FILE." + (declare (obsolete nil "29.1")) + (let (curr) + (if image-dired-tag-file-list + (if (setq curr (assoc tag image-dired-tag-file-list)) + (if (not (member file curr)) + (setcdr curr (cons file (cdr curr)))) + (setcdr image-dired-tag-file-list + (cons (list tag file) (cdr image-dired-tag-file-list)))) + (setq image-dired-tag-file-list (list (list tag file)))))) + +(defun image-dired-display-thumb-properties () + "Display thumbnail properties in the echo area." + (declare (obsolete image-dired-update-header-line "29.1")) + (image-dired-update-header-line)) + +(defvar image-dired-slideshow-count 0 + "Keeping track on number of images in slideshow.") +(make-obsolete-variable 'image-dired-slideshow-count "no longer used." "29.1") + +(defvar image-dired-slideshow-times 0 + "Number of pictures to display in slideshow.") +(make-obsolete-variable 'image-dired-slideshow-times "no longer used." "29.1") + +(define-obsolete-function-alias 'image-dired-create-display-image-buffer + #'ignore "29.1") +(define-obsolete-function-alias 'image-dired-create-gallery-lists + #'image-dired--create-gallery-lists "29.1") +(define-obsolete-function-alias 'image-dired-add-to-file-comment-list + #'image-dired--add-to-file-comment-list "29.1") +(define-obsolete-function-alias 'image-dired-add-to-tag-file-lists + #'image-dired--add-to-tag-file-lists "29.1") +(define-obsolete-function-alias 'image-dired-hidden-p + #'image-dired--hidden-p "29.1") + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;; TEST-SECTION ;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; (defvar image-dired-dir-max-size 12300000) + +;; (defun image-dired-test-clean-old-files () +;; "Clean `image-dired-dir' from old thumbnail files. +;; \"Oldness\" measured using last access time. If the total size of all +;; thumbnail files in `image-dired-dir' is larger than 'image-dired-dir-max-size', +;; old files are deleted until the max size is reached." +;; (let* ((files +;; (sort +;; (mapcar +;; (lambda (f) +;; (let ((fattribs (file-attributes f))) +;; `(,(file-attribute-access-time fattribs) +;; ,(file-attribute-size fattribs) ,f))) +;; (directory-files (image-dired-dir) t ".+\\.thumb\\..+$")) +;; ;; Sort function. Compare time between two files. +;; (lambda (l1 l2) +;; (time-less-p (car l1) (car l2))))) +;; (dirsize (apply '+ (mapcar (lambda (x) (cadr x)) files)))) +;; (while (> dirsize image-dired-dir-max-size) +;; (y-or-n-p +;; (format "Size of thumbnail directory: %d, delete old file %s? " +;; dirsize (cadr (cdar files)))) +;; (delete-file (cadr (cdar files))) +;; (setq dirsize (- dirsize (car (cdar files)))) +;; (setq files (cdr files))))) + +(provide 'image-dired) + +;;; image-dired.el ends here diff --git a/lisp/image/image-dired.el b/lisp/image/image-dired.el new file mode 100644 index 0000000000..9f12354111 --- /dev/null +++ b/lisp/image/image-dired.el @@ -0,0 +1,3080 @@ +;;; image-dired.el --- use dired to browse and manipulate your images -*- lexical-binding: t -*- + +;; Copyright (C) 2005-2022 Free Software Foundation, Inc. + +;; Version: 0.4.11 +;; Keywords: multimedia +;; Author: Mathias Dahl + +;; 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: + +;; BACKGROUND +;; ========== +;; +;; I needed a program to browse, organize and tag my pictures. I got +;; tired of the old gallery program I used as it did not allow +;; multi-file operations easily. Also, it put things out of my +;; control. Image viewing programs I tested did not allow multi-file +;; operations or did not do what I wanted it to. +;; +;; So, I got the idea to use the wonderful functionality of Emacs and +;; `dired' to do it. It would allow me to do almost anything I wanted, +;; which is basically just to browse all my pictures in an easy way, +;; letting me manipulate and tag them in various ways. `dired' already +;; provide all the file handling and navigation facilities; I only +;; needed to add some functions to display the images. +;; +;; I briefly tried out thumbs.el, and although it seemed more +;; powerful than this package, it did not work the way I wanted to. It +;; was too slow to create thumbnails of all files in a directory (I +;; currently keep all my 2000+ images in the same directory) and +;; browsing the thumbnail buffer was slow too. image-dired.el will not +;; create thumbnails until they are needed and the browsing is done +;; quickly and easily in Dired. I copied a great deal of ideas and +;; code from there though... :) +;; +;; `image-dired' stores the thumbnail files in `image-dired-dir' +;; using the file name format ORIGNAME.thumb.ORIGEXT. For example +;; ~/.emacs.d/image-dired/myimage01.thumb.jpg. The "database" is for +;; now just a plain text file with the following format: +;; +;; file-name-non-directory;comment:comment-text;tag1;tag2;tag3;...;tagN +;; +;; +;; PREREQUISITES +;; ============= +;; +;; * The GraphicsMagick or ImageMagick package; Image-Dired uses +;; whichever is available. +;; +;; A) For GraphicsMagick, `gm' is used. +;; Find it here: http://www.graphicsmagick.org/ +;; +;; B) For ImageMagick, `convert' and `mogrify' are used. +;; Find it here: https://www.imagemagick.org. +;; +;; * For non-lossy rotation of JPEG images, the JpegTRAN program is +;; needed. +;; +;; * For `image-dired-set-exif-data' to work, the command line tool `exiftool' is +;; needed. It can be found here: https://exiftool.org/. This +;; function is, among other things, used for writing comments to +;; image files using `image-dired-thumbnail-set-image-description'. +;; +;; +;; USAGE +;; ===== +;; +;; This information has been moved to the manual. Type `C-h r' to open +;; the Emacs manual and go to the node Thumbnails by typing `g +;; Image-Dired RET'. +;; +;; Quickstart: M-x image-dired RET DIRNAME RET +;; +;; where DIRNAME is a directory containing image files. +;; +;; LIMITATIONS +;; =========== +;; +;; * Supports all image formats that Emacs and convert supports, but +;; the thumbnails are hard-coded to JPEG or PNG format. It uses +;; JPEG by default, but can optionally follow the Thumbnail Managing +;; Standard (v0.9.0, Dec 2020), which mandates PNG. See the user +;; option `image-dired-thumbnail-storage'. +;; +;; * WARNING: The "database" format used might be changed so keep a +;; backup of `image-dired-db-file' when testing new versions. +;; +;; TODO +;; ==== +;; +;; * Investigate if it is possible to also write the tags to the image +;; files. +;; +;; * From thumbs.el: Add an option for clean-up/max-size functionality +;; for thumbnail directory. +;; +;; * From thumbs.el: Add setroot function. +;; +;; * Add `image-dired-display-thumbs-ring' and functions to cycle that. Find out +;; which is best, saving old batch just before inserting new, or +;; saving the current batch in the ring when inserting it. Adding +;; it probably needs rewriting `image-dired-display-thumbs' to be more general. +;; +;; * Find some way of toggling on and off really nice keybindings in +;; Dired (for example, using C-n or instead of C-S-n). +;; Richard suggested that we could keep C-t as prefix for +;; image-dired commands as it is currently not used in Dired. He +;; also suggested that `dired-next-line' and `dired-previous-line' +;; figure out if image-dired is enabled in the current buffer and, +;; if it is, call `image-dired-dired-next-line' and `image-dired-dired-previous-line', +;; respectively. Update: This is partly done; some bindings have +;; now been added to Dired. +;; +;; * In some way keep track of buffers and windows and stuff so that +;; it works as the user expects. +;; +;; * More/better documentation. + +;;; Code: + +(require 'dired) +(require 'exif) +(require 'image-mode) +(require 'widget) +(require 'xdg) + +(eval-when-compile + (require 'cl-lib) + (require 'wid-edit)) + + +;;; Customizable variables + +(defgroup image-dired nil + "Use Dired to browse your images as thumbnails, and more." + :prefix "image-dired-" + :link '(info-link "(emacs) Image-Dired") + :group 'multimedia) + +(defcustom image-dired-dir (locate-user-emacs-file "image-dired/") + "Directory where thumbnail images are stored. + +The value of this option will be ignored if Image-Dired is +customized to use the Thumbnail Managing Standard; they will be +saved in \"$XDG_CACHE_HOME/thumbnails/\" instead. See +`image-dired-thumbnail-storage'." + :type 'directory) + +(defcustom image-dired-thumbnail-storage 'use-image-dired-dir + "How `image-dired' stores thumbnail files. +There are two ways that Image-Dired can store and generate +thumbnails. If you set this variable to one of the two following +values, they will be stored in the JPEG format: + +- `use-image-dired-dir' means that the thumbnails are stored in a + central directory. + +- `per-directory' means that each thumbnail is stored in a + subdirectory called \".image-dired\" in the same directory + where the image file is. + +It can also use the \"Thumbnail Managing Standard\", which allows +sharing of thumbnails across different programs. Thumbnails will +be stored in \"$XDG_CACHE_HOME/thumbnails/\" instead of in +`image-dired-dir'. Thumbnails are saved in the PNG format, and +can be one of the following sizes: + +- `standard' means use thumbnails sized 128x128. +- `standard-large' means use thumbnails sized 256x256. +- `standard-x-large' means use thumbnails sized 512x512. +- `standard-xx-large' means use thumbnails sized 1024x1024. + +For more information on the Thumbnail Managing Standard, see: +https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html" + :type '(choice :tag "How to store thumbnail files" + (const :tag "Use image-dired-dir" use-image-dired-dir) + (const :tag "Thumbnail Managing Standard (normal 128x128)" + standard) + (const :tag "Thumbnail Managing Standard (large 256x256)" + standard-large) + (const :tag "Thumbnail Managing Standard (larger 512x512)" + standard-x-large) + (const :tag "Thumbnail Managing Standard (extra large 1024x1024)" + standard-xx-large) + (const :tag "Per-directory" per-directory)) + :version "29.1") + +(defconst image-dired--thumbnail-standard-sizes + '( standard standard-large + standard-x-large standard-xx-large) + "List of symbols representing thumbnail sizes in Thumbnail Managing Standard.") + +(defcustom image-dired-db-file + (expand-file-name ".image-dired_db" image-dired-dir) + "Database file where file names and their associated tags are stored." + :type 'file) + +(defcustom image-dired-cmd-create-thumbnail-program + (if (executable-find "gm") "gm" "convert") + "Executable used to create thumbnail. +Used together with `image-dired-cmd-create-thumbnail-options'." + :type 'file + :version "29.1") + +(defcustom image-dired-cmd-create-thumbnail-options + (let ((opts '("-size" "%wx%h" "%f[0]" + "-resize" "%wx%h>" + "-strip" "jpeg:%t"))) + (if (executable-find "gm") (cons "convert" opts) opts)) + "Options of command used to create thumbnail image. +Used with `image-dired-cmd-create-thumbnail-program'. +Available format specifiers are: %w which is replaced by +`image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', +%f which is replaced by the file name of the original image and %t +which is replaced by the file name of the thumbnail file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-pngnq-program + ;; Prefer pngquant to pngnq-s9 as it is faster on my machine. + ;; The project also seems more active than the alternatives. + ;; Prefer pngnq-s9 to pngnq as it fixes bugs in pngnq. + ;; The pngnq project seems dead (?) since 2011 or so. + (or (executable-find "pngquant") + (executable-find "pngnq-s9") + (executable-find "pngnq")) + "The file name of the `pngquant' or `pngnq' program. +It quantizes colors of PNG images down to 256 colors or fewer +using the NeuQuant algorithm." + :version "29.1" + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-pngnq-options + (if (executable-find "pngquant") + '("--ext" "-nq8.png" "%t") ; same extension as "pngnq" + '("-f" "%t")) + "Arguments to pass `image-dired-cmd-pngnq-program'. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options'." + :type '(repeat (string :tag "Argument")) + :version "29.1") + +(defcustom image-dired-cmd-pngcrush-program (executable-find "pngcrush") + "The file name of the `pngcrush' program. +It optimizes the compression of PNG images. Also it adds PNG textual chunks +with the information required by the Thumbnail Managing Standard." + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-pngcrush-options + `("-q" + "-text" "b" "Description" "Thumbnail of file://%f" + "-text" "b" "Software" ,(emacs-version) + ;; "-text b \"Thumb::Image::Height\" \"%oh\" " + ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" " + ;; "-text b \"Thumb::Image::Width\" \"%ow\" " + "-text" "b" "Thumb::MTime" "%m" + ;; "-text b \"Thumb::Size\" \"%b\" " + "-text" "b" "Thumb::URI" "file://%f" + "%q" "%t") + "Arguments for `image-dired-cmd-pngcrush-program'. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options', with %q for a +temporary file name (typically generated by pnqnq)." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-optipng-program (executable-find "optipng") + "The file name of the `optipng' program." + :version "26.1" + :type '(choice (const :tag "Not Set" nil) file)) + +(defcustom image-dired-cmd-optipng-options '("-o5" "%t") + "Arguments passed to `image-dired-cmd-optipng-program'. +Available format specifiers are described in +`image-dired-cmd-create-thumbnail-options'." + :version "26.1" + :type '(repeat (string :tag "Argument")) + :link '(url-link "man:optipng(1)")) + +(defcustom image-dired-cmd-create-standard-thumbnail-options + (append '("-size" "%wx%h" "%f[0]") + (unless (or image-dired-cmd-pngcrush-program + image-dired-cmd-pngnq-program) + (list + "-set" "Thumb::MTime" "%m" + "-set" "Thumb::URI" "file://%f" + "-set" "Description" "Thumbnail of file://%f" + "-set" "Software" (emacs-version))) + '("-thumbnail" "%wx%h>" "png:%t")) + "Options for creating thumbnails according to the Thumbnail Managing Standard. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options', with %m for file modification time." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-cmd-rotate-original-program + "jpegtran" + "Executable used to rotate original image. +Used together with `image-dired-cmd-rotate-original-options'." + :type 'file) + +(defcustom image-dired-cmd-rotate-original-options + '("-rotate" "%d" "-copy" "all" "-outfile" "%t" "%o") + "Arguments of command used to rotate original image. +Used with `image-dired-cmd-rotate-original-program'. +Available format specifiers are: %d which is replaced by the +number of (positive) degrees to rotate the image, normally 90 or +270 \(for 90 degrees right and left), %o which is replaced by the +original image file name and %t which is replaced by +`image-dired-temp-image-file'." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-temp-rotate-image-file + (expand-file-name ".image-dired_rotate_temp" image-dired-dir) + "Temporary file for rotate operations." + :type 'file) + +(defcustom image-dired-rotate-original-ask-before-overwrite t + "Confirm overwrite of original file after rotate operation. +If non-nil, ask user for confirmation before overwriting the +original file with `image-dired-temp-rotate-image-file'." + :type 'boolean) + +(defcustom image-dired-cmd-write-exif-data-program + "exiftool" + "Program used to write EXIF data to image. +Used together with `image-dired-cmd-write-exif-data-options'." + :type 'file) + +(defcustom image-dired-cmd-write-exif-data-options + '("-%t=%v" "%f") + "Arguments of command used to write EXIF data. +Used with `image-dired-cmd-write-exif-data-program'. +Available format specifiers are: %f which is replaced by +the image file name, %t which is replaced by the tag name and %v +which is replaced by the tag value." + :version "26.1" + :type '(repeat (string :tag "Argument"))) + +(defcustom image-dired-thumb-size + (cond + ((eq 'standard image-dired-thumbnail-storage) 128) + ((eq 'standard-large image-dired-thumbnail-storage) 256) + ((eq 'standard-x-large image-dired-thumbnail-storage) 512) + ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) + (t 100)) + "Size of thumbnails, in pixels. +This is the default size for both `image-dired-thumb-width' +and `image-dired-thumb-height'. + +The value of this option will be ignored if Image-Dired is +customized to use the Thumbnail Managing Standard; the standard +sizes will be used instead. See `image-dired-thumbnail-storage'." + :type 'integer) + +(defcustom image-dired-thumb-width image-dired-thumb-size + "Width of thumbnails, in pixels." + :type 'integer) + +(defcustom image-dired-thumb-height image-dired-thumb-size + "Height of thumbnails, in pixels." + :type 'integer) + +(defcustom image-dired-thumb-relief 2 + "Size of button-like border around thumbnails." + :type 'integer) + +(defcustom image-dired-thumb-margin 2 + "Size of the margin around thumbnails. +This is where you see the cursor." + :type 'integer) + +(defcustom image-dired-thumb-visible-marks t + "Make marks and flags visible in thumbnail buffer. +If non-nil, apply the `image-dired-thumb-mark' face to marked +images and `image-dired-thumb-flagged' to images flagged for +deletion." + :type 'boolean + :version "28.1") + +(defface image-dired-thumb-mark + '((((class color) (min-colors 16)) :background "DarkOrange") + (((class color)) :foreground "yellow")) + "Face for marked images in thumbnail buffer." + :version "29.1") + +(defface image-dired-thumb-flagged + '((((class color) (min-colors 88) (background light)) :background "Red3") + (((class color) (min-colors 88) (background dark)) :background "Pink") + (((class color) (min-colors 16) (background light)) :background "Red3") + (((class color) (min-colors 16) (background dark)) :background "Pink") + (((class color) (min-colors 8)) :background "red") + (t :inverse-video t)) + "Face for images flagged for deletion in thumbnail buffer." + :version "29.1") + +(defcustom image-dired-line-up-method 'dynamic + "Default method for line-up of thumbnails in thumbnail buffer. +Used by `image-dired-display-thumbs' and other functions that needs +to line-up thumbnails. Dynamic means to use the available width of +the window containing the thumbnail buffer, Fixed means to use +`image-dired-thumbs-per-row', Interactive is for asking the user, +and No line-up means that no automatic line-up will be done." + :type '(choice :tag "Default line-up method" + (const :tag "Dynamic" dynamic) + (const :tag "Fixed" fixed) + (const :tag "Interactive" interactive) + (const :tag "No line-up" none))) + +(defcustom image-dired-thumbs-per-row 3 + "Number of thumbnails to display per row in thumb buffer." + :type 'integer) + +(defcustom image-dired-track-movement t + "The current state of the tracking and mirroring. +For more information, see the documentation for +`image-dired-toggle-movement-tracking'." + :type 'boolean) + +(defcustom image-dired-append-when-browsing nil + "Append thumbnails in thumbnail buffer when browsing. +If non-nil, using `image-dired-next-line-and-display' and +`image-dired-previous-line-and-display' will leave a trail of thumbnail +images in the thumbnail buffer. If you enable this and want to clean +the thumbnail buffer because it is filled with too many thumbnails, +just call `image-dired-display-thumb' to display only the image at point. +This value can be toggled using `image-dired-toggle-append-browsing'." + :type 'boolean) + +(defcustom image-dired-dired-disp-props t + "If non-nil, display properties for Dired file when browsing. +Used by `image-dired-next-line-and-display', +`image-dired-previous-line-and-display' and `image-dired-mark-and-display-next'. +If the database file is large, this can slow down image browsing in +Dired and you might want to turn it off." + :type 'boolean) + +(defcustom image-dired-display-properties-format "%b: %f (%t): %c" + "Display format for thumbnail properties. +%b is replaced with associated Dired buffer name, %f with file +name (without path) of original image file, %t with the list of +tags and %c with the comment." + :type 'string) + +(defcustom image-dired-external-viewer + ;; TODO: Use mailcap, dired-guess-shell-alist-default, + ;; dired-view-command-alist. + (cond ((executable-find "display")) + ((executable-find "xli")) + ((executable-find "qiv") "qiv -t") + ((executable-find "feh") "feh")) + "Name of external viewer. +Including parameters. Used when displaying original image from +`image-dired-thumbnail-mode'." + :version "28.1" + :type '(choice string + (const :tag "Not Set" nil))) + +(defcustom image-dired-main-image-directory + (or (xdg-user-dir "PICTURES") "~/pics/") + "Name of main image directory, if any. +Used by `image-dired-copy-with-exif-file-name'." + :type 'string + :version "29.1") + +(defcustom image-dired-show-all-from-dir-max-files 500 + "Maximum number of files in directory before prompting. + +If there are more image files than this in a selected directory, +the `image-dired-show-all-from-dir' command will ask for +confirmation before creating the thumbnail buffer. If this +variable is nil, it will never ask." + :type '(choice integer + (const :tag "Disable warning" nil)) + :version "29.1") + +(defcustom image-dired-marking-shows-next t + "If non-nil, marking, unmarking or flagging an image shows the next image. + +This affects the following commands: +\\ + `image-dired-flag-thumb-original-file' (bound to \\[image-dired-flag-thumb-original-file]) + `image-dired-mark-thumb-original-file' (bound to \\[image-dired-mark-thumb-original-file]) + `image-dired-unmark-thumb-original-file' (bound to \\[image-dired-unmark-thumb-original-file])" + :type 'boolean + :version "29.1") + + +;;; Util functions + +(defvar image-dired-debug nil + "Non-nil means enable debug messages.") + +(defun image-dired-debug-message (&rest args) + "Display debug message ARGS when `image-dired-debug' is non-nil." + (when image-dired-debug + (apply #'message args))) + +(defmacro image-dired--with-db-file (&rest body) + "Run BODY in a temp buffer containing `image-dired-db-file'. +Return the last form in BODY." + (declare (indent 0) (debug t)) + `(with-temp-buffer + (if (file-exists-p image-dired-db-file) + (insert-file-contents image-dired-db-file)) + ,@body)) + +(defun image-dired-dir () + "Return the current thumbnail directory (from variable `image-dired-dir'). +Create the thumbnail directory if it does not exist." + (let ((image-dired-dir (file-name-as-directory + (expand-file-name image-dired-dir)))) + (unless (file-directory-p image-dired-dir) + (with-file-modes #o700 + (make-directory image-dired-dir t)) + (message "Thumbnail directory created: %s" image-dired-dir)) + image-dired-dir)) + +(defun image-dired-insert-image (file type relief margin) + "Insert image FILE of image TYPE, using RELIEF and MARGIN, at point." + (let ((i `(image :type ,type + :file ,file + :relief ,relief + :margin ,margin))) + (insert-image i))) + +(defun image-dired-get-thumbnail-image (file) + "Return the image descriptor for a thumbnail of image file FILE." + (unless (string-match-p (image-file-name-regexp) file) + (error "%s is not a valid image file" file)) + (let* ((thumb-file (image-dired-thumb-name file)) + (thumb-attr (file-attributes thumb-file))) + (when (or (not thumb-attr) + (time-less-p (file-attribute-modification-time thumb-attr) + (file-attribute-modification-time + (file-attributes file)))) + (image-dired-create-thumb file thumb-file)) + (create-image thumb-file))) + +(defun image-dired-insert-thumbnail (file original-file-name + associated-dired-buffer) + "Insert thumbnail image FILE. +Add text properties ORIGINAL-FILE-NAME and ASSOCIATED-DIRED-BUFFER." + (let (beg end) + (setq beg (point)) + (image-dired-insert-image + file + ;; Thumbnails are created asynchronously, so we might not yet + ;; have a file. But if it exists, it might have been cached from + ;; before and we should use it instead of our current settings. + (or (and (file-exists-p file) + (image-type-from-file-header file)) + (and (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + 'png) + 'jpeg) + image-dired-thumb-relief + image-dired-thumb-margin) + (setq end (point)) + (add-text-properties + beg end + (list 'image-dired-thumbnail t + 'original-file-name original-file-name + 'associated-dired-buffer associated-dired-buffer + 'tags (image-dired-list-tags original-file-name) + 'mouse-face 'highlight + 'comment (image-dired-get-comment original-file-name))))) + +(defun image-dired-thumb-name (file) + "Return absolute file name for thumbnail FILE. +Depending on the value of `image-dired-thumbnail-storage', the +file name of the thumbnail will vary: +- For `use-image-dired-dir', make a SHA1-hash of the image file's + directory name and add that to make the thumbnail file name + unique. +- For `per-directory' storage, just add a subdirectory. +- For `standard' storage, produce the file name according to the + Thumbnail Managing Standard. Among other things, an MD5-hash + of the image file's directory name will be added to the + filename. +See also `image-dired-thumbnail-storage'." + (cond ((memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + (let ((thumbdir (cl-case image-dired-thumbnail-storage + (standard "thumbnails/normal") + (standard-large "thumbnails/large") + (standard-x-large "thumbnails/x-large") + (standard-xx-large "thumbnails/xx-large")))) + (expand-file-name + ;; MD5 is mandated by the Thumbnail Managing Standard. + (concat (md5 (concat "file://" (expand-file-name file))) ".png") + (expand-file-name thumbdir (xdg-cache-home))))) + ((eq 'use-image-dired-dir image-dired-thumbnail-storage) + (let* ((f (expand-file-name file)) + (hash + (md5 (file-name-as-directory (file-name-directory f))))) + (format "%s%s%s.thumb.%s" + (file-name-as-directory (expand-file-name (image-dired-dir))) + (file-name-base f) + (if hash (concat "_" hash) "") + (file-name-extension f)))) + ((eq 'per-directory image-dired-thumbnail-storage) + (let ((f (expand-file-name file))) + (format "%s.image-dired/%s.thumb.%s" + (file-name-directory f) + (file-name-base f) + (file-name-extension f)))))) + +(defun image-dired--check-executable-exists (executable) + (unless (executable-find (symbol-value executable)) + (error "Executable %S not found" executable))) + + +;;; Creating thumbnails + +(defun image-dired-thumb-size (dimension) + "Return thumb size depending on `image-dired-thumbnail-storage'. +DIMENSION should be either the symbol `width' or `height'." + (cond + ((eq 'standard image-dired-thumbnail-storage) 128) + ((eq 'standard-large image-dired-thumbnail-storage) 256) + ((eq 'standard-x-large image-dired-thumbnail-storage) 512) + ((eq 'standard-xx-large image-dired-thumbnail-storage) 1024) + (t (cl-ecase dimension + (width image-dired-thumb-width) + (height image-dired-thumb-height))))) + +(defvar image-dired--generate-thumbs-start nil + "Time when `display-thumbs' was called.") + +(defvar image-dired-queue nil + "List of items in the queue. +Each item has the form (ORIGINAL-FILE TARGET-FILE).") + +(defvar image-dired-queue-active-jobs 0 + "Number of active jobs in `image-dired-queue'.") + +(defvar image-dired-queue-active-limit (min 4 (max 2 (/ (num-processors) 2))) + "Maximum number of concurrent jobs permitted for generating images. +Increase at own risk. If you want to experiment with this, +consider setting `image-dired-debug' to a non-nil value to see +the time spent on generating thumbnails. Run `image-clear-cache' +and remove the cached thumbnail files between each trial run.") + +(defun image-dired-pngnq-thumb (spec) + "Quantize thumbnail described by format SPEC with pngnq(1)." + (let ((process + (apply #'start-process "image-dired-pngnq" nil + image-dired-cmd-pngnq-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-pngnq-options)))) + (setf (process-sentinel process) + (lambda (process status) + (if (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + ;; Pass off to pngcrush, or just rename the + ;; THUMB-nq8.png file back to THUMB.png + (if (and image-dired-cmd-pngcrush-program + (executable-find image-dired-cmd-pngcrush-program)) + (image-dired-pngcrush-thumb spec) + (let ((nq8 (cdr (assq ?q spec))) + (thumb (cdr (assq ?t spec)))) + (rename-file nq8 thumb t))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))))) + process)) + +(defun image-dired-pngcrush-thumb (spec) + "Optimize thumbnail described by format SPEC with pngcrush(1)." + ;; If pngnq wasn't run, then the THUMB-nq8.png file does not exist. + ;; pngcrush needs an infile and outfile, so we just copy THUMB to + ;; THUMB-nq8.png and use the latter as a temp file. + (when (not image-dired-cmd-pngnq-program) + (let ((temp (cdr (assq ?q spec))) + (thumb (cdr (assq ?t spec)))) + (copy-file thumb temp))) + (let ((process + (apply #'start-process "image-dired-pngcrush" nil + image-dired-cmd-pngcrush-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-pngcrush-options)))) + (setf (process-sentinel process) + (lambda (process status) + (unless (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))) + (when (memq (process-status process) '(exit signal)) + (let ((temp (cdr (assq ?q spec)))) + (delete-file temp))))) + process)) + +(defun image-dired-optipng-thumb (spec) + "Optimize thumbnail described by format SPEC with optipng(1)." + (let ((process + (apply #'start-process "image-dired-optipng" nil + image-dired-cmd-optipng-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-optipng-options)))) + (setf (process-sentinel process) + (lambda (process status) + (unless (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + (message "command %S %s" (process-command process) + (string-replace "\n" "" status))))) + process)) + +(defun image-dired-create-thumb-1 (original-file thumbnail-file) + "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE." + (image-dired--check-executable-exists + 'image-dired-cmd-create-thumbnail-program) + (let* ((width (int-to-string (image-dired-thumb-size 'width))) + (height (int-to-string (image-dired-thumb-size 'height))) + (modif-time (format-time-string + "%s" (file-attribute-modification-time + (file-attributes original-file)))) + (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png" + thumbnail-file)) + (spec + (list + (cons ?w width) + (cons ?h height) + (cons ?m modif-time) + (cons ?f original-file) + (cons ?q thumbnail-nq8-file) + (cons ?t thumbnail-file))) + (thumbnail-dir (file-name-directory thumbnail-file)) + process) + (when (not (file-exists-p thumbnail-dir)) + (with-file-modes #o700 + (make-directory thumbnail-dir t)) + (message "Thumbnail directory created: %s" thumbnail-dir)) + + ;; Thumbnail file creation processes begin here and are marshaled + ;; in a queue by `image-dired-create-thumb'. + (setq process + (apply #'start-process "image-dired-create-thumbnail" nil + image-dired-cmd-create-thumbnail-program + (mapcar + (lambda (arg) (format-spec arg spec)) + (if (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + image-dired-cmd-create-standard-thumbnail-options + image-dired-cmd-create-thumbnail-options)))) + + (setf (process-sentinel process) + (lambda (process status) + ;; Trigger next in queue once a thumbnail has been created + (cl-decf image-dired-queue-active-jobs) + (image-dired-thumb-queue-run) + (when (= image-dired-queue-active-jobs 0) + (image-dired-debug-message + (format-time-string + "Generated thumbnails in %s.%3N seconds" + (time-subtract nil + image-dired--generate-thumbs-start)))) + (if (not (and (eq (process-status process) 'exit) + (zerop (process-exit-status process)))) + (message "Thumb could not be created for %s: %s" + (abbreviate-file-name original-file) + (string-replace "\n" "" status)) + (set-file-modes thumbnail-file #o600) + (clear-image-cache thumbnail-file) + ;; PNG thumbnail has been created since we are + ;; following the XDG thumbnail spec, so try to optimize + (when (memq image-dired-thumbnail-storage + image-dired--thumbnail-standard-sizes) + (cond + ((and image-dired-cmd-pngnq-program + (executable-find image-dired-cmd-pngnq-program)) + (image-dired-pngnq-thumb spec)) + ((and image-dired-cmd-pngcrush-program + (executable-find image-dired-cmd-pngcrush-program)) + (image-dired-pngcrush-thumb spec)) + ((and image-dired-cmd-optipng-program + (executable-find image-dired-cmd-optipng-program)) + (image-dired-optipng-thumb spec))))))) + process)) + +(defun image-dired-thumb-queue-run () + "Run a queued job if one exists and not too many jobs are running. +Queued items live in `image-dired-queue'." + (while (and image-dired-queue + (< image-dired-queue-active-jobs + image-dired-queue-active-limit)) + (cl-incf image-dired-queue-active-jobs) + (apply #'image-dired-create-thumb-1 (pop image-dired-queue)))) + +(defun image-dired-create-thumb (original-file thumbnail-file) + "Add a job for generating ORIGINAL-FILE thumbnail to `image-dired-queue'. +The new file will be named THUMBNAIL-FILE." + (setq image-dired-queue + (nconc image-dired-queue + (list (list original-file thumbnail-file)))) + (run-at-time 0 nil #'image-dired-thumb-queue-run)) + +(defmacro image-dired--with-marked (&rest body) + "Eval BODY with point on each marked thumbnail. +If no marked file could be found, execute BODY on the current +thumbnail." + `(with-current-buffer image-dired-thumbnail-buffer + (let (found) + (save-mark-and-excursion + (goto-char (point-min)) + (while (not (eobp)) + (when (image-dired-thumb-file-marked-p) + (setq found t) + ,@body) + (forward-char))) + (unless found + ,@body)))) + +;;;###autoload +(defun image-dired-dired-toggle-marked-thumbs (&optional arg) + "Toggle thumbnails in front of file names in the Dired buffer. +If no marked file could be found, insert or hide thumbnails on the +current line. ARG, if non-nil, specifies the files to use instead +of the marked files. If ARG is an integer, use the next ARG (or +previous -ARG, if ARG<0) files." + (interactive "P") + (dired-map-over-marks + (let ((image-pos (dired-move-to-filename)) + (image-file (dired-get-filename nil t)) + thumb-file + overlay) + (when (and image-file + (string-match-p (image-file-name-regexp) image-file)) + (setq thumb-file (image-dired-get-thumbnail-image image-file)) + ;; If image is not already added, then add it. + (let ((thumb-ov (cl-loop for ov in (overlays-in (point) (1+ (point))) + if (overlay-get ov 'thumb-file) return ov))) + (if thumb-ov + (delete-overlay thumb-ov) + (put-image thumb-file image-pos) + (setq overlay + (cl-loop for ov in (overlays-in (point) (1+ (point))) + if (overlay-get ov 'put-image) return ov)) + (overlay-put overlay 'image-file image-file) + (overlay-put overlay 'thumb-file thumb-file))))) + arg ; Show or hide image on ARG next files. + 'show-progress) ; Update dired display after each image is updated. + (add-hook 'dired-after-readin-hook + 'image-dired-dired-after-readin-hook nil t)) + +(defun image-dired-dired-after-readin-hook () + "Relocate existing thumbnail overlays in Dired buffer after reverting. +Move them to their corresponding files if they still exist. +Otherwise, delete overlays." + (mapc (lambda (overlay) + (when (overlay-get overlay 'put-image) + (let* ((image-file (overlay-get overlay 'image-file)) + (image-pos (dired-goto-file image-file))) + (if image-pos + (move-overlay overlay image-pos image-pos) + (delete-overlay overlay))))) + (overlays-in (point-min) (point-max)))) + +(defun image-dired-next-line-and-display () + "Move to next Dired line and display thumbnail image." + (interactive) + (dired-next-line 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-previous-line-and-display () + "Move to previous Dired line and display thumbnail image." + (interactive) + (dired-previous-line 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-toggle-append-browsing () + "Toggle `image-dired-append-when-browsing'." + (interactive) + (setq image-dired-append-when-browsing + (not image-dired-append-when-browsing)) + (message "Append browsing %s" + (if image-dired-append-when-browsing + "on" + "off"))) + +(defun image-dired-mark-and-display-next () + "Mark current file in Dired and display next thumbnail image." + (interactive) + (dired-mark 1) + (image-dired-display-thumbs + t (or image-dired-append-when-browsing nil) t) + (if image-dired-dired-disp-props + (image-dired-dired-display-properties))) + +(defun image-dired-toggle-dired-display-properties () + "Toggle `image-dired-dired-disp-props'." + (interactive) + (setq image-dired-dired-disp-props + (not image-dired-dired-disp-props)) + (message "Dired display properties %s" + (if image-dired-dired-disp-props + "on" + "off"))) + +(defvar image-dired-thumbnail-buffer "*image-dired*" + "Image-Dired's thumbnail buffer.") + +(defun image-dired-create-thumbnail-buffer () + "Create thumb buffer and set `image-dired-thumbnail-mode'." + (let ((buf (get-buffer-create image-dired-thumbnail-buffer))) + (with-current-buffer buf + (setq buffer-read-only t) + (if (not (eq major-mode 'image-dired-thumbnail-mode)) + (image-dired-thumbnail-mode))) + buf)) + +(defvar image-dired-display-image-buffer "*image-dired-display-image*" + "Where larger versions of the images are display.") + +(defvar image-dired-saved-window-configuration nil + "Saved window configuration.") + +;;;###autoload +(defun image-dired-dired-with-window-configuration (dir &optional arg) + "Open directory DIR and create a default window configuration. + +Convenience command that: + + - Opens Dired in folder DIR + - Splits windows in most useful (?) way + - Sets `truncate-lines' to t + +After the command has finished, you would typically mark some +image files in Dired and type +\\[image-dired-display-thumbs] (`image-dired-display-thumbs'). + +If called with prefix argument ARG, skip splitting of windows. + +The current window configuration is saved and can be restored by +calling `image-dired-restore-window-configuration'." + (interactive "DDirectory: \nP") + (let ((buf (image-dired-create-thumbnail-buffer)) + (buf2 (get-buffer-create image-dired-display-image-buffer))) + (setq image-dired-saved-window-configuration + (current-window-configuration)) + (dired dir) + (delete-other-windows) + (when (not arg) + (split-window-right) + (setq truncate-lines t) + (save-excursion + (other-window 1) + (pop-to-buffer-same-window buf) + (select-window (split-window-below)) + (pop-to-buffer-same-window buf2) + (other-window -2))))) + +(defun image-dired-restore-window-configuration () + "Restore window configuration. +Restore any changes to the window configuration made by calling +`image-dired-dired-with-window-configuration'." + (interactive nil image-dired-thumbnail-mode) + (if image-dired-saved-window-configuration + (set-window-configuration image-dired-saved-window-configuration) + (message "No saved window configuration"))) + +(defun image-dired--line-up-with-method () + "Line up thumbnails according to `image-dired-line-up-method'." + (cond ((eq 'dynamic image-dired-line-up-method) + (image-dired-line-up-dynamic)) + ((eq 'fixed image-dired-line-up-method) + (image-dired-line-up)) + ((eq 'interactive image-dired-line-up-method) + (image-dired-line-up-interactive)) + ((eq 'none image-dired-line-up-method) + nil) + (t + (image-dired-line-up-dynamic)))) + +;;;###autoload +(defun image-dired-display-thumbs (&optional arg append do-not-pop) + "Display thumbnails of all marked files, in `image-dired-thumbnail-buffer'. +If a thumbnail image does not exist for a file, it is created on the +fly. With prefix argument ARG, display only thumbnail for file at +point (this is useful if you have marked some files but want to show +another one). + +Recommended usage is to split the current frame horizontally so that +you have the Dired buffer in the left window and the +`image-dired-thumbnail-buffer' buffer in the right window. + +With optional argument APPEND, append thumbnail to thumbnail buffer +instead of erasing it first. + +Optional argument DO-NOT-POP controls if `pop-to-buffer' should be +used or not. If non-nil, use `display-buffer' instead of +`pop-to-buffer'. This is used from functions like +`image-dired-next-line-and-display' and +`image-dired-previous-line-and-display' where we do not want the +thumbnail buffer to be selected." + (interactive "P") + (setq image-dired--generate-thumbs-start (current-time)) + (let ((buf (image-dired-create-thumbnail-buffer)) + thumb-name files dired-buf) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (setq dired-buf (current-buffer)) + (with-current-buffer buf + (let ((inhibit-read-only t)) + (if (not append) + (erase-buffer) + (goto-char (point-max))) + (dolist (curr-file files) + (setq thumb-name (image-dired-thumb-name curr-file)) + (when (not (file-exists-p thumb-name)) + (image-dired-create-thumb curr-file thumb-name)) + (image-dired-insert-thumbnail thumb-name curr-file dired-buf))) + (if do-not-pop + (display-buffer buf) + (pop-to-buffer buf)) + (image-dired--line-up-with-method)))) + +;;;###autoload +(defun image-dired-show-all-from-dir (dir) + "Make a thumbnail buffer for all images in DIR and display it. +Any file matching `image-file-name-regexp' is considered an image +file. + +If the number of image files in DIR exceeds +`image-dired-show-all-from-dir-max-files', ask for confirmation +before creating the thumbnail buffer. If that variable is nil, +never ask for confirmation." + (interactive "DImage-Dired: ") + (dired dir) + (dired-mark-files-regexp (image-file-name-regexp)) + (let ((files (dired-get-marked-files nil nil nil t))) + (cond ((and (null (cdr files))) + (message "No image files in directory")) + ((or (not image-dired-show-all-from-dir-max-files) + (<= (length (cdr files)) image-dired-show-all-from-dir-max-files) + (and (> (length (cdr files)) image-dired-show-all-from-dir-max-files) + (y-or-n-p + (format + "Directory contains more than %d image files. Proceed?" + image-dired-show-all-from-dir-max-files)))) + (image-dired-display-thumbs) + (pop-to-buffer image-dired-thumbnail-buffer) + (setq default-directory dir) + (image-dired-unmark-all-marks)) + (t (message "Image-Dired canceled"))))) + +;;;###autoload +(defalias 'image-dired 'image-dired-show-all-from-dir) + + +;;; Tags + +(defun image-dired-sane-db-file () + "Check if `image-dired-db-file' exists. +If not, try to create it (including any parent directories). +Signal error if there are problems creating it." + (or (file-exists-p image-dired-db-file) + (let (dir buf) + (unless (file-directory-p (setq dir (file-name-directory + image-dired-db-file))) + (with-file-modes #o700 + (make-directory dir t))) + (with-current-buffer (setq buf (create-file-buffer + image-dired-db-file)) + (with-file-modes #o600 + (write-file image-dired-db-file))) + (kill-buffer buf) + (file-exists-p image-dired-db-file)) + (error "Could not create %s" image-dired-db-file))) + +(defvar image-dired-tag-history nil "Variable holding the tag history.") + +(defun image-dired-write-tags (file-tags) + "Write file tags to database. +Write each file and tag in FILE-TAGS to the database. +FILE-TAGS is an alist in the following form: + ((FILE . TAG) ... )" + (image-dired-sane-db-file) + (let (end file tag) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (dolist (elt file-tags) + (setq file (car elt) + tag (cdr elt)) + (goto-char (point-min)) + (if (search-forward-regexp (format "^%s.*$" file) nil t) + (progn + (setq end (point)) + (beginning-of-line) + (when (not (search-forward (format ";%s" tag) end t)) + (end-of-line) + (insert (format ";%s" tag)))) + (goto-char (point-max)) + (insert (format "%s;%s\n" file tag)))) + (save-buffer)))) + +(defun image-dired-remove-tag (files tag) + "For all FILES, remove TAG from the image database." + (image-dired-sane-db-file) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (let (end) + (unless (listp files) + (if (stringp files) + (setq files (list files)) + (error "Files must be a string or a list of strings!"))) + (dolist (file files) + (goto-char (point-min)) + (when (search-forward-regexp (format "^%s;" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (search-forward-regexp + (format "\\(;%s\\)\\($\\|;\\)" tag) end t) + (delete-region (match-beginning 1) (match-end 1)) + ;; Check if file should still be in the database. If + ;; it has no tags or comments, it will be removed. + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (not (search-forward ";" end t)) + (kill-line 1)))))) + (save-buffer))) + +(defun image-dired-list-tags (file) + "Read all tags for image FILE from the image database." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end (tags "")) + (when (search-forward-regexp (format "^%s" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (if (search-forward ";" end t) + (if (search-forward "comment:" end t) + (if (search-forward ";" end t) + (setq tags (buffer-substring (point) end))) + (setq tags (buffer-substring (point) end))))) + (split-string tags ";")))) + +;;;###autoload +(defun image-dired-tag-files (arg) + "Tag marked file(s) in Dired. With prefix ARG, tag file at point." + (interactive "P") + (let ((tag (completing-read + "Tags to add (separate tags with a semicolon): " + image-dired-tag-history nil nil nil 'image-dired-tag-history)) + files) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (image-dired-write-tags + (mapcar + (lambda (x) + (cons x tag)) + files)))) + +(defun image-dired-tag-thumbnail () + "Tag current or marked thumbnails." + (interactive) + (let ((tag (completing-read + "Tags to add (separate tags with a semicolon): " + image-dired-tag-history nil nil nil 'image-dired-tag-history))) + (image-dired--with-marked + (image-dired-write-tags + (list (cons (image-dired-original-file-name) tag))) + (image-dired-update-property + 'tags (image-dired-list-tags (image-dired-original-file-name)))))) + +;;;###autoload +(defun image-dired-delete-tag (arg) + "Remove tag for selected file(s). +With prefix argument ARG, remove tag from file at point." + (interactive "P") + (let ((tag (completing-read "Tag to remove: " image-dired-tag-history + nil nil nil 'image-dired-tag-history)) + files) + (if arg + (setq files (list (dired-get-filename))) + (setq files (dired-get-marked-files))) + (image-dired-remove-tag files tag))) + +(defun image-dired-tag-thumbnail-remove () + "Remove tag from current or marked thumbnails." + (interactive) + (let ((tag (completing-read "Tag to remove: " image-dired-tag-history + nil nil nil 'image-dired-tag-history))) + (image-dired--with-marked + (image-dired-remove-tag (image-dired-original-file-name) tag) + (image-dired-update-property + 'tags (image-dired-list-tags (image-dired-original-file-name)))))) + + +;;; Thumbnail mode (cont.) + +(defun image-dired-original-file-name () + "Get original file name for thumbnail or display image at point." + (get-text-property (point) 'original-file-name)) + +(defun image-dired-file-name-at-point () + "Get abbreviated file name for thumbnail or display image at point." + (let ((f (image-dired-original-file-name))) + (when f + (abbreviate-file-name f)))) + +(defun image-dired-associated-dired-buffer () + "Get associated Dired buffer at point." + (get-text-property (point) 'associated-dired-buffer)) + +(defun image-dired-get-buffer-window (buf) + "Return window where buffer BUF is." + (get-window-with-predicate + (lambda (window) + (equal (window-buffer window) buf)) + nil t)) + +(defun image-dired-track-original-file () + "Track the original file in the associated Dired buffer. +See documentation for `image-dired-toggle-movement-tracking'. +Interactive use only useful if `image-dired-track-movement' is nil." + (interactive) + (let* ((dired-buf (image-dired-associated-dired-buffer)) + (file-name (image-dired-original-file-name)) + (window (image-dired-get-buffer-window dired-buf))) + (and (buffer-live-p dired-buf) file-name + (with-current-buffer dired-buf + (if (not (dired-goto-file file-name)) + (message "Could not track file") + (if window (set-window-point window (point)))))))) + +(defun image-dired-toggle-movement-tracking () + "Turn on and off `image-dired-track-movement'. +Tracking of the movements between thumbnail and Dired buffer so that +they are \"mirrored\" in the dired buffer. When this is on, moving +around in the thumbnail or dired buffer will find the matching +position in the other buffer." + (interactive) + (setq image-dired-track-movement (not image-dired-track-movement)) + (message "Movement tracking %s" (if image-dired-track-movement "on" "off"))) + +(defun image-dired-track-thumbnail () + "Track current Dired file's thumb in `image-dired-thumbnail-buffer'. +This is almost the same as what `image-dired-track-original-file' does, +but the other way around." + (let ((file (dired-get-filename)) + prop-val found window) + (when (get-buffer image-dired-thumbnail-buffer) + (with-current-buffer image-dired-thumbnail-buffer + (goto-char (point-min)) + (while (and (not (eobp)) + (not found)) + (if (and (setq prop-val + (get-text-property (point) 'original-file-name)) + (string= prop-val file)) + (setq found t)) + (if (not found) + (forward-char 1))) + (when found + (if (setq window (image-dired-thumbnail-window)) + (set-window-point window (point))) + (image-dired-update-header-line)))))) + +(defun image-dired-dired-next-line (&optional arg) + "Call `dired-next-line', then track thumbnail. +This can safely replace `dired-next-line'. +With prefix argument, move ARG lines." + (interactive "P") + (dired-next-line (or arg 1)) + (if image-dired-track-movement + (image-dired-track-thumbnail))) + +(defun image-dired-dired-previous-line (&optional arg) + "Call `dired-previous-line', then track thumbnail. +This can safely replace `dired-previous-line'. +With prefix argument, move ARG lines." + (interactive "P") + (dired-previous-line (or arg 1)) + (if image-dired-track-movement + (image-dired-track-thumbnail))) + +(defun image-dired--display-thumb-properties-fun () + (let ((old-buf (current-buffer)) + (old-point (point))) + (lambda () + (when (and (equal (current-buffer) old-buf) + (= (point) old-point)) + (ignore-errors + (image-dired-update-header-line)))))) + +(defun image-dired-forward-image (&optional arg wrap-around) + "Move to next image and display properties. +Optional prefix ARG says how many images to move; the default is +one image. Negative means move backwards. +On reaching end or beginning of buffer, stop and show a message. + +If optional argument WRAP-AROUND is non-nil, wrap around: if +point is on the last image, move to the last one and vice versa." + (interactive "p") + (setq arg (or arg 1)) + (let (pos) + (dotimes (_ (abs arg)) + (if (and (not (if (> arg 0) (eobp) (bobp))) + (save-excursion + (forward-char (if (> arg 0) 1 -1)) + (while (and (not (if (> arg 0) (eobp) (bobp))) + (not (image-dired-image-at-point-p))) + (forward-char (if (> arg 0) 1 -1))) + (setq pos (point)) + (image-dired-image-at-point-p))) + (progn (goto-char pos) + (image-dired-update-header-line)) + (if wrap-around + (progn (goto-char (if (> arg 0) + (point-min) + ;; There are two spaces after the last image. + (- (point-max) 2))) + (image-dired-update-header-line)) + (message "At %s image" (if (> arg 0) "last" "first")) + (run-at-time 1 nil (image-dired--display-thumb-properties-fun)))))) + (when image-dired-track-movement + (image-dired-track-original-file))) + +(defun image-dired-backward-image (&optional arg) + "Move to previous image and display properties. +Optional prefix ARG says how many images to move; the default is +one image. Negative means move forward. +On reaching end or beginning of buffer, stop and show a message." + (interactive "p") + (image-dired-forward-image (- (or arg 1)))) + +(defun image-dired-next-line () + "Move to next line and display properties." + (interactive nil image-dired-thumbnail-mode) + (let ((goal-column (current-column))) + (forward-line 1) + (move-to-column goal-column)) + ;; If we end up in an empty spot, back up to the next thumbnail. + (if (not (image-dired-image-at-point-p)) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + + +(defun image-dired-previous-line () + "Move to previous line and display properties." + (interactive nil image-dired-thumbnail-mode) + (let ((goal-column (current-column))) + (forward-line -1) + (move-to-column goal-column)) + ;; If we end up in an empty spot, back up to the next + ;; thumbnail. This should only happen if the user deleted a + ;; thumbnail and did not refresh, so it is not very common. But we + ;; can handle it in a good manner, so why not? + (if (not (image-dired-image-at-point-p)) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-beginning-of-buffer () + "Move to the first image in the buffer and display properties." + (interactive nil image-dired-thumbnail-mode) + (goto-char (point-min)) + (while (and (not (image-at-point-p)) + (not (eobp))) + (forward-char 1)) + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-end-of-buffer () + "Move to the last image in the buffer and display properties." + (interactive nil image-dired-thumbnail-mode) + (goto-char (point-max)) + (while (and (not (image-at-point-p)) + (not (bobp))) + (forward-char -1)) + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + +(defun image-dired-format-properties-string (buf file props comment) + "Format display properties. +BUF is the associated Dired buffer, FILE is the original image file +name, PROPS is a stringified list of tags and COMMENT is the image file's +comment." + (format-spec + image-dired-display-properties-format + (list + (cons ?b (or buf "")) + (cons ?f file) + (cons ?t (or props "")) + (cons ?c (or comment ""))))) + +(defun image-dired-update-header-line () + "Update image information in the header line." + (when (and (not (eobp)) + (memq major-mode '(image-dired-thumbnail-mode + image-dired-display-image-mode))) + (let ((file-name (file-name-nondirectory (image-dired-original-file-name))) + (dired-buf (buffer-name (image-dired-associated-dired-buffer))) + (props (mapconcat #'identity (get-text-property (point) 'tags) ", ")) + (comment (get-text-property (point) 'comment)) + (message-log-max nil)) + (if file-name + (setq header-line-format + (image-dired-format-properties-string + dired-buf + file-name + props + comment)))))) + +(defun image-dired-dired-file-marked-p (&optional marker) + "In Dired, return t if file on current line is marked. +If optional argument MARKER is non-nil, it is a character to look +for. The default is to look for `dired-marker-char'." + (setq marker (or marker dired-marker-char)) + (save-excursion + (beginning-of-line) + (and (looking-at dired-re-mark) + (= (aref (match-string 0) 0) marker)))) + +(defun image-dired-dired-file-flagged-p () + "In Dired, return t if file on current line is flagged for deletion." + (image-dired-dired-file-marked-p dired-del-marker)) + +(defmacro image-dired--with-thumbnail-buffer (&rest body) + (declare (indent defun) (debug t)) + `(if-let ((buf (get-buffer image-dired-thumbnail-buffer))) + (with-current-buffer buf + (if-let ((win (get-buffer-window buf))) + (with-selected-window win + ,@body) + ,@body)) + (user-error "No such buffer: %s" image-dired-thumbnail-buffer))) + +(defmacro image-dired--on-file-in-dired-buffer (&rest body) + "Run BODY with point on file at point in Dired buffer. +Should be called from commands in `image-dired-thumbnail-mode'." + (declare (indent defun) (debug t)) + `(let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (if (not (and dired-buf file-name)) + (message "No image, or image with correct properties, at point") + (with-current-buffer dired-buf + (when (dired-goto-file file-name) + ,@body + (image-dired-thumb-update-marks)))))) + +(defmacro image-dired--do-mark-command (maybe-next &rest body) + "Helper macro for the mark, unmark and flag commands. +Run BODY in Dired buffer. +If optional argument MAYBE-NEXT is non-nil, show next image +according to `image-dired-marking-shows-next'." + (declare (indent defun) (debug t)) + `(image-dired--with-thumbnail-buffer + (image-dired--on-file-in-dired-buffer + ,@body) + ,(when maybe-next + '(if image-dired-marking-shows-next + (image-dired-display-next-thumbnail-original) + (image-dired-next-line))))) + +(defun image-dired-mark-thumb-original-file () + "Mark original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-mark 1))) + +(defun image-dired-unmark-thumb-original-file () + "Unmark original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-unmark 1))) + +(defun image-dired-flag-thumb-original-file () + "Flag original image file for deletion in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command t + (dired-flag-file-deletion 1))) + +(defun image-dired-toggle-mark-thumb-original-file () + "Toggle mark on original image file in associated Dired buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command nil + (if (image-dired-dired-file-marked-p) + (dired-unmark 1) + (dired-mark 1)))) + +(defun image-dired-unmark-all-marks () + "Remove all marks from all files in associated Dired buffer. +Also update the marks in the thumbnail buffer." + (interactive nil image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--do-mark-command nil + (dired-unmark-all-marks)) + (image-dired--with-thumbnail-buffer + (image-dired-thumb-update-marks))) + +(defun image-dired-jump-original-dired-buffer () + "Jump to the Dired buffer associated with the current image file. +You probably want to use this together with +`image-dired-track-original-file'." + (interactive nil image-dired-thumbnail-mode) + (let ((buf (image-dired-associated-dired-buffer)) + window frame) + (setq window (image-dired-get-buffer-window buf)) + (if window + (progn + (if (not (equal (selected-frame) (setq frame (window-frame window)))) + (select-frame-set-input-focus frame)) + (select-window window)) + (message "Associated dired buffer not visible")))) + +;;;###autoload +(defun image-dired-jump-thumbnail-buffer () + "Jump to thumbnail buffer." + (interactive) + (let ((window (image-dired-thumbnail-window)) + frame) + (if window + (progn + (if (not (equal (selected-frame) (setq frame (window-frame window)))) + (select-frame-set-input-focus frame)) + (select-window window)) + (message "Thumbnail buffer not visible")))) + +(defvar image-dired-thumbnail-mode-line-up-map + (let ((map (make-sparse-keymap))) + ;; map it to "g" so that the user can press it more quickly + (define-key map "g" #'image-dired-line-up-dynamic) + ;; "f" for "fixed" number of thumbs per row + (define-key map "f" #'image-dired-line-up) + ;; "i" for "interactive" + (define-key map "i" #'image-dired-line-up-interactive) + map) + "Keymap for line-up commands in `image-dired-thumbnail-mode'.") + +(defvar image-dired-thumbnail-mode-tag-map + (let ((map (make-sparse-keymap))) + ;; map it to "t" so that the user can press it more quickly + (define-key map "t" #'image-dired-tag-thumbnail) + ;; "r" for "remove" + (define-key map "r" #'image-dired-tag-thumbnail-remove) + map) + "Keymap for tag commands in `image-dired-thumbnail-mode'.") + +(defvar image-dired-thumbnail-mode-map + (let ((map (make-sparse-keymap))) + (define-key map [right] #'image-dired-forward-image) + (define-key map [left] #'image-dired-backward-image) + (define-key map [up] #'image-dired-previous-line) + (define-key map [down] #'image-dired-next-line) + (define-key map "\C-f" #'image-dired-forward-image) + (define-key map "\C-b" #'image-dired-backward-image) + (define-key map "\C-p" #'image-dired-previous-line) + (define-key map "\C-n" #'image-dired-next-line) + + (define-key map "<" #'image-dired-beginning-of-buffer) + (define-key map ">" #'image-dired-end-of-buffer) + (define-key map (kbd "M-<") #'image-dired-beginning-of-buffer) + (define-key map (kbd "M->") #'image-dired-end-of-buffer) + + (define-key map "d" #'image-dired-flag-thumb-original-file) + (define-key map [delete] #'image-dired-flag-thumb-original-file) + (define-key map "m" #'image-dired-mark-thumb-original-file) + (define-key map "u" #'image-dired-unmark-thumb-original-file) + (define-key map "U" #'image-dired-unmark-all-marks) + (define-key map "." #'image-dired-track-original-file) + (define-key map [tab] #'image-dired-jump-original-dired-buffer) + + ;; add line-up map + (define-key map "g" image-dired-thumbnail-mode-line-up-map) + ;; add tag map + (define-key map "t" image-dired-thumbnail-mode-tag-map) + + (define-key map "\C-m" #'image-dired-display-thumbnail-original-image) + (define-key map [C-return] #'image-dired-thumbnail-display-external) + + (define-key map "L" #'image-dired-rotate-original-left) + (define-key map "R" #'image-dired-rotate-original-right) + + (define-key map "D" #'image-dired-thumbnail-set-image-description) + (define-key map "S" #'image-dired-slideshow-start) + (define-key map "\C-d" #'image-dired-delete-char) + (define-key map " " #'image-dired-display-next-thumbnail-original) + (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) + (define-key map "c" #'image-dired-comment-thumbnail) + + ;; Mouse + (define-key map [mouse-2] #'image-dired-mouse-display-image) + (define-key map [mouse-1] #'image-dired-mouse-select-thumbnail) + (define-key map [mouse-3] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-1] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-2] #'image-dired-mouse-select-thumbnail) + (define-key map [down-mouse-3] #'image-dired-mouse-select-thumbnail) + ;; Seems I must first set C-down-mouse-1 to undefined, or else it + ;; will trigger the buffer menu. If I try to instead bind + ;; C-down-mouse-1 to `image-dired-mouse-toggle-mark', I get a message + ;; about C-mouse-1 not being defined afterwards. Annoying, but I + ;; probably do not completely understand mouse events. + (define-key map [C-down-mouse-1] #'undefined) + (define-key map [C-mouse-1] #'image-dired-mouse-toggle-mark) + map) + "Keymap for `image-dired-thumbnail-mode'.") + +(easy-menu-define image-dired-thumbnail-mode-menu image-dired-thumbnail-mode-map + "Menu for `image-dired-thumbnail-mode'." + '("Image-Dired" + ["Display image" image-dired-display-thumbnail-original-image] + ["Display in external viewer" image-dired-thumbnail-display-external] + ["Jump to Dired buffer" image-dired-jump-original-dired-buffer] + "---" + ["Mark image" image-dired-mark-thumb-original-file] + ["Unmark image" image-dired-unmark-thumb-original-file] + ["Unmark all images" image-dired-unmark-all-marks] + ["Flag for deletion" image-dired-flag-thumb-original-file] + ["Delete marked images" image-dired-delete-marked] + "---" + ["Rotate original right" image-dired-rotate-original-right] + ["Rotate original left" image-dired-rotate-original-left] + "---" + ["Comment thumbnail" image-dired-comment-thumbnail] + ["Tag current or marked thumbnails" image-dired-tag-thumbnail] + ["Remove tag from current or marked thumbnails" + image-dired-tag-thumbnail-remove] + ["Start slideshow" image-dired-slideshow-start] + "---" + ("View Options" + ["Toggle movement tracking" image-dired-toggle-movement-tracking + :style toggle + :selected image-dired-track-movement] + "---" + ["Line up thumbnails" image-dired-line-up] + ["Dynamic line up" image-dired-line-up-dynamic] + ["Refresh thumb" image-dired-refresh-thumb]) + ["Quit" quit-window])) + +(defvar image-dired-display-image-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "S" #'image-dired-slideshow-start) + (define-key map (kbd "SPC") #'image-dired-display-next-thumbnail-original) + (define-key map (kbd "DEL") #'image-dired-display-previous-thumbnail-original) + (define-key map "n" #'image-dired-display-next-thumbnail-original) + (define-key map "p" #'image-dired-display-previous-thumbnail-original) + (define-key map "m" #'image-dired-mark-thumb-original-file) + (define-key map "d" #'image-dired-flag-thumb-original-file) + (define-key map "u" #'image-dired-unmark-thumb-original-file) + (define-key map "U" #'image-dired-unmark-all-marks) + ;; Disable keybindings from `image-mode-map' that doesn't make sense here. + (define-key map "o" nil) ; image-save + map) + "Keymap for `image-dired-display-image-mode'.") + +(define-derived-mode image-dired-thumbnail-mode + special-mode "image-dired-thumbnail" + "Browse and manipulate thumbnail images using Dired. +Use `image-dired-minor-mode' to get a nice setup." + :interactive nil + (buffer-disable-undo) + (add-hook 'file-name-at-point-functions 'image-dired-file-name-at-point nil t) + (setq-local window-resize-pixelwise t) + (setq-local bookmark-make-record-function #'image-dired-bookmark-make-record) + ;; Use approximately as much vertical spacing as horizontal. + (setq-local line-spacing (frame-char-width))) + + +;;; Display image mode + +(define-derived-mode image-dired-display-image-mode + image-mode "image-dired-image-display" + "Mode for displaying and manipulating original image. +Resized or in full-size." + :interactive nil + (add-hook 'file-name-at-point-functions #'image-dired-file-name-at-point nil t)) + +(defvar image-dired-minor-mode-map + (let ((map (make-sparse-keymap))) + ;; (set-keymap-parent map dired-mode-map) + ;; Hijack previous and next line movement. Let C-p and C-b be + ;; though... + (define-key map "p" #'image-dired-dired-previous-line) + (define-key map "n" #'image-dired-dired-next-line) + (define-key map [up] #'image-dired-dired-previous-line) + (define-key map [down] #'image-dired-dired-next-line) + + (define-key map (kbd "C-S-n") #'image-dired-next-line-and-display) + (define-key map (kbd "C-S-p") #'image-dired-previous-line-and-display) + (define-key map (kbd "C-S-m") #'image-dired-mark-and-display-next) + + (define-key map "\C-td" #'image-dired-display-thumbs) + (define-key map [tab] #'image-dired-jump-thumbnail-buffer) + (define-key map "\C-ti" #'image-dired-dired-display-image) + (define-key map "\C-tx" #'image-dired-dired-display-external) + (define-key map "\C-ta" #'image-dired-display-thumbs-append) + (define-key map "\C-t." #'image-dired-display-thumb) + (define-key map "\C-tc" #'image-dired-dired-comment-files) + (define-key map "\C-tf" #'image-dired-mark-tagged-files) + map) + "Keymap for `image-dired-minor-mode'.") + +(easy-menu-define image-dired-minor-mode-menu image-dired-minor-mode-map + "Menu for `image-dired-minor-mode'." + '("Image-dired" + ["Display thumb for next file" image-dired-next-line-and-display] + ["Display thumb for previous file" image-dired-previous-line-and-display] + ["Mark and display next" image-dired-mark-and-display-next] + "---" + ["Create thumbnails for marked files" image-dired-create-thumbs] + "---" + ["Display thumbnails append" image-dired-display-thumbs-append] + ["Display this thumbnail" image-dired-display-thumb] + ["Display image" image-dired-dired-display-image] + ["Display in external viewer" image-dired-dired-display-external] + "---" + ["Toggle display properties" image-dired-toggle-dired-display-properties + :style toggle + :selected image-dired-dired-disp-props] + ["Toggle append browsing" image-dired-toggle-append-browsing + :style toggle + :selected image-dired-append-when-browsing] + ["Toggle movement tracking" image-dired-toggle-movement-tracking + :style toggle + :selected image-dired-track-movement] + "---" + ["Jump to thumbnail buffer" image-dired-jump-thumbnail-buffer] + ["Mark tagged files" image-dired-mark-tagged-files] + ["Comment files" image-dired-dired-comment-files] + ["Copy with EXIF file name" image-dired-copy-with-exif-file-name])) + +;;;###autoload +(define-minor-mode image-dired-minor-mode + "Setup easy-to-use keybindings for the commands to be used in Dired mode. +Note that n, p and and will be hijacked and bound to +`image-dired-dired-next-line' and `image-dired-dired-previous-line'." + :keymap image-dired-minor-mode-map) + +(declare-function clear-image-cache "image.c" (&optional filter)) + +(defun image-dired-create-thumbs (&optional arg) + "Create thumbnail images for all marked files in Dired. +With prefix argument ARG, create thumbnails even if they already exist +\(i.e. use this to refresh your thumbnails)." + (interactive "P") + (let (thumb-name) + (dolist (curr-file (dired-get-marked-files)) + (setq thumb-name (image-dired-thumb-name curr-file)) + ;; If the user overrides the exist check, we must clear the + ;; image cache so that if the user wants to display the + ;; thumbnail, it is not fetched from cache. + (when arg + (clear-image-cache (expand-file-name thumb-name))) + (when (or (not (file-exists-p thumb-name)) + arg) + (image-dired-create-thumb curr-file thumb-name))))) + + +;;; Slideshow + +(defcustom image-dired-slideshow-delay 5.0 + "Seconds to wait before showing the next image in a slideshow. +This is used by `image-dired-slideshow-start'." + :type 'float + :version "29.1") + +(define-obsolete-variable-alias 'image-dired-slideshow-timer + 'image-dired--slideshow-timer "29.1") +(defvar image-dired--slideshow-timer nil + "Slideshow timer.") + +(defvar image-dired--slideshow-initial nil) + +(defun image-dired-slideshow-step () + "Step to next image in a slideshow." + (if-let ((buf (get-buffer image-dired-thumbnail-buffer))) + (with-current-buffer buf + (image-dired-display-next-thumbnail-original)) + (image-dired-slideshow-stop))) + +(defun image-dired-slideshow-start (&optional arg) + "Start a slideshow, waiting `image-dired-slideshow-delay' between images. + +With prefix argument ARG, wait that many seconds before going to +the next image. + +With a negative prefix argument, prompt user for the delay." + (interactive "P" image-dired-thumbnail-mode image-dired-display-image-mode) + (let ((delay (if (not arg) + image-dired-slideshow-delay + (if (> arg 0) + arg + (string-to-number + (let ((delay (number-to-string image-dired-slideshow-delay))) + (read-string + (format-prompt "Delay, in seconds. Decimals are accepted" delay)) + delay)))))) + (setq image-dired--slideshow-timer + (run-with-timer + 0 delay + 'image-dired-slideshow-step)) + (add-hook 'post-command-hook 'image-dired-slideshow-stop) + (setq image-dired--slideshow-initial t) + (message "Running slideshow; use any command to stop"))) + +(defun image-dired-slideshow-stop () + "Cancel slideshow." + ;; Make sure we don't immediately stop after + ;; `image-dired-slideshow-start'. + (unless image-dired--slideshow-initial + (remove-hook 'post-command-hook 'image-dired-slideshow-stop) + (cancel-timer image-dired--slideshow-timer)) + (setq image-dired--slideshow-initial nil)) + + +;;; Thumbnail mode (cont. 3) + +(defun image-dired-delete-char () + "Remove current thumbnail from thumbnail buffer and line up." + (interactive nil image-dired-thumbnail-mode) + (let ((inhibit-read-only t)) + (delete-char 1) + (when (= (following-char) ?\s) + (delete-char 1)))) + +;;;###autoload +(defun image-dired-display-thumbs-append () + "Append thumbnails to `image-dired-thumbnail-buffer'." + (interactive) + (image-dired-display-thumbs nil t t)) + +;;;###autoload +(defun image-dired-display-thumb () + "Shorthand for `image-dired-display-thumbs' with prefix argument." + (interactive) + (image-dired-display-thumbs t nil t)) + +(defun image-dired-line-up () + "Line up thumbnails according to `image-dired-thumbs-per-row'. +See also `image-dired-line-up-dynamic'." + (interactive) + (let ((inhibit-read-only t)) + (goto-char (point-min)) + (while (and (not (image-dired-image-at-point-p)) + (not (eobp))) + (delete-char 1)) + (while (not (eobp)) + (forward-char) + (while (and (not (image-dired-image-at-point-p)) + (not (eobp))) + (delete-char 1))) + (goto-char (point-min)) + (let ((seen 0) + (thumb-prev-pos 0) + (thumb-width-chars + (ceiling (/ (+ (* 2 image-dired-thumb-relief) + (* 2 image-dired-thumb-margin) + (image-dired-thumb-size 'width)) + (float (frame-char-width)))))) + (while (not (eobp)) + (forward-char) + (if (= image-dired-thumbs-per-row 1) + (insert "\n") + (cl-incf thumb-prev-pos thumb-width-chars) + (insert (propertize " " 'display `(space :align-to ,thumb-prev-pos))) + (cl-incf seen) + (when (and (= seen (- image-dired-thumbs-per-row 1)) + (not (eobp))) + (forward-char) + (insert "\n") + (setq seen 0) + (setq thumb-prev-pos 0))))) + (goto-char (point-min)))) + +(defun image-dired-line-up-dynamic () + "Line up thumbnails images dynamically. +Calculate how many thumbnails fit." + (interactive) + (let* ((char-width (frame-char-width)) + (width (image-dired-window-width-pixels (image-dired-thumbnail-window))) + (image-dired-thumbs-per-row + (/ width + (+ (* 2 image-dired-thumb-relief) + (* 2 image-dired-thumb-margin) + (image-dired-thumb-size 'width) + char-width)))) + (image-dired-line-up))) + +(defun image-dired-line-up-interactive () + "Line up thumbnails interactively. +Ask user how many thumbnails should be displayed per row." + (interactive) + (let ((image-dired-thumbs-per-row + (string-to-number (read-string "How many thumbs per row: ")))) + (if (not (> image-dired-thumbs-per-row 0)) + (message "Number must be greater than 0") + (image-dired-line-up)))) + +(defun image-dired-thumbnail-display-external () + "Display original image for thumbnail at point using external viewer." + (interactive) + (let ((file (image-dired-original-file-name))) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (if (not file) + (message "No original file name found") + (start-process "image-dired-thumb-external" nil + image-dired-external-viewer file))))) + +;;;###autoload +(defun image-dired-dired-display-external () + "Display file at point using an external viewer." + (interactive) + (let ((file (dired-get-filename))) + (start-process "image-dired-external" nil + image-dired-external-viewer file))) + +(defun image-dired-window-width-pixels (window) + "Calculate WINDOW width in pixels." + (* (window-width window) (frame-char-width))) + +(defun image-dired-display-window () + "Return window where `image-dired-display-image-buffer' is visible." + (get-window-with-predicate + (lambda (window) + (equal (buffer-name (window-buffer window)) image-dired-display-image-buffer)) + nil t)) + +(defun image-dired-thumbnail-window () + "Return window where `image-dired-thumbnail-buffer' is visible." + (get-window-with-predicate + (lambda (window) + (equal (buffer-name (window-buffer window)) image-dired-thumbnail-buffer)) + nil t)) + +(defun image-dired-associated-dired-buffer-window () + "Return window where associated Dired buffer is visible." + (let (buf) + (if (image-dired-image-at-point-p) + (progn + (setq buf (image-dired-associated-dired-buffer)) + (get-window-with-predicate + (lambda (window) + (equal (window-buffer window) buf)))) + (error "No thumbnail image at point")))) + +(defun image-dired-display-image (file &optional _ignored) + "Display image FILE in image buffer. +Use this when you want to display the image, in a new window. +The window will use `image-dired-display-image-mode' which is +based on `image-mode'." + (declare (advertised-calling-convention (file) "29.1")) + (setq file (expand-file-name file)) + (when (not (file-exists-p file)) + (error "No such file: %s" file)) + (let ((buf (get-buffer image-dired-display-image-buffer)) + (cur-win (selected-window))) + (when buf + (kill-buffer buf)) + (when-let ((buf (find-file-noselect file nil t))) + (pop-to-buffer buf) + (rename-buffer image-dired-display-image-buffer) + (image-dired-display-image-mode) + (select-window cur-win)))) + +(defun image-dired-display-thumbnail-original-image (&optional arg) + "Display current thumbnail's original image in display buffer. +See documentation for `image-dired-display-image' for more information. +With prefix argument ARG, display image in its original size." + (interactive "P") + (let ((file (image-dired-original-file-name))) + (if (not (string-equal major-mode "image-dired-thumbnail-mode")) + (message "Not in image-dired-thumbnail-mode") + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (if (not file) + (message "No original file name found") + (image-dired-display-image file arg)))))) + + +;;;###autoload +(defun image-dired-dired-display-image (&optional arg) + "Display current image file. +See documentation for `image-dired-display-image' for more information. +With prefix argument ARG, display image in its original size." + (interactive "P") + (image-dired-display-image (dired-get-filename) arg)) + +(defun image-dired-image-at-point-p () + "Return non-nil if there is an `image-dired' thumbnail at point." + (get-text-property (point) 'image-dired-thumbnail)) + +(defun image-dired-refresh-thumb () + "Force creation of new image for current thumbnail." + (interactive nil image-dired-thumbnail-mode) + (let* ((file (image-dired-original-file-name)) + (thumb (expand-file-name (image-dired-thumb-name file)))) + (clear-image-cache (expand-file-name thumb)) + (image-dired-create-thumb file thumb))) + +(defun image-dired-rotate-original (degrees) + "Rotate original image DEGREES degrees." + (image-dired--check-executable-exists + 'image-dired-cmd-rotate-original-program) + (if (not (image-dired-image-at-point-p)) + (message "No image at point") + (let* ((file (image-dired-original-file-name)) + (spec + (list + (cons ?d degrees) + (cons ?o (expand-file-name file)) + (cons ?t image-dired-temp-rotate-image-file)))) + (unless (eq 'jpeg (image-type file)) + (user-error "Only JPEG images can be rotated")) + (if (not (= 0 (apply #'call-process image-dired-cmd-rotate-original-program + nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-rotate-original-options)))) + (error "Could not rotate image") + (image-dired-display-image image-dired-temp-rotate-image-file) + (if (or (and image-dired-rotate-original-ask-before-overwrite + (y-or-n-p + "Rotate to temp file OK. Overwrite original image? ")) + (not image-dired-rotate-original-ask-before-overwrite)) + (progn + (copy-file image-dired-temp-rotate-image-file file t) + (image-dired-refresh-thumb)) + (image-dired-display-image file)))))) + +(defun image-dired-rotate-original-left () + "Rotate original image left (counter clockwise) 90 degrees. +The result of the rotation is displayed in the image display area +and a confirmation is needed before the original image files is +overwritten. This confirmation can be turned off using +`image-dired-rotate-original-ask-before-overwrite'." + (interactive) + (image-dired-rotate-original "270")) + +(defun image-dired-rotate-original-right () + "Rotate original image right (clockwise) 90 degrees. +The result of the rotation is displayed in the image display area +and a confirmation is needed before the original image files is +overwritten. This confirmation can be turned off using +`image-dired-rotate-original-ask-before-overwrite'." + (interactive) + (image-dired-rotate-original "90")) + + +;;; EXIF support + +(defun image-dired-get-exif-file-name (file) + "Use the image's EXIF information to return a unique file name. +The file name should be unique as long as you do not take more than +one picture per second. The original file name is suffixed at the end +for traceability. The format of the returned file name is +YYYY_MM_DD_HH_MM_DD_ORIG_FILE_NAME.jpg. Used from +`image-dired-copy-with-exif-file-name'." + (let (data no-exif-data-found) + (if (not (eq 'jpeg (image-type (expand-file-name file)))) + (setq no-exif-data-found t + data (format-time-string + "%Y:%m:%d %H:%M:%S" + (file-attribute-modification-time + (file-attributes (expand-file-name file))))) + (setq data (exif-field 'date-time (exif-parse-file + (expand-file-name file))))) + (while (string-match "[ :]" data) + (setq data (replace-match "_" nil nil data))) + (format "%s%s%s" data + (if no-exif-data-found + "_noexif_" + "_") + (file-name-nondirectory file)))) + +(defun image-dired-thumbnail-set-image-description () + "Set the ImageDescription EXIF tag for the original image. +If the image already has a value for this tag, it is used as the +default value at the prompt." + (interactive) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (let* ((file (image-dired-original-file-name)) + (old-value (or (exif-field 'description (exif-parse-file file)) ""))) + (if (eq 0 + (image-dired-set-exif-data file "ImageDescription" + (read-string "Value of ImageDescription: " + old-value))) + (message "Successfully wrote ImageDescription tag") + (error "Could not write ImageDescription tag"))))) + +(defun image-dired-set-exif-data (file tag-name tag-value) + "In FILE, set EXIF tag TAG-NAME to value TAG-VALUE." + (image-dired--check-executable-exists + 'image-dired-cmd-write-exif-data-program) + (let ((spec + (list + (cons ?f (expand-file-name file)) + (cons ?t tag-name) + (cons ?v tag-value)))) + (apply #'call-process image-dired-cmd-write-exif-data-program nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-write-exif-data-options)))) + +(defun image-dired-copy-with-exif-file-name () + "Copy file with unique name to main image directory. +Copy current or all marked files in Dired to a new file in your +main image directory, using a file name generated by +`image-dired-get-exif-file-name'. A typical usage for this if when +copying images from a digital camera into the image directory. + + Typically, you would open up the folder with the incoming +digital images, mark the files to be copied, and execute this +function. The result is a couple of new files in +`image-dired-main-image-directory' called +2005_05_08_12_52_00_dscn0319.jpg, +2005_05_08_14_27_45_dscn0320.jpg etc." + (interactive) + (let (new-name + (files (dired-get-marked-files))) + (mapc + (lambda (curr-file) + (setq new-name + (format "%s/%s" + (file-name-as-directory + (expand-file-name image-dired-main-image-directory)) + (image-dired-get-exif-file-name curr-file))) + (message "Copying %s to %s" curr-file new-name) + (copy-file curr-file new-name)) + files))) + +;;; Thumbnail mode (cont.) + +(defun image-dired-display-next-thumbnail-original (&optional arg) + "Move to the next image in the thumbnail buffer and display it. +With prefix ARG, move that many thumbnails." + (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired--with-thumbnail-buffer + (image-dired-forward-image arg t) + (image-dired-display-thumbnail-original-image))) + +(defun image-dired-display-previous-thumbnail-original (arg) + "Move to the previous image in the thumbnail buffer and display it. +With prefix ARG, move that many thumbnails." + (interactive "p" image-dired-thumbnail-mode image-dired-display-image-mode) + (image-dired-display-next-thumbnail-original (- arg))) + + +;;; Image Comments + +(defun image-dired-write-comments (file-comments) + "Write file comments to database. +Write file comments to one or more files. +FILE-COMMENTS is an alist on the following form: + ((FILE . COMMENT) ... )" + (image-dired-sane-db-file) + (let (end comment-beg-pos comment-end-pos file comment) + (image-dired--with-db-file + (setq buffer-file-name image-dired-db-file) + (dolist (elt file-comments) + (setq file (car elt) + comment (cdr elt)) + (goto-char (point-min)) + (if (search-forward-regexp (format "^%s.*$" file) nil t) + (progn + (setq end (point)) + (beginning-of-line) + ;; Delete old comment, if any + (when (search-forward ";comment:" end t) + (setq comment-beg-pos (match-beginning 0)) + ;; Any tags after the comment? + (if (search-forward ";" end t) + (setq comment-end-pos (- (point) 1)) + (setq comment-end-pos end)) + ;; Delete comment tag and comment + (delete-region comment-beg-pos comment-end-pos)) + ;; Insert new comment + (beginning-of-line) + (unless (search-forward ";" end t) + (end-of-line) + (insert ";")) + (insert (format "comment:%s;" comment))) + ;; File does not exist in database - add it. + (goto-char (point-max)) + (insert (format "%s;comment:%s\n" file comment)))) + (save-buffer)))) + +(defun image-dired-update-property (prop value) + "Update text property PROP with value VALUE at point." + (let ((inhibit-read-only t)) + (put-text-property + (point) (1+ (point)) + prop + value))) + +;;;###autoload +(defun image-dired-dired-comment-files () + "Add comment to current or marked files in Dired." + (interactive) + (let ((comment (image-dired-read-comment))) + (image-dired-write-comments + (mapcar + (lambda (curr-file) + (cons curr-file comment)) + (dired-get-marked-files))))) + +(defun image-dired-comment-thumbnail () + "Add comment to current thumbnail in thumbnail buffer." + (interactive) + (let* ((file (image-dired-original-file-name)) + (comment (image-dired-read-comment file))) + (image-dired-write-comments (list (cons file comment))) + (image-dired-update-property 'comment comment)) + (image-dired-update-header-line)) + +(defun image-dired-read-comment (&optional file) + "Read comment for an image. +Optionally use old comment from FILE as initial value." + (let ((comment + (read-string + "Comment: " + (if file (image-dired-get-comment file))))) + comment)) + +(defun image-dired-get-comment (file) + "Get comment for file FILE." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end comment-beg-pos comment-end-pos comment) + (when (search-forward-regexp (format "^%s" file) nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (when (search-forward ";comment:" end t) + (setq comment-beg-pos (point)) + (if (search-forward ";" end t) + (setq comment-end-pos (- (point) 1)) + (setq comment-end-pos end)) + (setq comment (buffer-substring + comment-beg-pos comment-end-pos)))) + comment))) + +;;;###autoload +(defun image-dired-mark-tagged-files (regexp) + "Use REGEXP to mark files with matching tag. +A `tag' is a keyword, a piece of meta data, associated with an +image file and stored in image-dired's database file. This command +lets you input a regexp and this will be matched against all tags +on all image files in the database file. The files that have a +matching tag will be marked in the Dired buffer." + (interactive "sMark tagged files (regexp): ") + (image-dired-sane-db-file) + (let ((hits 0) + files) + (image-dired--with-db-file + ;; Collect matches + (while (search-forward-regexp "\\(^[^;\n]+\\);\\(.*\\)" nil t) + (let ((file (match-string 1)) + (tags (split-string (match-string 2) ";"))) + (when (seq-find (lambda (tag) + (string-match-p regexp tag)) + tags) + (push file files))))) + ;; Mark files + (dolist (curr-file files) + ;; I tried using `dired-mark-files-regexp' but it was waaaay to + ;; slow. Don't bother about hits found in other directories + ;; than the current one. + (when (string= (file-name-as-directory + (expand-file-name default-directory)) + (file-name-as-directory + (file-name-directory curr-file))) + (setq curr-file (file-name-nondirectory curr-file)) + (goto-char (point-min)) + (when (search-forward-regexp (format "\\s %s$" curr-file) nil t) + (setq hits (+ hits 1)) + (dired-mark 1)))) + (message "%d files with matching tag marked" hits))) + + + +;;; Mouse support + +(defun image-dired-mouse-display-image (event) + "Use mouse EVENT, call `image-dired-display-image' to display image. +Track this in associated Dired buffer if `image-dired-track-movement' is +non-nil." + (interactive "e") + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (unless (image-at-point-p) + (image-dired-backward-image)) + (let ((file (image-dired-original-file-name))) + (when file + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-display-image file)))) + +(defun image-dired-mouse-select-thumbnail (event) + "Use mouse EVENT to select thumbnail image. +Track this in associated Dired buffer if `image-dired-track-movement' is +non-nil." + (interactive "e") + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (unless (image-at-point-p) + (image-dired-backward-image)) + (if image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-update-header-line)) + + + +;;; Dired marks and tags + +(defun image-dired-thumb-file-marked-p (&optional flagged) + "Check if file is marked in associated Dired buffer. +If optional argument FLAGGED is non-nil, check if file is flagged +for deletion instead." + (let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (when (and dired-buf file-name) + (with-current-buffer dired-buf + (save-excursion + (when (dired-goto-file file-name) + (if flagged + (image-dired-dired-file-flagged-p) + (image-dired-dired-file-marked-p)))))))) + +(defun image-dired-thumb-file-flagged-p () + "Check if file is flagged for deletion in associated Dired buffer." + (image-dired-thumb-file-marked-p t)) + +(defun image-dired-delete-marked () + "Delete current or marked thumbnails and associated images." + (interactive) + (image-dired--with-marked + (image-dired-delete-char) + (unless (bobp) + (backward-char))) + (image-dired--line-up-with-method) + (with-current-buffer (image-dired-associated-dired-buffer) + (dired-do-delete))) + +(defun image-dired-thumb-update-marks () + "Update the marks in the thumbnail buffer." + (when image-dired-thumb-visible-marks + (with-current-buffer image-dired-thumbnail-buffer + (save-mark-and-excursion + (goto-char (point-min)) + (let ((inhibit-read-only t)) + (while (not (eobp)) + (with-silent-modifications + (cond ((image-dired-thumb-file-marked-p) + (add-face-text-property (point) (1+ (point)) + 'image-dired-thumb-mark)) + ((image-dired-thumb-file-flagged-p) + (add-face-text-property (point) (1+ (point)) + 'image-dired-thumb-flagged)) + (t (remove-text-properties (point) (1+ (point)) + '(face image-dired-thumb-mark))))) + (forward-char))))))) + +(defun image-dired-mouse-toggle-mark-1 () + "Toggle Dired mark for current thumbnail. +Track this in associated Dired buffer if +`image-dired-track-movement' is non-nil." + (when image-dired-track-movement + (image-dired-track-original-file)) + (image-dired-toggle-mark-thumb-original-file)) + +(defun image-dired-mouse-toggle-mark (event) + "Use mouse EVENT to toggle Dired mark for thumbnail. +Toggle marks of all thumbnails in region, if it's active. +Track this in associated Dired buffer if +`image-dired-track-movement' is non-nil." + (interactive "e") + (if (use-region-p) + (let ((end (region-end))) + (save-excursion + (goto-char (region-beginning)) + (while (<= (point) end) + (when (image-dired-image-at-point-p) + (image-dired-mouse-toggle-mark-1)) + (forward-char)))) + (mouse-set-point event) + (goto-char (posn-point (event-end event))) + (image-dired-mouse-toggle-mark-1)) + (image-dired-thumb-update-marks)) + +(defun image-dired-dired-display-properties () + "Display properties for Dired file in the echo area." + (interactive) + (let* ((file (dired-get-filename)) + (file-name (file-name-nondirectory file)) + (dired-buf (buffer-name (current-buffer))) + (props (mapconcat #'identity (image-dired-list-tags file) ", ")) + (comment (image-dired-get-comment file)) + (message-log-max nil)) + (if file-name + (message "%s" + (image-dired-format-properties-string + dired-buf + file-name + props + comment))))) + + + +;;; Gallery support + +;; TODO: +;; * Support gallery creation when using per-directory thumbnail +;; storage. +;; * Enhanced gallery creation with basic CSS-support and pagination +;; of tag pages with many pictures. + +(defgroup image-dired-gallery nil + "Image-Dired support for generating a HTML gallery." + :prefix "image-dired-" + :group 'image-dired + :version "29.1") + +(defcustom image-dired-gallery-dir + (expand-file-name ".image-dired_gallery" image-dired-dir) + "Directory to store generated gallery html pages. +The name of this directory needs to be \"shared\" to the public +so that it can access the index.html page that image-dired creates." + :type 'directory) + +(defcustom image-dired-gallery-image-root-url + "https://example.org/image-diredpics" + "URL where the full size images are to be found on your web server. +Note that this URL has to be configured on your web server. +Image-Dired expects to find pictures in this directory. +This is used by `image-dired-gallery-generate'." + :type 'string + :version "29.1") + +(defcustom image-dired-gallery-thumb-image-root-url + "https://example.org/image-diredthumbs" + "URL where the thumbnail images are to be found on your web server. +Note that URL path has to be configured on your web server. +Image-Dired expects to find pictures in this directory. +This is used by `image-dired-gallery-generate'." + :type 'string + :version "29.1") + +(defcustom image-dired-gallery-hidden-tags + (list "private" "hidden" "pending") + "List of \"hidden\" tags. +Used by `image-dired-gallery-generate' to leave out \"hidden\" images." + :type '(repeat string)) + +(defvar image-dired-tag-file-list nil + "List to store tag-file structure.") + +(defvar image-dired-file-tag-list nil + "List to store file-tag structure.") + +(defvar image-dired-file-comment-list nil + "List to store file comments.") + +(defun image-dired--add-to-tag-file-lists (tag file) + "Helper function used from `image-dired--create-gallery-lists'. + +Add TAG to FILE in one list and FILE to TAG in the other. + +Lisp structures look like the following: + +image-dired-file-tag-list: + + ((\"filename1\" \"tag1\" \"tag2\" \"tag3\" ...) + (\"filename2\" \"tag1\" \"tag2\" \"tag3\" ...) + ...) + +image-dired-tag-file-list: + + ((\"tag1\" \"filename1\" \"filename2\" \"filename3\" ...) + (\"tag2\" \"filename1\" \"filename2\" \"filename3\" ...) + ...)" + ;; Add tag to file list + (let (curr) + (if image-dired-file-tag-list + (if (setq curr (assoc file image-dired-file-tag-list)) + (setcdr curr (cons tag (cdr curr))) + (setcdr image-dired-file-tag-list + (cons (list file tag) (cdr image-dired-file-tag-list)))) + (setq image-dired-file-tag-list (list (list file tag)))) + ;; Add file to tag list + (if image-dired-tag-file-list + (if (setq curr (assoc tag image-dired-tag-file-list)) + (if (not (member file curr)) + (setcdr curr (cons file (cdr curr)))) + (setcdr image-dired-tag-file-list + (cons (list tag file) (cdr image-dired-tag-file-list)))) + (setq image-dired-tag-file-list (list (list tag file)))))) + +(defun image-dired--add-to-file-comment-list (file comment) + "Helper function used from `image-dired--create-gallery-lists'. + +For FILE, add COMMENT to list. + +Lisp structure looks like the following: + +image-dired-file-comment-list: + + ((\"filename1\" . \"comment1\") + (\"filename2\" . \"comment2\") + ...)" + (if image-dired-file-comment-list + (if (not (assoc file image-dired-file-comment-list)) + (setcdr image-dired-file-comment-list + (cons (cons file comment) + (cdr image-dired-file-comment-list)))) + (setq image-dired-file-comment-list (list (cons file comment))))) + +(defun image-dired--create-gallery-lists () + "Create temporary lists used by `image-dired-gallery-generate'." + (image-dired-sane-db-file) + (image-dired--with-db-file + (let (end beg file row-tags) + (setq image-dired-tag-file-list nil) + (setq image-dired-file-tag-list nil) + (setq image-dired-file-comment-list nil) + (goto-char (point-min)) + (while (search-forward-regexp "^." nil t) + (end-of-line) + (setq end (point)) + (beginning-of-line) + (setq beg (point)) + (unless (search-forward ";" end nil) + (error "Something is really wrong, check format of database")) + (setq row-tags (split-string + (buffer-substring beg end) ";")) + (setq file (car row-tags)) + (dolist (x (cdr row-tags)) + (if (not (string-match "^comment:\\(.*\\)" x)) + (image-dired--add-to-tag-file-lists x file) + (image-dired--add-to-file-comment-list file (match-string 1 x))))))) + ;; Sort tag-file list + (setq image-dired-tag-file-list + (sort image-dired-tag-file-list + (lambda (x y) + (string< (car x) (car y)))))) + +(defun image-dired--hidden-p (file) + "Return t if image FILE has a \"hidden\" tag." + (cl-loop for tag in (cdr (assoc file image-dired-file-tag-list)) + if (member tag image-dired-gallery-hidden-tags) return t)) + +(defun image-dired-gallery-generate () + "Generate gallery pages. +First we create a couple of Lisp structures from the database to make +it easier to generate, then HTML-files are created in +`image-dired-gallery-dir'." + (interactive) + (if (eq 'per-directory image-dired-thumbnail-storage) + (error "Currently, gallery generation is not supported \ +when using per-directory thumbnail file storage")) + (image-dired--create-gallery-lists) + (let ((tags image-dired-tag-file-list) + (index-file (format "%s/index.html" image-dired-gallery-dir)) + count tag tag-file + comment file-tags tag-link tag-link-list) + ;; Make sure gallery root exist + (if (file-exists-p image-dired-gallery-dir) + (if (not (file-directory-p image-dired-gallery-dir)) + (error "Variable image-dired-gallery-dir is not a directory")) + ;; FIXME: Should we set umask to 077 here, as we do for thumbnails? + (make-directory image-dired-gallery-dir)) + ;; Open index file + (with-temp-file index-file + (if (file-exists-p index-file) + (insert-file-contents index-file)) + (insert "\n") + (insert " \n") + (insert "

Image-Dired Gallery

\n") + (insert (format "

\n Gallery generated %s\n

\n" + (current-time-string))) + (insert "

Tag index

\n") + (setq count 1) + ;; Pre-generate list of all tag links + (dolist (curr tags) + (setq tag (car curr)) + (when (not (member tag image-dired-gallery-hidden-tags)) + (setq tag-link (format "%s" count tag)) + (if tag-link-list + (setq tag-link-list + (append tag-link-list (list (cons tag tag-link)))) + (setq tag-link-list (list (cons tag tag-link)))) + (setq count (1+ count)))) + (setq count 1) + ;; Main loop where we generated thumbnail pages per tag + (dolist (curr tags) + (setq tag (car curr)) + ;; Don't display hidden tags + (when (not (member tag image-dired-gallery-hidden-tags)) + ;; Insert link to tag page in index + (insert (format " %s
\n" (cdr (assoc tag tag-link-list)))) + ;; Open per-tag file + (setq tag-file (format "%s/%s.html" image-dired-gallery-dir count)) + (with-temp-file tag-file + (if (file-exists-p tag-file) + (insert-file-contents tag-file)) + (erase-buffer) + (insert "\n") + (insert " \n") + (insert "

Index

\n") + (insert (format "

Images with tag "%s"

" tag)) + ;; Main loop for files per tag page + (dolist (file (cdr curr)) + (unless (image-dired-hidden-p file) + ;; Insert thumbnail with link to full image + (insert + (format "\n" + image-dired-gallery-image-root-url + (file-name-nondirectory file) + image-dired-gallery-thumb-image-root-url + (file-name-nondirectory (image-dired-thumb-name file)) file)) + ;; Insert comment, if any + (if (setq comment (cdr (assoc file image-dired-file-comment-list))) + (insert (format "
\n%s
\n" comment)) + (insert "
\n")) + ;; Insert links to other tags, if any + (when (> (length + (setq file-tags (assoc file image-dired-file-tag-list))) 2) + (insert "[ ") + (dolist (extra-tag file-tags) + ;; Only insert if not file name or the main tag + (if (and (not (equal extra-tag tag)) + (not (equal extra-tag file))) + (insert + (format "%s " (cdr (assoc extra-tag tag-link-list)))))) + (insert "]
\n")))) + (insert "

Index

\n") + (insert " \n") + (insert "\n")) + (setq count (1+ count)))) + (insert " \n") + (insert "")))) + + +;;; Tag support + +(defvar image-dired-widget-list nil + "List to keep track of meta data in edit buffer.") + +(declare-function widget-forward "wid-edit" (arg)) + +;;;###autoload +(defun image-dired-dired-edit-comment-and-tags () + "Edit comment and tags of current or marked image files. +Edit comment and tags for all marked image files in an +easy-to-use form." + (interactive) + (setq image-dired-widget-list nil) + ;; Setup buffer. + (let ((files (dired-get-marked-files))) + (pop-to-buffer-same-window "*Image-Dired Edit Meta Data*") + (kill-all-local-variables) + (let ((inhibit-read-only t)) + (erase-buffer)) + (remove-overlays) + ;; Some help for the user. + (widget-insert +"\nEdit comments and tags for each image. Separate multiple tags +with a comma. Move forward between fields using TAB or RET. +Move to the previous field using backtab (S-TAB). Save by +activating the Save button at the bottom of the form or cancel +the operation by activating the Cancel button.\n\n") + ;; Here comes all images and a comment and tag field for each + ;; image. + (let (thumb-file img comment-widget tag-widget) + + (dolist (file files) + + (setq thumb-file (image-dired-thumb-name file) + img (create-image thumb-file)) + + (insert-image img) + (widget-insert "\n\nComment: ") + (setq comment-widget + (widget-create 'editable-field + :size 60 + :format "%v " + :value (or (image-dired-get-comment file) ""))) + (widget-insert "\nTags: ") + (setq tag-widget + (widget-create 'editable-field + :size 60 + :format "%v " + :value (or (mapconcat + #'identity + (image-dired-list-tags file) + ",") ""))) + ;; Save information in all widgets so that we can use it when + ;; the user saves the form. + (setq image-dired-widget-list + (append image-dired-widget-list + (list (list file comment-widget tag-widget)))) + (widget-insert "\n\n"))) + + ;; Footer with Save and Cancel button. + (widget-insert "\n") + (widget-create 'push-button + :notify + (lambda (&rest _ignore) + (image-dired-save-information-from-widgets) + (bury-buffer) + (message "Done")) + "Save") + (widget-insert " ") + (widget-create 'push-button + :notify + (lambda (&rest _ignore) + (bury-buffer) + (message "Operation canceled")) + "Cancel") + (widget-insert "\n") + (use-local-map widget-keymap) + (widget-setup) + ;; Jump to the first widget. + (widget-forward 1))) + +(defun image-dired-save-information-from-widgets () + "Save information found in `image-dired-widget-list'. +Use the information in `image-dired-widget-list' to save comments and +tags to their respective image file. Internal function used by +`image-dired-dired-edit-comment-and-tags'." + (let (file comment tag-string tag-list lst) + (image-dired-write-comments + (mapcar + (lambda (widget) + (setq file (car widget) + comment (widget-value (cadr widget))) + (cons file comment)) + image-dired-widget-list)) + (image-dired-write-tags + (dolist (widget image-dired-widget-list lst) + (setq file (car widget) + tag-string (widget-value (car (cddr widget))) + tag-list (split-string tag-string ",")) + (dolist (tag tag-list) + (push (cons file tag) lst)))))) + + +;;; bookmark.el support + +(declare-function bookmark-make-record-default + "bookmark" (&optional no-file no-context posn)) +(declare-function bookmark-prop-get "bookmark" (bookmark prop)) + +(defun image-dired-bookmark-name () + "Create a default bookmark name for the current EWW buffer." + (file-name-nondirectory + (directory-file-name + (file-name-directory (image-dired-original-file-name))))) + +(defun image-dired-bookmark-make-record () + "Create a bookmark for the current EWW buffer." + `(,(image-dired-bookmark-name) + ,@(bookmark-make-record-default t) + (location . ,(file-name-directory (image-dired-original-file-name))) + (image-dired-file . ,(file-name-nondirectory (image-dired-original-file-name))) + (handler . image-dired-bookmark-jump))) + +;;;###autoload +(defun image-dired-bookmark-jump (bookmark) + "Default bookmark handler for Image-Dired buffers." + ;; User already cached thumbnails, so disable any checking. + (let ((image-dired-show-all-from-dir-max-files nil)) + (image-dired (bookmark-prop-get bookmark 'location)) + ;; TODO: Go to the bookmarked file, if it exists. + ;; (bookmark-prop-get bookmark 'image-dired-file) + (goto-char (point-min)))) + +(put 'image-dired-bookmark-jump 'bookmark-handler-type "Image-Dired") + +;;; Obsolete + +;;;###autoload +(define-obsolete-function-alias 'tumme #'image-dired "24.4") + +;;;###autoload +(define-obsolete-function-alias 'image-dired-setup-dired-keybindings + #'image-dired-minor-mode "26.1") + +(defcustom image-dired-temp-image-file + (expand-file-name ".image-dired_temp" image-dired-dir) + "Name of temporary image file used by various commands." + :type 'file) +(make-obsolete-variable 'image-dired-temp-image-file + "no longer used." "29.1") + +(defcustom image-dired-cmd-create-temp-image-program + (if (executable-find "gm") "gm" "convert") + "Executable used to create temporary image. +Used together with `image-dired-cmd-create-temp-image-options'." + :type 'file + :version "29.1") +(make-obsolete-variable 'image-dired-cmd-create-temp-image-program + "no longer used." "29.1") + +(defcustom image-dired-cmd-create-temp-image-options + (let ((opts '("-size" "%wx%h" "%f[0]" + "-resize" "%wx%h>" + "-strip" "jpeg:%t"))) + (if (executable-find "gm") (cons "convert" opts) opts)) + "Options of command used to create temporary image for display window. +Used together with `image-dired-cmd-create-temp-image-program', +Available format specifiers are: %w and %h which are replaced by +the calculated max size for width and height in the image display window, +%f which is replaced by the file name of the original image and %t which +is replaced by the file name of the temporary file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-create-temp-image-options + "no longer used." "29.1") + +(defcustom image-dired-display-window-width-correction 1 + "Number to be used to correct image display window width. +Change if the default (1) does not work (i.e. if the image does not +completely fit)." + :type 'integer) +(make-obsolete-variable 'image-dired-display-window-width-correction + "no longer used." "29.1") + +(defcustom image-dired-display-window-height-correction 0 + "Number to be used to correct image display window height. +Change if the default (0) does not work (i.e. if the image does not +completely fit)." + :type 'integer) +(make-obsolete-variable 'image-dired-display-window-height-correction + "no longer used." "29.1") + +(defun image-dired-display-window-width (window) + "Return width, in pixels, of WINDOW." + (declare (obsolete nil "29.1")) + (- (image-dired-window-width-pixels window) + image-dired-display-window-width-correction)) + +(defun image-dired-display-window-height (window) + "Return height, in pixels, of WINDOW." + (declare (obsolete nil "29.1")) + (- (image-dired-window-height-pixels window) + image-dired-display-window-height-correction)) + +(defun image-dired-window-height-pixels (window) + "Calculate WINDOW height in pixels." + (declare (obsolete nil "29.1")) + ;; Note: The mode-line consumes one line + (* (- (window-height window) 1) (frame-char-height))) + +(defcustom image-dired-cmd-read-exif-data-program "exiftool" + "Program used to read EXIF data to image. +Used together with `image-dired-cmd-read-exif-data-options'." + :type 'file) +(make-obsolete-variable 'image-dired-cmd-read-exif-data-program + "use `exif-parse-file' and `exif-field' instead." "29.1") + +(defcustom image-dired-cmd-read-exif-data-options '("-s" "-s" "-s" "-%t" "%f") + "Arguments of command used to read EXIF data. +Used with `image-dired-cmd-read-exif-data-program'. +Available format specifiers are: %f which is replaced +by the image file name and %t which is replaced by the tag name." + :version "26.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-read-exif-data-options + "use `exif-parse-file' and `exif-field' instead." "29.1") + +(defun image-dired-get-exif-data (file tag-name) + "From FILE, return EXIF tag TAG-NAME." + (declare (obsolete "use `exif-parse-file' and `exif-field' instead." "29.1")) + (image-dired--check-executable-exists + 'image-dired-cmd-read-exif-data-program) + (let ((buf (get-buffer-create "*image-dired-get-exif-data*")) + (spec (list (cons ?f file) (cons ?t tag-name))) + tag-value) + (with-current-buffer buf + (delete-region (point-min) (point-max)) + (if (not (eq (apply #'call-process image-dired-cmd-read-exif-data-program + nil t nil + (mapcar + (lambda (arg) (format-spec arg spec)) + image-dired-cmd-read-exif-data-options)) + 0)) + (error "Could not get EXIF tag") + (goto-char (point-min)) + ;; Clean buffer from newlines and carriage returns before + ;; getting final info + (while (search-forward-regexp "[\n\r]" nil t) + (replace-match "" nil t)) + (setq tag-value (buffer-substring (point-min) (point-max))))) + tag-value)) + +(defcustom image-dired-cmd-rotate-thumbnail-program + (if (executable-find "gm") "gm" "mogrify") + "Executable used to rotate thumbnail. +Used together with `image-dired-cmd-rotate-thumbnail-options'." + :type 'file + :version "29.1") +(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-program nil "29.1") + +(defcustom image-dired-cmd-rotate-thumbnail-options + (let ((opts '("-rotate" "%d" "%t"))) + (if (executable-find "gm") (cons "mogrify" opts) opts)) + "Arguments of command used to rotate thumbnail image. +Used with `image-dired-cmd-rotate-thumbnail-program'. +Available format specifiers are: %d which is replaced by the +number of (positive) degrees to rotate the image, normally 90 or 270 +\(for 90 degrees right and left), %t which is replaced by the file name +of the thumbnail file." + :version "29.1" + :type '(repeat (string :tag "Argument"))) +(make-obsolete-variable 'image-dired-cmd-rotate-thumbnail-options nil "29.1") + +(defun image-dired-rotate-thumbnail (degrees) + "Rotate thumbnail DEGREES degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (image-dired--check-executable-exists + 'image-dired-cmd-rotate-thumbnail-program) + (if (not (image-dired-image-at-point-p)) + (message "No thumbnail at point") + (let* ((file (image-dired-thumb-name (image-dired-original-file-name))) + (thumb (expand-file-name file)) + (spec (list (cons ?d degrees) (cons ?t thumb)))) + (apply #'call-process image-dired-cmd-rotate-thumbnail-program nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-rotate-thumbnail-options)) + (clear-image-cache thumb)))) + +(defun image-dired-rotate-thumbnail-left () + "Rotate thumbnail left (counter clockwise) 90 degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (interactive) + (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) + (image-dired-rotate-thumbnail "270"))) + +(defun image-dired-rotate-thumbnail-right () + "Rotate thumbnail counter right (clockwise) 90 degrees." + (declare (obsolete image-dired-refresh-thumb "29.1")) + (interactive) + (with-suppressed-warnings ((obsolete image-dired-rotate-thumbnail)) + (image-dired-rotate-thumbnail "90"))) + +(defun image-dired-modify-mark-on-thumb-original-file (command) + "Modify mark in Dired buffer. +COMMAND is one of `mark' for marking file in Dired, `unmark' for +unmarking file in Dired or `flag' for flagging file for delete in +Dired." + (declare (obsolete image-dired--on-file-in-dired-buffer "29.1")) + (let ((file-name (image-dired-original-file-name)) + (dired-buf (image-dired-associated-dired-buffer))) + (if (not (and dired-buf file-name)) + (message "No image, or image with correct properties, at point") + (with-current-buffer dired-buf + (message "%s" file-name) + (when (dired-goto-file file-name) + (cond ((eq command 'mark) (dired-mark 1)) + ((eq command 'unmark) (dired-unmark 1)) + ((eq command 'toggle) + (if (image-dired-dired-file-marked-p) + (dired-unmark 1) + (dired-mark 1))) + ((eq command 'flag) (dired-flag-file-deletion 1))) + (image-dired-thumb-update-marks)))))) + +(defun image-dired-display-current-image-full () + "Display current image in full size." + (declare (obsolete image-transform-original "29.1")) + (interactive nil image-dired-thumbnail-mode) + (let ((file (image-dired-original-file-name))) + (if file + (progn + (image-dired-display-image file) + (with-current-buffer image-dired-display-image-buffer + (image-transform-original))) + (error "No original file name at point")))) + +(defun image-dired-display-current-image-sized () + "Display current image in sized to fit window dimensions." + (declare (obsolete image-mode-fit-frame "29.1")) + (interactive nil image-dired-thumbnail-mode) + (let ((file (image-dired-original-file-name))) + (if file + (progn + (image-dired-display-image file)) + (error "No original file name at point")))) + +(defun image-dired-add-to-tag-file-list (tag file) + "Add relation between TAG and FILE." + (declare (obsolete nil "29.1")) + (let (curr) + (if image-dired-tag-file-list + (if (setq curr (assoc tag image-dired-tag-file-list)) + (if (not (member file curr)) + (setcdr curr (cons file (cdr curr)))) + (setcdr image-dired-tag-file-list + (cons (list tag file) (cdr image-dired-tag-file-list)))) + (setq image-dired-tag-file-list (list (list tag file)))))) + +(defun image-dired-display-thumb-properties () + "Display thumbnail properties in the echo area." + (declare (obsolete image-dired-update-header-line "29.1")) + (image-dired-update-header-line)) + +(defvar image-dired-slideshow-count 0 + "Keeping track on number of images in slideshow.") +(make-obsolete-variable 'image-dired-slideshow-count "no longer used." "29.1") + +(defvar image-dired-slideshow-times 0 + "Number of pictures to display in slideshow.") +(make-obsolete-variable 'image-dired-slideshow-times "no longer used." "29.1") + +(define-obsolete-function-alias 'image-dired-create-display-image-buffer + #'ignore "29.1") +(define-obsolete-function-alias 'image-dired-create-gallery-lists + #'image-dired--create-gallery-lists "29.1") +(define-obsolete-function-alias 'image-dired-add-to-file-comment-list + #'image-dired--add-to-file-comment-list "29.1") +(define-obsolete-function-alias 'image-dired-add-to-tag-file-lists + #'image-dired--add-to-tag-file-lists "29.1") +(define-obsolete-function-alias 'image-dired-hidden-p + #'image-dired--hidden-p "29.1") + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;; TEST-SECTION ;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; (defvar image-dired-dir-max-size 12300000) + +;; (defun image-dired-test-clean-old-files () +;; "Clean `image-dired-dir' from old thumbnail files. +;; \"Oldness\" measured using last access time. If the total size of all +;; thumbnail files in `image-dired-dir' is larger than 'image-dired-dir-max-size', +;; old files are deleted until the max size is reached." +;; (let* ((files +;; (sort +;; (mapcar +;; (lambda (f) +;; (let ((fattribs (file-attributes f))) +;; `(,(file-attribute-access-time fattribs) +;; ,(file-attribute-size fattribs) ,f))) +;; (directory-files (image-dired-dir) t ".+\\.thumb\\..+$")) +;; ;; Sort function. Compare time between two files. +;; (lambda (l1 l2) +;; (time-less-p (car l1) (car l2))))) +;; (dirsize (apply '+ (mapcar (lambda (x) (cadr x)) files)))) +;; (while (> dirsize image-dired-dir-max-size) +;; (y-or-n-p +;; (format "Size of thumbnail directory: %d, delete old file %s? " +;; dirsize (cadr (cdar files)))) +;; (delete-file (cadr (cdar files))) +;; (setq dirsize (- dirsize (car (cdar files)))) +;; (setq files (cdr files))))) + +(provide 'image-dired) + +;;; image-dired.el ends here diff --git a/test/lisp/image-dired-tests.el b/test/lisp/image/image-dired-tests.el similarity index 100% rename from test/lisp/image-dired-tests.el rename to test/lisp/image/image-dired-tests.el