Writing an Asciinema to Gif Tool

I've just recently done some looking into tools to record your terminal to Gifs and while Asciinema seems like its gotten the recording part down, and asciinema-rs has conveniently given us a nice Rust implementation of it, I haven't found a solution that will convert to a gif without Electron or a web browser! I'm really trying to get away from electron and I think it would be a great use to have a gif renderer for asciinema files written in Rust and without any extra dependencies on Electron.

I wanted to see if anybody had ideas of libraries that could be used to render the terminal images to PNG format which could then be fed into gifski to convert to gif.

2 Likes

I suppose if you have access to the frame buffer directly you can use the png crate's encoder:

1 Like

That's good, now I just have to find a terminal emulator that I can embed and get the framebuffer from. Obviously there's allacritty, but I don't know if I can use that as a library or not.


Or maybe we could lay the text out manually, using glyph-brush for the text rendering, surfman for the offscreen rendering context, and vte for the ANSI parser.


Actually vt100 looks like it could be great for parsing the terminal input. So the idea then would be:

  1. Use surfman to create an offscreen rendering context
  2. Use vt100 to read the terminal frames extracted from the asciinema file
  3. Use glyph-brush to render text in the layout and style retrieved from vt100 to the surfman surface
  4. use surfman to get the framebuffer with the rendered pixels
  5. Feed the pixels to the png encoder and
  6. Feed the png images to gifski to convert to a gif

That doesn't sound horrible.

2 Likes

I'm actually thinking that an easier and more useful approach would be to create an SVG representation of each terminal frame and render that to a PNG before feeding it to gifski.

Then we could render to svg animations like termtosvg. Also, we might as well throw animated PNGs with apng in there, too.

Then we just have to find a good SVG renderer. I'm thinking skia.


This looks like a good svg crate, writing the SVG files.


Oh, and resvg is pure rust SVG renderer that might be suitable. It would be nice to get away from C++ bindings to skia.


OK, I just tested out resvg with the pure rust raqote backend for rendering and it seems like it will work great. I was able to render an SVG image with text, gradients, and paths and it worked perfect. Now I've just got to put it all together. The latest plan:

  1. Parse the asciinema file to get the terminal frames
  2. Feed terminal frames to vt100 to parse out the layout and color information.
  3. Use information from vt100 and feed that to svg to create an svg representation of the terminal frame
  4. Use resvg to render the frame to a png
  5. Collect all of the PNG frames and render them with gifski.

It would be great to support the SVG templates from termtosvg, too.


Starting a repo:

https://github.com/katharostech/cast2gif

We'll see where it goes. :slight_smile:

5 Likes

It looks like this is going to work! I've got basic SVG frame generation working, and I've just finished rendering the SVG to PNG as well. Only step left for a basic working version is to stitch the PNGs together with something like gifski. Here is an example of using the gifski CLI on the resultant PNGs without having the associated timing data:

3 Likes

That looks slick!
You should write up a blog/tutorial about the process.

1 Like

Oh, that's not a bad idea. :slight_smile:

It was a fun weekend project and is actually pretty simple because all of the difficult work is offloaded to other crates.

Something is still a little broken with it, but its coming along :slight_smile:

1 Like

Thanks for sharing this tool.
But my first test left the output .gif empty. Is this a known problem ?
The playback of the .cast file (by means of asciinema-rs) looks ok.

1 Like

@CapelliC I'm glad that there's a use for it! It only worked for a second before I started making changes to improve the parallelism. I could have broken it right before I pushed it so it wouldn't be surprising if it didn't work. I think I'll have it working today, tomorrow, or the next day.

Of course, I noticed those 2 warnings. Guess they are related to the problem, but I'm too much a novice in rust to attempt to fix them.

warning: unused variable: `frame`
   --> src/lib.rs:134:9
    |
134 |     for frame in rendered_frames {
    |         ^^^^^ help: consider prefixing with an underscore: `_frame`
    |
    = note: `#[warn(unused_variables)]` on by default

warning: unused variable: `writer`
  --> src/lib.rs:73:5
   |
73 |     writer: W,
   |     ^^^^^^ help: consider prefixing with an underscore: `_writer`

Ah, yes, that must have been before I had even gotten it writing the file out at all! :slight_smile:

Don't worry, I'll have it working soon.

Well, threading isn't right yet still, but it is creating a GIF now. The timing on the GIF is way slower than it should be, too. I pushed the changes to GitHub.


Threading are working now! It can rasterize and sequence at the same time. Just have to fix the messed up timing and the weird blinking issue:

1 Like

Interestingly it appears that it isn't possible to have an inconsistent frame rate inside of GIF files. I'm not sure of this, but it seems to be the problem from the symptoms. I'm calling add_frame_rgba and setting the timestamp to the proper timestamp from the recording, but despite that, it chooses to play the images at a consistent frame rate. That is why the progress bar is blinking in the image above. Those frames where the progress bar isn't there are relatively extremely short to the frames adjacent to them, but in playback they get the same amount of frame time. I guess I'm going to have to sample the frames at a consistent rate, which is probably best for performance's sake anyway, because, for example, lolcat prints each character essentially individually, but it happens so quickly that there is no reason I should be rendering an entire GIF frame for each printed character in lolcal.

I've got a work-in-progress attempt at producing the GIF with a consistent frame rate and it does indeed fix the timing issues. I simply sample the terminal screen every 0.1 seconds ( a configurable interval ) and discard all of the frames in between. Unfortunately, I think this makes certain flickering behavior unavoidable on certain applications ( which are hopefully somewhat rare in practice ). The issue is that if you are not sampling all of the frames, you have to discard some, and there is no way for me to know which frames in between to keep. That means that if something, like the progressbar in the GIF below, flashes on an off very quickly, while you would not necessarily see it in real life, you will see it in the GIF because of the limited frame rate:

The only possible way around this I can think of would be to blend the discarded frames, but I don't think that would look right either so I don't think it is worth the work.

I think that that will work fine as soon as I fix another threading issue.

Then there are some other niceties to add in priority order:

  • Support for changing the output resolution
  • Support for custom SVG templates to allow you to style the output
  • Support for rendering animated PNGs
  • Support for rendering animated SVGs

Edit: Dealt with the perceived threading issue ( just by adding an implementation note :slight_smile: ). It should be basically working now.

4 Likes

OK, I've still got to fix a timing issue, because it plays faster with a higher frame interval and slower with a lower frame interval, but it shouldn't be a big deal to fix.

1 Like

Hey everybody, I've done a little more work on this and I've replaced the SVG based renderer with a manual font-kit based renderer which is ~2x faster than the old one. I've still got to implement italics, bold, and underlining, but it is looking really nice:


Before I add font style support I'm going to try to fix the timing issues and I think I might even be able to fix the flickering.

The new idea to fix the flickering is to sample at intervals, but for each interval select the longest lasting frame displayed in the interval. We'll see if that makes progress bars look right.

The changes aren't merged to GitHub master yet, but I will merge it soon.

3 Likes

Whoa, either it was an update to gifski or I was just missing something earlier, but it looks like I may have fixed the timing issue without doing anything but multiplying the time by 100 to convert it from milis to seconds. And now flickering!

7 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.