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)))
Advertisement

12 comments

  1. 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 🙂

    Like

    1. 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!

      Like

      1. Hi, here it is:


        ;; [[elfeed:pragmaticemacs.com#http://pragmaticemacs.com/?p=752][A workflow to quickly add photos to org-mode notes]]
        ;; use case is taking a photo of a slide in a conference and uploading
        ;; it to syncthing and get it on your computer. You then want to embed
        ;; it in an org-mode document by moving it to the subfolder and
        ;; renaming according to the current section of the org file, avoiding
        ;; name clashes
        ;; required libraries
        (require 'dash)
        (require 'swiper)
        (require 's)
        (require 'f)
        ;; Default move, copy on prefix (C-u)
        (defun nx/insert-screenshot (arg)
        (interactive "P")
        (bjm/insert-image "~/Immagini/Screenshots/" arg))
        ;; This defaults to copy: move with prefix (C-u)
        (defun nx/insert-syncthing-photo (arg)
        (interactive "P")
        (bjm/insert-image "~/Syncthing/Photos/" (not arg)))
        ;; Always copy, ignore prefix
        (defun nx/copy-syncthing-photo (arg)
        (interactive "P")
        (bjm/insert-image "~/Syncthing/Photos/" t))
        (defun bjm/insert-image (image-dir copy)
        "Insert image from conference directory, rename and add link in
        current file.
        The file is taken from a start directory set by `image-dir' and
        copied/moved to the img subdirectory, 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. The `copy` argument decides if it will be copied
        or moved"'
        (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 subfolder)
        (setq subfolder "imgs")
        ;; clean directories from list but keep times
        (setq file-list
        (-remove (lambda (x) (nth 1 x))
        (directory-files-and-attributes image-dir)))
        ;; get target directory
        (setq target-dir (concat
        (expand-file-name subfolder (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
        (let (action)
        (setq action (if copy "Copy" "Move"))
        (setq start-file (ivy-read
        (concat action " 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 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
        (concat (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
        (message (concat target-dir end-file))
        (while (file-exists-p (concat target-dir 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))
        ;; if target-dir does not exists, create it, else copy-file will fail
        (if (not (f-directory? target-dir))
        (make-directory target-dir))
        ;; rename file if move it t
        (if copy
        (copy-file start-file-full end-file-full)
        (rename-file start-file-full end-file-full))
        (let (action)
        (setq action (if copy "Copied" "Moved"))
        (message action " %s to %s" start-file-full end-file))
        ;; insert link
        (insert (org-make-link-string (format "file:%s" (concat "./" subfolder "/" end-file))))
        ;; display image
        (org-display-inline-images t t)))

        view raw

        org-image.el

        hosted with ❤ by GitHub

        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ò

        Like

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

        Like

      3. 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

        Like

  2. 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.

    Like

  3. 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?

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s