SVG Images in Org Mode

Posted on May 6, 2023

I have taken an interest in chemistry, and have taken to using org-noter to take notes on the books that I read. Since it's organic chemistry, I thought it would be great to somehow incorporate my own diagrams of benzene rings and things into my notes. I like using GIMP for editing images, but thought a simpler image editing tool would be better for making simple diagrams. Since I wanted to use this with Org-mode in Emacs, I wanted something I could integrate somehow. Somewhere on the internet (probably a Reddit post), I discovered org-krita.

Taking Inspiration from Org-Krita

The source code is here, but the short of it is, you create a basic template file that gets copied to a specified directory, a link to this copy is inserted into the document, and Krita is opened to allow you to edit the file. There is a nice touch that org-krita adds, which is to add a watcher to the link, which automatically re-renders the document when the file is saved in Krita, assuming you have configured inline images to be rendered at all.

There is some other plumbing code that goes into making this work, but the result is specialized code which only work on Krita files. The source code hasn't been updated in a year, and I have an unspoken rule that if the code's been getting stale for that long, it's better to take the bits I want and get those working than install the package spend a lot of effort tuning the functionality through a layer of stale code. So, I shamelessly copied the code that gave me the functionality I wanted, and it was cool to see. I thought the live-update feature in particular was pretty slick.

There's just one problem: I don't like Krita. It's a fine image editing program, but I personally prefer GIMP, and it's just as (surprisingly) difficult to draw a benzene ring in both programs. In short, this was a lot of code for a compromise.

Finding an Alternative

With org-krita's implementation in mind, I went back to the drawing board. Somehow I got it in my head to try Inkscape (an SVG image editor), and lo and behold it's super easy to draw benzene rings there, since SVG markup supports circles and arbitrary polygons. Inkscape is also much simpler than GIMP or Krita, so using it doesn't feel like a compromise, it feels like the right tool for the job.

The last step was to figure out how to integrate it into Emacs. Once I evaluated what I really wanted, it was easy, as a lot of the functionality is built-in. In short, I boiled it down to three scenarios:

Inserting an Image Flexibly

I'll get to inserting an image, but it's easier to talk about opening it first. Emacs can call out to any program that's on its execution path. To open Inkscape, the code would look like this:

(defun as/open-in-inkscape (path)
  "Open a file at PATH with InkScape"
  (call-process "inkscape" nil 0 nil (expand-file-name path)))

For those unfamiliar with elisp, expand-file-name makes sure any short-notation paths (e.g. ~/file.path) get properly expanded to absolute paths.

Back to the issue at hand: Wouldn't it be nice to make this work for <insert preferred program here>? This isn't hard, it turns out. I'm on Linux, which provides the Windows functionality of opening programs in default applications through a program called XDG. I don't believe Emacs has native support for this, but it is quite easy to add:

(defun as/xdg-open (path)
  "Open a file at PATH with the default application per XDG"
  (call-process "xdg-open" nil 0 nil (expand-file-name path)))

This also comes in handy when you want to open a file from dired or even mu4e if you're into managing email from Emacs (I sure am). I don't know about Windows, but I'm pretty sure opening files with default applications isn't too bad to get working there either, probably depends on if you're using Windows Subsystems for Linux to run Emacs, no idea. Anyway, so I'm sure you can see where this is going: Now I don't need a special function to open a Krita file, I have a generic function that opens any file in whatever program I have set to be the default for my system. That's flexibility, that's power.

Now, I can use this simple function as part of my insert-link-then-open scheme. Here's the code:

  (defun as/org-svg-insert ()
    (interactive)
    (let* ((template (expand-file-name "templates/inkscape.svg" org-directory))
     (img-dir (expand-file-name "images" org-directory))
     (file (read-string "Image name: "))
     (output (expand-file-name file img-dir)))
(if (file-exists-p output)
    (org-insert-link nil output)
  (progn
    (copy-file template output)
    (org-insert-link nil output)
    (as/xdg-open output)))))

Admittedly, this code is more complex. It fetches a template image (SVG files require a minimal amount of markup), copies it to an images directory (where all of my images used in org files are kept) after prompting the user for a file name, and opens the image in its default program (i.e. Inkscape). This is a pretty minimal example, I made it a little more fancy by providing a list of existing images to choose from when prompting for an image name, in case I don't want to create a new image. If I go that route, there's a good chance I don't want to jump into Inkscape, so the function only opens the image if it's new. Here's the code for that:

  (defun as/org-svg-insert ()
    (interactive)
    (let* ((template (expand-file-name "templates/inkscape.svg" org-directory))
     (img-dir (expand-file-name "images" org-directory))
     (file (completing-read "Image name: " (directory-files img-dir nil "[^ \.]\\{1,2\\}$")))
     (output (expand-file-name file img-dir)))
(if (file-exists-p output)
    (org-insert-link nil output)
  (progn
    (copy-file template output)
    (org-insert-link nil output)
    (as/xdg-open output)))))

The difference here is I use completing-read to prompt for a file name instead. The motivation for the ugly regular expression at the end is that directory-files returns all listings in the directory even the current-directory and parent-directory markers (. and .., respectively), and I don't want to see those. I acknowledge that the function is opaque by layman's standards, but the regular expression is the hardest part to understand, and it's not even necessary.

At any rate, this solves my first two scenarios. At this point, it's worth mentioning that this is the only function I needed to add once I got past the disappointment of not having live updates.

Opening Images Flexibly

"What?" I hear you asking, "wasn't that the point of adding that xdg-open function?" This is true! But I don't want to add another keybinding to org-mode, it has plenty of its own! In fact, org-mode provides a function called org-open-at-point, which is configurable! I would rather use that. In keeping with my irrational desire to keep things generic, we can set org-open-at-point to open any file using my handy as/xdg-open function:

(org-link-set-parameters "file" :follow #'as/xdg-open)

Perhaps unsurprisingly, links can represent a lot of things, and in org-mode all you have to do is prepend the URI part of the link with what that link represents. Examples include http for browser links, irc if you use Emacs as an IRC client (I sure do!), eww if you use Emacs as a web browser (I sure do!), as well as the one we're interested in: file. As an aside, file happens to be the default if you don't specify anything on the URI, which is also handy for me, as that's what I would expect the default to be.

With that, we're done! The only pity is that we have to call org-redisplay-inline-images every now and again.

Retro

Since my projects never go this well, I wanted to do a quick retrospective on how this situation played out: I found some functionality I wanted, I wrote a modest function to get it, and was able to extend this functionality to other files besides images without any extra work. There is the redisplay images thing, and it would be nice to somehow incorporate the inserting component into another function, but I was still able to leverage built-in functionality to get a lot of what I wanted, plus some stuff that I hadn't even considered getting (almost) for free. A lot of times after I finish these projects I feel like I'm working for Emacs, it's rewarding to have it go the other way.