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.

  • disqus_NGUkO53xWm

    this is great!

    anyone knows how to change the directory where the images are copied to so thats its user definable? so instead of saving it in the org file dir a separate dir?

  • Nice tip. I use Copyq to directly move and generate image links which seems more fluent 🙂