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: