Images in Org mode

:: emacs, org

Org-mode prides itself on being purely plain text. That doesn’t mean that it can’t do images, though!

In fact, org-mode has built-in support for including images in its documents. Not only can those images be inserted into exported formats like HTML and LaTeX, they can be displayed inline in emacs.



Displaying images

Displaying inline images is as simple as inserting a link to an image without a description and executing org-toggle-inline-images. For example:

[[file:/path/to/an/image.png]]

and hitting the default binding for org-toggle-inline-images, C-c C-x C-v causes the link to be overlayed with the image it links to.



Image size

While this is great, when I started using this I quickly noticed that org doesn’t like displaying large images inline. If the image takes up much or all of the vertical screen space, scrolling through the buffer with that image is a nightmare; this happens because inline images are overlayed over the link text in the buffer, meaning that the image is considered by emacs to be on a single line. Emacs’ minimum scrolling unit is a line, so either the whole line (image) is visible or not.

While there are some workarounds like inserting images “sliced” into multiple smaller images that each fit on one line, or a relatively new pixel-based scrolling feature, I have found that these each have their own issues. Instead, I find it easiest to just resize the image being linked so that it is a reasonable size and the above scrolling behavior isn’t problematic.

There are two ways to do this; the first is builtin but only works in recent versions of emacs/org-mode, and I have sometimes had issues with it in exports. Nonetheless, it is certainly the simplest for just viewing images within org. Just add an ATTR_ORG line before the image specifying a width for the inserted image (in pixels) before toggling inline images.

#+ATTR_ORG: :width 1200
[[file:/path/to/an/image.png]]

The second way is to resize the image itself. This might seem like a hassle, which it can be, but it can be made pretty painless. It also integrates pretty well with the editing functions I describe below. To do this, I wrote a function that uses an external tool to resize the image under point, and bind this to a key of choice. The function can be found below, along with some other useful functions.



Incorporating images into my note-taking workflow

I often find org’s ability to show inline images most useful when taking notes. It’s said that a picture is worth a thousand words, and indeed I often find it easiest when taking notes to just take a quick screenshot of something rather than trying to describe it; or worse, making ascii-art to depict it – much as I love ascii art, it really isn’t an efficient way to draw pictures (even with artist-mode). Furthermore, a simple screenshot may not be enough: I typically want to annotate the image, such as to highlight some portions of it or write comments directly beside the item of interest.

To these ends, I wrote a pair of functions that allow me to quickly take screenshots and edit images directly from emacs. This eliminates the cognitive burden of opening a separate screenshot or image editing program etc, as well as feeling very natural. I bind these functions to easily accessible keys, and now inserting and marking up images in my notes feels just as natural as entering text.



The code

These three functions implement the behavior of taking screenshots, editing images, and resizing them. They are intended to be invoked within org mode, with the point at the location at which to insert the image or over the image to edit.

A few notes:

  • The screenshot function automatically creates a resources directory in same directory as current org file.
  • These functions really just delegate to external programs in a convenient way, so they depend upon these programs being installed to work. The specific programs are import, mogrify, and gimp. The first two are part of the imagemagick package on Ubuntu and the last to the gimp package.

Given those caveats, here’s what I use.

(defvar ll/org/insert-screenshot/redisplay-images t
  "Redisplay images after inserting a screenshot with
`ll/org/insert-screenshot'?")

(defun ll/org/insert-screenshot (&optional arg)
  "Capture a screenshot and insert a link to it in the current
buffer. If `ll/org/insert-screenshot/redisplay-images' is non-nil,
redisplay images in the current buffer.

By default saves images to ./resources/screen_%Y%m%d_%H%M%S.png,
creating the resources directory if necessary.

With a prefix arg (C-u) prompt for a filename instead of using the default.

Depends upon `import` from ImageMagick."
  (interactive)
  (unless (or arg
              (file-directory-p "./resources"))
    (make-directory "resources"))
  (let* ((default-dest
           (format-time-string "./resources/screen_%Y%m%d_%H%M%S.png"))
         (dest (if arg
                   (helm-read-string "Save to: " default-dest)
                 default-dest)))
    (start-process "import" nil "/usr/bin/import" dest)
    (read-char "Taking screenshot... Press any key when done.")
    (org-insert-link t (concat "file:" dest) "")
    (when ll/org/insert-screenshot/redisplay-images
      (org-remove-inline-images)
      (org-display-inline-images))))


(defvar ll/org/edit-image/redisplay-images t
  "Redisplay images after editing an image with `ll/org/edit-image'?")

(defun ll/org/edit-image (&optional arg)
  "Edit the image linked at point. If
`ll/org/insert-screenshot/redisplay-images' is non-nil, redisplay
images in the current buffer."
  (interactive)
  (let ((img (ll/org/link-file-path-at-point)))
    (start-process "gimp" nil "/usr/bin/gimp" img)
    (read-char "Editing image... Press any key when done.")
    (when ll/org/edit-image/redisplay-images
      (org-remove-inline-images)
      (org-display-inline-images))))

(defun ll/org/resize-image-at-point (&optional arg)
  "Resize the image linked at point. If
`ll/org/insert-screenshot/redisplay-images' is non-nil, redisplay
images in the current buffer."
  (interactive)
  (let ((img (ll/org/link-file-path-at-point))
        (percent (read-number "Resize to what percentage of current size? ")))
    (start-process "mogrify" nil "/usr/bin/mogrify"
                   "-resize"
                   (format "%s%%" percent)
                   img)
    (when ll/org/edit-image/redisplay-images
      (org-remove-inline-images)
      (org-display-inline-images))))

(defun ll/org/link-file-path-at-point ()
  "Get the path of the file referred to by the link at point."
  (let* ((org-element (org-element-context))
         (is-subscript-p (equal (org-element-type org-element) 'subscript))
         (is-link-p (equal (org-element-type org-element) 'link))
         (is-file-p (equal (org-element-property :type org-element) "file")))
    (when is-subscript-p
      (user-error "Org thinks you're in a subscript. Move the point and try again."))
    (unless (and is-link-p is-file-p)
      (user-error "Not on file link"))
    (expand-file-name (org-element-property :path org-element))))