Labeling wedding pictures with Emacs
Context
I am going through the PAIP book by Peter Norvig using emacs-lisp and would encourage every Emacs fan to try that. However, AI cannot solve all of our problems just yet! Sometimes you just need the computer to guide you to accomplish a repetitive task that requires human input. This article is about one such case: labelling wedding pictures for making an album.
Problem statement
In broad terms, the problem is: I have a large collection (>100) of items that I want to label those items with a fixed set of tags. I have the knowledge to decide what tag to put on each item them but the process of applying them is slow.
I can later use those tags for filtering through the collection efficiently. For example, after finishing the labelling, I can filter “excellent” and “black and white” pictures from the “ceremony” of my wedding.
This problem statement is applicable to a broad range of situations – just think about your last month at work, and it is likely you have encountered such a situation!
Technologies
I use MacOS and Emacs, so I decided to write an Emacs mode and use a command line tool to manipulate tags on Mac OS X files, and to query for files with those tags.. Once done with labelling the pictures I can use the MacOS Finder to narrow down by tags.
I also wanted a UI as simple as possible, like magit’s transients commands: one key to say it is a good pic, one key to say before the ceremony, one key to remove all the tags, etc.
Here is what the results look like. First, when you start the mode you can hit enter to are show a picture to tag and you can press keys to select tags:
Once you validate the tags, it shows you the next picture immediately:
The pictures I used for this example are respectively by Calum Lewis and Joseph Gonzalez
Let me walk you through how to build this!
Building the tabulated list
First off, if you haven’t watched the talk conquering kubernetes with emacs, add it to your bookmarks and go watch it after reading this. I was very inspired by this talk, and Adrien did a terrific job at demystifying tabulated mode and transient for me.
In the talk he explains how to customize transient commands, which are used by the magit interface as well as tabulated list mode an Emacs mode to display and interact with a list of items.
Let’s start by building the list of items. The list should contain all the pictures in my photos folder and items should be green if they are tagged and pink otherwise:
(defun wedding-propertize (fields)
"Given a list of fields representing a photo
propertize them in green if the photo as no tags
and pink otherwise. Tags are stored in the field
with index 2"
(-map (lambda (x)
(propertize x 'font-lock-face
`(:foreground
,(if (string= (nth 2 fields) "")
"pink"
"green"))))
fields))
(defun laurent-add-number (fields)
"Extract the number from the filename field
and append it in from of the fields"
(push (replace-regexp-in-string
".*-\\|.jpg" ""
(first fields))
fields))
(defun photoline->tabulatedmode (x)
"Given x like: 'Photo-7.jpg|beforeceremony,good'
return propertize fields suitable for tabulated mode
for this picture, representing its number, name and tags"
`(,(first (split-string x "|"))
,(vconcat (wedding-propertize
(laurent-add-number
(split-string x "|"))))))
(defun laurent-photos ()
"Return list of photos that can be used for the tabulated mode"
(interactive)
(let ((lines
(split-string
(replace-regexp-in-string
"\n$" ""
(shell-command-to-string
"cd ~/Downloads/photos && tag --list | sed -E 's/jpg[[:blank:]]*/jpg|/g'"))
"\n")))
;; lines is like
;; ("Photo-7.jpg|beforeceremony,good" "Photo-34.jpg|bad,dinner" ... )
(-map #'photoline->tabulatedmode lines)))
And a derived mode to display them
(defun wedding-popup()
(interactive)
"implementation coming later")
(defvar wedding-evil-mode-map (make-sparse-keymap))
(evil-define-key 'motion wedding-evil-mode-map
(kbd "t") #'wedding-popup)
(define-derived-mode wedding-mode tabulated-list-mode "Wedding"
"Wedding mode"
(let ((columns [("#" 5 (lambda (A B)
(let ((a (string-to-number (aref (cadr A) 0)))
(b (string-to-number (aref (cadr B) 0))))
(< a b))) )("Picture" 30 t) ("Tags" 100 t)])
(rows (laurent-photos)))
(buffer-disable-undo)
(setq truncate-lines t)
(setq mode-name "Wedding")
(setq major-mode 'wedding-mode)
(setq tabulated-list-sort-key '("#" . nil))
(setq tabulated-list-format columns)
(setq tabulated-list-entries rows)
(tabulated-list-init-header)
(use-local-map wedding-evil-mode-map)
(tabulated-list-print)
(hl-line-mode 1)))
(defun wedding ()
(interactive)
(evil-set-initial-state 'wedding-mode 'motion)
(switch-to-buffer "*wedding*")
(wedding-mode))
So at this point we can call M-x “wedding” and see the list of photos like:
The only thing left is adding interactivity to this list.
Creating and binding actions
We can start by defining functions to get the current picture, add tag, remove tags and open a given picture in the preview app or Emacs:
(defun tag-add (tag fname)
(shell-command-to-string (format "/usr/local/bin/tag -a '%s' '%s'" tag fname)))
(defun tag-remove-all (fname)
(shell-command-to-string (format "tag -r $(tag -l '%s' | awk '{print $2}')
'%s'" fname fname)))
(defun wedding-fname ()
"Return the path to the picture at point"
(interactive)
(format
"/Users/laurent/Downloads/photos/%s"
(aref (tabulated-list-get-entry) 1)))
(defun wedding-open-in-preview ()
(interactive)
(shell-command-to-string (format "open '%s'" (wedding-fname))))
(defun wedding-show-details ()
"Show the picture at point in emacs"
(interactive)
(delete-other-windows)
(find-file-other-window (wedding-fname))
(evil-window-increase-width 130)
(other-window 1)
(wedding-popup)) ;; see below
(defun wedding-delete-all-tags()
(interactive)
(tag-remove-all (wedding-fname))
(let ((p (point)))
(wedding-mode)
(goto-char p)))
And we can then bind those actions to the wedding-popup that we redefined as a transient command:
(defun wedding-tag (&optional args)
"Tag the picture and go to the next one"
(interactive (list (transient-args 'wedding-popup)))
(-map (lambda (x) (tag-add x (wedding-fname))) args)
(delete-other-windows)
(let ((p (point)))
(wedding-mode)
(goto-char p)
(next-line)
(wedding-show-details)))
(require 'transient)
(define-transient-command wedding-popup ()
"Manipulating pictures, adding, removing tags and opening"
["Arguments"
["Quality"
("w" "Black and white" "blackandwhite")
("b" "Bad" "bad")
("a" "Average" "average")
("g" "Good" "good")
("e" "Excellent" "excellent")]
["Location"
("q" "Before" "before")
("c" "Ceremony" "ceremony")
("i" "In between" "inbetween")
("d" "Dinner" "dinner")]]
["Action"
("RET" "Tag" wedding-tag )
("o" "Open" wedding-open-in-preview )
("x" "Remove all the tags" wedding-delete-all-tags)])
Now we can launch the mode the same way we did before, but when I press enter it pulls up a picture, that I can tag. After tagging the picture, the mode moves me automatically onto the next picture.
NOTE: If you try to do this at home, you may have to set the following settings to remove warnings when opening large files:
(setq large-file-warning-threshold nil)