A workflow to quickly add photos to org-mode notes

I was at a conference this week and a colleague was making notes using Evernote on her laptop and taking photos of key slides on her phone which then appeared in her notes. Of course I was making my notes in org-mode but I was envious of this behaviour so decided to emulate it.

With the function below, I can take a photo on my phone and upload to google drive (I use Photo & Picture Resizer, but you could use anything you like to get the pictures onto your computer). Then with a single command in Emacs, I am prompted with a list of photos in the folder to which they are uploaded, with the most recent first. The selected image is then:

  1. Moved the same directory as my org-mode notes file
  2. Renamed based on the heading of the current section in my notes, with a numeric suffix if there is already a photo with that name
  3. Linked in the notes and then the image is displayed

Here is a demonstration:

insert-slide-image.gif

Here is the code:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; add image from conference phone upload                                 ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; use case is taking a photo of a slide in a conference and uploading
;; it to google drive or dropbox or whatever to get it on your
;; computer. You then want to embed it in an org-mode document by
;; moving it to the same folder and renaming according to the current
;; section of the org file, avoiding name clashes

;; required libraries
(require 'dash)
(require 'swiper)
(require 's)

;; start directory
(defvar bjm/conference-image-dir (expand-file-name "/path/to/image/upload/dir"))

(defun bjm/insert-conference-image ()
  "Insert image from conference directory, rename and add link in current file.

The file is taken from a start directory set by `bjm/conference-image-dir' and moved to the current directory, renamed and embedded at the point as an org-mode link. The user is presented with a list of files in the start directory, from which to select the file to move, sorted by most recent first."
  (interactive)
  (let (file-list target-dir file-list-sorted start-file start-file-full file-ext end-file end-file-base end-file-full file-number)
    ;; clean directories from list but keep times
    (setq file-list
          (-remove (lambda (x) (nth 1 x))
                   (directory-files-and-attributes bjm/conference-image-dir)))

    ;; get target directory
    (setq target-dir (file-name-directory (buffer-file-name)))

    ;; sort list by most recent
  ;; http://stackoverflow.com/questions/26514437/emacs-sort-list-of-directories-files-by-modification-date
  (setq file-list-sorted
        (mapcar #'car
                (sort file-list
                      #'(lambda (x y) (time-less-p (nth 6 y) (nth 6 x))))))

  ;; use ivy to select start-file
  (setq start-file (ivy-read
                    (concat "Move selected file to " target-dir ":")
                    file-list-sorted
                    :re-builder #'ivy--regex
                    :sort nil
                    :initial-input nil))

  ;; add full path to start file and end-file
  (setq start-file-full
        (expand-file-name start-file bjm/conference-image-dir))
  ;; generate target file name from current org section
  ;; (setq file-ext (file-name-extension start-file t))

  ;; my phone app doesn't add an extension to the image so I do it
  ;; here. If you want to keep the existing extension then use the
  ;; line above
  (setq file-ext ".jpg")
  ;; get section heading and clean it up
  (setq end-file-base (s-downcase (s-dashed-words (nth 4 (org-heading-components)))))
  ;; shorten to first 40 chars to avoid long file names
  (setq end-file-base (s-left 40 end-file-base))
  ;; number to append to ensure unique name
  (setq file-number 1)
  (setq end-file (concat
                  end-file-base
                  (format "-%s" file-number)
                  file-ext))

  ;; increment number at end of name if file exists
  (while (file-exists-p end-file)
    ;; increment
    (setq file-number (+ file-number 1))
    (setq end-file (concat
                    end-file-base
                    (format "-%s" file-number)
                    file-ext))
    )

  ;; final file name including path
  (setq end-file-full
        (expand-file-name end-file target-dir))
  ;; rename file
  (rename-file start-file-full end-file-full)
  (message "moved %s to %s" start-file-full end-file)
  ;; insert link
  (insert (org-make-link-string (format "file:%s" end-file)))
  ;; display image
  (org-display-inline-images t t)))
  • Nicolò Balzarotti

    Thanks, I changed it just to take the source dir as a parameter (so that I can use different functions for different sync sources, and one for local-screenshots too) and to save the images in a subfolder called imgs. Other than this, it’s extremely useful; i was using a bash script to do the same thing, this is better 🙂

    • Glad you like it! I like the idea of a subfolder – I should have thought of that to keep things tidy!

    • philipp

      I just started to use and like this function a lot – thanks for this, Ben! But I face problems implementing the two nice add-ons Nicolò mentions – source dir as parameter and pushing the images into a subfolder. Could you share how that is done exactly? Sorry for the noob-question. Thank you!

      • Nicolò Balzarotti

        Hi, here it is:

        https://gist.github.com/nico202/1c645c2a0a6cfb5a06bf2f6717d0cf54

        the first 3 functions are the one you can create to customize the source directory. If they are called with the prefix argument (C-u) the file is moved, else is just copied.

        I don’t code in elisp so probably there’s a better way to do all of this, it would be useful if anybody with more experience can comment on it.

        Thanks, Nicolò

        • philipp

          That’s kind of you, thanks! I think with this I can work it out.

        • Jay Dixit

          This is awesome! How can I get your version to copy the image file instead of moving it?

          • Nicolò Balzarotti

            You mean, by default? Because right now you can just call it using the prefix (like: C-u M-x nx/insert-screenshot) and you are done.
            If you want to copy by default, you can just edit:

            (defun nx/insert-screenshot (arg)
            (interactive “P”)
            (bjm/insert-image “~/Immagini/Screenshots/” arg))

            replacing
            (bjm/insert-image “~/Immagini/Screenshots/” arg)
            with
            (bjm/insert-image “~/Immagini/Screenshots/” t))

            This will disable the C-u option. Else, you can replace “arg” with “(not arg)”: that way you have copy by default, move with C-u

          • Jay Dixit

            Wow, that works perfectly. Thank you!

  • Thank you man!

    I did some changes to fit my needs, you can see it here: https://github.com/squiter/emacs-dotfiles/blob/e9babf4e5b71e6b45e915ffd718592013aacb7ba/emacs.d/conf/orgmode/init-org-insert-image.el

    Changes:
    I wrapped your function in other two that change the directory to search the image and now the images are copied (instead of moved) to a new directory: “/images//”

    So, thank you so much! 😀

  • NoonianAtall

    Very nice. It would be great if this could be improved into a more general-purpose solution that could show thumbnails of images in a directory for choosing.