Monospace Games

How to set up emacs as an SVG editor

In this post I'll describe how to set up emacs as an SVG editor, complete with autocompletion, error checking, and live previews. Even though there are at least two emacs packages that provide a point-and-click interface for SVG editing (sketch-mode and el-easydraw) we'll be looking into editing SVG files textually instead.

In usual emacs fashion almost all of the functionality we need is actually built-in, we just need to enable and configure things. Nevertheless there'll still be a fair bit of elisp involved to get things working smoothly.

This premise may sound odd to some, but remember that SVG is XML, XML is text, and emacs is a text editor capable of rendering vector graphics. So it's kind of a match made in heaven. Plus the SVG format does provide a lot of tools necessary to reason about images on an abstract level. With a bit of knowledge about them you can get quite efficient at typing images.

Before we begin, here's a short demonstration of the end result:

Step 1: Setting up nXML mode schemas for autocompletion and error checking

nXML mode is the default major mode emacs uses for XML-based formats such as SVG. It's capable of using schemas, which are basically document descriptions for such formats. Through schemas emacs can tell which element and attribute names are valid, and provide autocompletion and error-checking.

There are several schema formats such as:

The SVG schema we'll use is in the DTD format, provided directly by W3C in Appendix A of the SVG 1.1 spec (second edition). You can take a look at it here here. We're going to convert it to the RELAX NG Compact format which is what nXML mode works with. For this we'll use trang, which can convert schemas between any of the above listed formats.

# Install trang
you@home:~$ sudo apt-get install trang # or equivalent ...
# Download the schema from W3C
you@home:~$ wget https://www.w3.org/Graphics/SVG/1.1/DTD/svg11-flat-20110816.dtd
# Convert it to RELAX NG Compact format with trang
you@home:~$ trang svg11-flat-20110816.dtd svg.rnc

Now that we have the schema it's time to make emacs use it. But first move the schema to a location that's convenient for you. I use the no-littering package, so I created a ~/emacs.d/var/xml-schemas directory and moved it there. In the same directory create a schemas.xml file and insert the following into it:

<locatingRules xmlns="http://thaiopensource.com/ns/locating-rules/1.0">
  <namespace ns="http://www.w3.org/2000/svg" typeId="SVG" />
  <uri pattern="*.svg" typeId="SVG"/>
  <documentElement localName="svg" typeId="SVG"/>
  <typeId id="SVG" uri="svg.rnc"/>
</locatingRules>

Now make this schemas.xml file visible to emacs by adding its path to the rng-schema-locating-files list. This is how I've done it in my init:

(with-eval-after-load 'rng-loc
  (add-to-list 'rng-schema-locating-files
               (no-littering-expand-var-file-name "xml-schemas/schemas.xml")))

If you examine the value of the rng-schema-locating-files variable you'll see the schemas that are shipped with your emacs build.

Now open a SVG file, and enable nxml-mode. If you get a message saying "Using schema <path-to-your-schema>" all systems are operational! You can now get suggestions for element and attribute names with modes like company or corfu, and navigate between errors using the rng-next-error and rng-previous-error functions.

What we've done so far is not exclusive to SVG, it can be done for all XML based formats that you can find a schema for, such as RSS, DMARC reports, SMIL etc.

Step 2: Installing and configuring svg-preview-mode for live previews

Above configurations make it significantly easier to edit SVG's, but we still don't have a convenient way of viewing our images as we edit them. You can try changing to image-mode and back but this gets tiresome fast. The default buffer cloning mechanisms such as clone-buffer or clone-indirect-buffer also aren't helpful. So I developed a minor mode called svg-preview-mode for this task. It gives you a side by side view that updates as you type. You can install it by following the instructions here.

I've kept svg-preview-mode short to keep it simple and unopinionated - it has an empty keymap for you to add convenience functions as you please. Here's how I've configured it for my own use:

(with-eval-after-load 'svg-preview-mode
  ;; Enable company while editing svg's
  (add-hook 'svg-preview-mode-hook
            #'(lambda ()
                (company-mode)
                (setq-local company-minimum-prefix-length 0)))

  ;; Binding for inserting self-closing tag delimiters
  (define-key svg-preview-mode-map (kbd "C-.")
              #'(lambda () (interactive)
                  (insert "< />")
                  (backward-char 3)))

  ;; Bindings for numerical value adjustments
  (define-key svg-preview-mode-map (kbd "C-1") #'org-decrease-number-at-point)
  (define-key svg-preview-mode-map (kbd "C-2") #'org-increase-number-at-point)

  ;; Binding to generate open/close tag pairs via emmet (requires emmet-mode)
  (define-key svg-preview-mode-map (kbd "C-j") #'emmet-expand-line)

  ;; Bind TAB to jump to next attribute name when done entering current
  ;; attribute's value
  (defun my/svg-edit-tab-dwim ()
    (interactive)
    (if (eq (char-after) ?\")
        (progn
          (forward-char 1)
          (insert " ")
          (company-complete))
      (indent-for-tab-command)))
  (define-key svg-preview-mode-map (kbd "TAB")
              #'my/svg-edit-tab-dwim))

;; This hook preserves company-mode when you switch to image mode and back
;; while editing SVG files. It's necessary because major mode changes reset
;; active minor modes and local variables.
;; svg-preview-mode also uses a hook to restore itself.
(add-hook 'nxml-mode-hook
          #'(lambda ()
              (cond ((bound-and-true-p svg-preview-mode)
                     (company-mode)
                     (setq-local company-minimum-prefix-length 0)))))

Final Remarks

We're done! Now you can do text2image in a way that's more efficient than any of the state of the art methods. But before you go secure a billion dollar investment from Google or Amazon for this new AI breakthrough there's one more detail that I must share: not all SVG renderers are created equal. Emacs uses librsvg, which does not support animated SVG's, so the above preview method won't work for anything animated. librsvg also seems to use white as the default text color while firefox uses black. At any rate make sure to test your images in your target renderer.

If you want to eliminate all such rendering ambiguities you can use librsvg's command line utilities (found in the librsvg2-bin package on debian-based distros) to convert your images to a bitmap format.

Author's Gallery

Here's a few images I made while working on this stuff

The image of a crescent moon is commonly used in websites to indicate dark mode. This is a simple implementation of it using two circles, one for moon and one for earth's shadow.

<svg xmlns='http://www.w3.org/2000/svg' width="500" height="500" viewBox="0 0 100 100">
  <defs>
    <mask id="shadow">
      <rect x="0" y="0" width="100" height="100" fill="white" />
      <circle cx="35" cy="35" r="27.5" fill="black" />
    </mask>
  </defs>
  <circle cx="50" cy="50" r="37.5" fill="#ffd700" mask="url(#shadow)" />
</svg>
Image of a crescent moon

Here's my take on the coveted lip bite emoji. For whatever reason I couldn't nail the expression down so it ended up looking kind of anguished/creepy? At any rate emoji seem to be a fun way to practice.

Image of the lip bite emoji

And finally here's the logo I made for this website. It uses a lowercase pixel font with a stripe effect and a slight glow.

Monospace games logo

And that's it for this post. Thanks for reading!