blog

Extending image-dired

Emacs' image-dired is a companion package to dired that makes it easier to browse images and to do some very basic image editing. It is a perfect example of how Emacs can be extended to accomplish tasks that go beyond text manipulation.

I will not go through the details of how to use it, but I will illustrate a couple of things I recently added to it in my configuration.

I typically use image-dired to manipulate pictures that accompany blog posts and other stuff I publish on my personal website. These pictures are often exported from Google Photos, and when that is the case their size is usually not suitable for web.

So, I want to be able to know their resolution and, when it is too high, to quickly resize them.

More details in the header line

Let's tackle the first problem first. The image-dired header line only displays the name and the size of the the image file.

Unfortunately, image-dired does not provide a direct way to customize the header line format, so I have to recur to advicing.

The general idea of function advicing is to change the behavior of an existing function without altering its original code. More specifically, it allows to provide a new function that is called :before or :after each call to the original function, or even to replace the original function into a new one that can intercept the function call and completely control what is returned (this function composition method is called :around). Refer to the manual for further options and instructions.

In general, I tend to avoid functions advicing because it creates an invisible layer of indirection between the function definition and its behaviour; in other words, it makes it impossible to know the real result of a function call by just looking at the code of its implementation.

Anyway, here my code:

(defun my/image-size (file)
  (with-temp-buffer
    (call-process "identify" nil t nil "-format" "%wx%h" file )
    (buffer-string)))

(defun my/image-dired-enrich-properties (orig-fun buf file image-count props comment)
  (let ((orig-str (apply orig-fun (list buf file image-count props comment)))
        (size (my/image-size file)))
    (concat orig-str " " size)))

(advice-add 'image-dired-format-properties-string :around #'my/image-dired-enrich-properties)

The original function is image-dired-format-properties-string. I encapsulated a call to the external tool identify (part of ImageMagick) into my/image-size, which I then call to add that particular bit of information to the header line string. In what is a typical :around composition pattern, my advice function calls the original function, then adds extra information to the original input.

Here an example of the results:

image-dired-enrich-properties.png

Interactively resizing images

The mechanism here is even simpler. We just need to provide a function that can resize an image (again, using an utility courtesy of ImageMagick), and wrap it in a interactive function we can call when we are in the image-dired buffer.

(defun my/resize-image (file size)
  "Resize the image in FILE to the specified SIZE (interpreted as a percentage). "
  (call-process "magick" nil t nil file "-resize" (format "%d%%" size) file))

(defun image-dired-thumbnail-resize-image ()
  "Resize the image at point. The size is specified at the prompt as a percentage of the original size."
  (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))
           (defdir default-directory))
      (with-temp-buffer
        (setq default-directory defdir)
        (if (eq 0 (my/resize-image file (read-number "New size (%): " 50)))
            (message "Successfully resized image")
          (error "Could not resize image: %s"
                 (string-replace "\n" "" (buffer-string))))))))

If we want, we can even map this function to a keystroke, manipulating its specific keymap.

(keymap-set image-dired-thumbnail-mode-map "Z" 'image-dired-thumbnail-resize-image)

Links #75

byte-archive.png

Links #74

An Emacs Lisp macro to parse arguments in shell scripts

My Emacs system sometimes creeps out into the shell. This usually happens when I want to provide an additional entry point to data I maintain using Emacs.

This means that in my ~/bin I have a few scripts, written in Emacs Lisp, that are meant to be run from the command line, outside of an Emacs client.

With that, it comes the need to parse command line parameters.

After writing the same code a couple of times, and since back then I could not find an obvious other choice to do the same thing, I decided it was a good use case for a macro, which I called with-positional-args.

Here how it looks like in user-space. This is part of a script I use to search into annotated PDFs in a directory:

#!/usr/local/bin/emacs --script

;; ... omissis ...
;; (preparing load-path and requiring a couple
;; of features, including the one that implements
;; with-positional-args)

(defun pdf-search (pattern path)
  ;; the details are irrelevant in this context
  )

;; Here we go!
(with-positional-args ((pattern :mandatory "You must provide a PATTERN")
                       (path :default "."))
  (pdf-search pattern path))

And here the implementation:

;; Caveman args list parsing
(defun arg-resolver (arg-properties idx)
  (pcase (car arg-properties)
    (:default
     `(or (nth ,idx command-line-args)
              ,(cadr arg-properties)))
    (:mandatory
     `(or (nth ,idx command-line-args)
          (error (or ,(cadr arg-properties)
                     "Undefined error"))))
    (_ (nth idx command-line-args))))

(defmacro with-positional-args (arglist &rest body)
  "Bind command-line arguments as per ARGLIST, then evaluate BODY.

Each element of ARGLIST has the form: (VAR) for optional argument, (VAR
:default VALUE) for specifying a default value when missing, (VAR
:mandatory [MSG]) for required arguments with optional error message
MSG."
  (declare (indent 1))
  `(let ,(cl-loop for (arg-name . arg-properties) in arglist
                  for idx from 3
                  collect `(,arg-name ,(arg-resolver
                                        arg-properties idx)))
     ,@body))

Camping in Stendenitz

This wasn't a lucky season for bikepacking—bad weather, mechanical problems, and lack of time conspired against me. But I managed to catch a break this weekend. I chose a campsite almost at random (I just had a rough distance and direction in mind), but I hit the jackpot. It was a beautiful, quiet place, and I was assigned a wonderful spot right in front of the lake. This will most likely be my last night out this year, but it was a great one.

Other posts