UP | HOME
grm blog. Work is copyrighted unless otherwise stated.
2021-04-18 Sun
^

Make emacs gifs

Emacs developers in their infinite wisdom have provided a way to dump the frame data (i.e. what your GUI emacs shows on screen) into a file in either png, pdf, svg or postscript format. This is achieved though the x-export-frames function.

Capturing emacs frames

The first step to creating our gif is to have a way to export the emacs frame as a png image. First we are going to need a few variables:

(defvar gif--frame-id -1)
(defvar gif--tmp-path "/tmp/gif")

The gif--frame-id will be used to keep the correct order of the frames while the gif--temp-path is the path where the frames will be stored until the gif is generated.

Now to the function that dumps the frame data into the a file. This function will later be assigned at a timer to run every 0.2 seconds and export the emacs frame.

(defun gif--capture-frame ()
  "Capture curent frame."
  (let* ((filename (format "%s/gif-%04d.png" gif--tmp-path (setq gif--frame-id (+ 1 gif--frame-id))))
         (data (x-export-frames nil 'png)))
    (with-temp-file filename
      (insert data))
    (if (> gif--frame-id 9999)
        (gif-stop))))

I set a hard limit to 9999 frames to make sure the %04d is enough space for the numbers as to not mess up ordering and also as a hard stop in case something goes wrong with the timer. 9999 frames in 0.2 fps is around 33 minutes so it's more than enough for a gif.

Setting up the timer and user interface

The two following functions are the user facing controls to create a gif. The gif-start function starts the timer and captures the frames while the gif-stop one stop the timer, renders the gif and then clears the frames from the temporary path.

(defun gif-start ()
  "Start capturing."
  (interactive)
  (if (not (= gif--frame-id -1))
      (gif-stop))
  (setq gif--tmp-path (make-temp-file "emacs-gif" t))
  (setq gif--frame-id -1)
  (setq gif--timer
        (run-with-timer 0 0.2 'gif--capture-frame))
  (message "Started catpuring at %s" gif--tmp-path))

(defun gif-stop ()
  "Stop capturing and render result."
  (interactive)
  (if (= gif--frame-id -1)
      (message "No capture is running.")
    (cancel-timer gif--timer)
    (setq gif--timer (timer-create))
    (setq gif--frame-id -1)
    (call-process "/usr/bin/ffmpeg" nil "*gif*" nil
                  "-y" "-hide_banner" "-loglevel" "error" "-f" "image2" "-r" "5"
                  "-i" (format "%s/gif-%%04d.png" gif--tmp-path)
                  "-filter_complex" "fps=5,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse"
                  (format "%s" gif-output-name))
    (delete-directory gif--tmp-path t)))

Rendering the gif

I first attempted to render the captured frames to a gif using imagemagick but it was way too slow taking around 30 seconds to render a 5-10 sec gif.

After some digging around I found a way to do it way more efficiently in a fraction of that time using ffmpeg. I've tried 30 second long gifs and it only takes around 3-5 seconds on my i7-8700 CPU @ 3.20GHz system.

Here is the comand I use in detail:

ffmpeg -y -hide_banner -loglevel error \
       -f image2 -r 5 \
       -i '%s/gif-%%04d.png' \
       -filter_complex 'fps=5,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse' \
       output.gif

I found out about it here.

This is an example of a gif I can create using this:

emacs.gif