# zig-sixel A sans-I/O Zig library for reading and writing DEC sixel graphics as [z2d](https://github.com/vancluever/z2d) surfaces. API documentation, generated from the doc comments, is at . Sixel is the bitmap format of DEC's VT240 and VT340 terminals and their printers. An image travels as printable characters inside a device control string, and each character draws six pixels in a column. xterm, foot, WezTerm, mlterm, Windows Terminal and others still display it. The library has three entry points: - **`relay`** copies a `std.Io.Reader` to a `std.Io.Writer`. When it meets a sixel image it decodes the image and hands it to your handler as a z2d surface instead of writing it. Everything else passes through untouched. - **`decode`** turns sixel data already in memory into a surface. - **`encode`**, **`encodeAlloc`** and **`encodeBuffer`** write a surface as sixel to a writer, to an allocated slice, or into a buffer you supply. The library never opens, reads or writes anything itself, and takes no `Io`. Under both read paths is `Parser`, a state machine you feed bytes in pieces of any size. Requires Zig 0.17.0. ## Where this lives The repository is in three places, all with the same history: - **Forgejo:** , the web-visible one. ```console git clone https://git.jcollie.dev/jeff/zig-sixel.git ``` - **Tangled:** a mirror at . - **Radicle:** repository ID `rad:z3xgR2D5mHrWsCazcxsz5W7ta3fyg`. This fetches it from any node that seeds it: ```console rad clone rad:z3xgR2D5mHrWsCazcxsz5W7ta3fyg ``` Any one of them is the whole project. ## Using it ```console zig fetch --save git+https://git.jcollie.dev/jeff/zig-sixel.git ``` ```zig const sixel = b.dependency("sixel", .{ .target = target, .optimize = optimize }); exe.root_module.addImport("sixel", sixel.module("sixel")); ``` ### Picking images out of a stream ```zig const Handler = struct { gpa: std.mem.Allocator, pub fn image(self: *Handler, img: sixel.Image) !void { var owned = img; defer owned.deinit(self.gpa); // owned.surface is a z2d.Surface of owned.width() x owned.height() } }; var handler: Handler = .{ .gpa = gpa }; try sixel.relay(gpa, &stdin.interface, &stdout.interface, &handler, .{}); ``` `relay` keeps latency low. It writes text as soon as it has read it and flushes the writer before every read that might block. It only holds back an escape sequence that has started to arrive but isn't yet long enough to tell whether it starts an image. An image starts at `ESC P`, then optional digits and semicolons, then `q`. Other device control strings pass through untouched: DECRQSS (`ESC P $ q`), XTGETTCAP (`ESC P + q`) and tmux passthrough are all left alone. An image ends in one of four ways, reported in `Image.termination`: | Termination | How the image ended | What happens to the terminator | | --- | --- | --- | | `.st` | ST (`ESC \`) | Consumed | | `.aborted` | CAN or SUB | Consumed, since they abandon the string | | `.interrupted` | Any other escape sequence | Passed through | | `.end_of_data` | The input ran out | Nothing to pass through | The 8-bit C1 forms of DCS and ST (`0x90` and `0x9C`) are recognized only with `.c1 = true`, because in UTF-8 text those bytes are continuation bytes. ### Decoding a slice ```zig var img = try sixel.decode(gpa, bytes, .{}); defer img.deinit(gpa); ``` The slice can be a whole control string or just the data after the `q`. ### Encoding ```zig try sixel.encode(gpa, writer, &surface, .{}); const bytes = try sixel.encodeAlloc(gpa, &surface, .{}); const used = try sixel.encodeBuffer(gpa, &buffer, &surface, .{}); ``` - **Colors.** An image with at most `max_colors` colors (default 256) is written with exactly those colors. Each is rounded to whole percentages, because that is all sixel can express. An image with more colors is reduced by median cut and dithered with Floyd–Steinberg (`.dither = .none` turns dithering off). Output is deterministic. - **Transparency.** Pixels less opaque than `alpha_threshold` are not drawn. When there are any, the introducer asks for `P2 = 1`, so they stay transparent. ## Behavior worth knowing - **Limits.** `Limits` caps the width, the height and the total pixel count of the canvas a decoder will allocate. Anything drawn outside them is dropped, as a terminal drops what falls off its screen, and the image is marked `clipped`. No input can make the decoder panic or allocate without bound. - **Colors are applied when a pixel is drawn.** A pixel takes the color its register holds at that moment, as xterm does. Redefining a register later doesn't recolor what was already drawn. This is how `img2sixel -I` fits more colors than registers into one image. - **Background.** With `P2 = 1`, pixels that are never drawn are transparent. Otherwise they take the color of register 0 at the end of the image. - **Aspect ratio.** The pixel aspect ratio from `P1` or the raster attributes is reported in `Image.aspect` but not applied: pixels stay square, as in modern terminals. - **Default registers.** Registers 0–15 start with the VT340's colors. Registers 16–255 follow libsixel: a 6×6×6 color cube, then a gray ramp. Before any register is chosen, register 15 is selected, as in libsixel. - **HLS colors.** These use DEC's hue circle, which puts blue at 0°, red at 120° and green at 240°. ## libsixel as the oracle [libsixel](https://saitoha.github.io/libsixel/) is the reference both directions are tested against: - **Decoder.** The fixtures in `tests/fixtures` are sixel files, some written by `img2sixel` in its several modes and some written by hand from the documentation. Each is stored beside the pixels `sixel2png` decodes it to, and the decoder must match those pixels exactly. - **Encoder.** `zig build oracle` writes the test pictures with this encoder, has `sixel2png` read them back, and compares the result with the original pictures. The two decoders differ deliberately in a few places, and the tests document each one: - **Late register colors.** libsixel colors pixels with the final contents of each register, so it misreads its own `img2sixel -I` output. - **HLS above 50% lightness.** libsixel's HLS conversion is wrong there. - **`P2`.** libsixel ignores it. ## Development Everything is in the Nix devshell: Zig, libsixel, Pillow, reuse, kcov and zon2nix. ```console nix develop zig build test # unit, integration, fixture and round-trip tests zig build test --fuzz # fuzz the decoder, the relay and the encoder zig build test --fuzz=1M # a bounded fuzzing run zig build oracle # check the encoder's output with libsixel zig build coverage # line coverage of src/ with kcov, in zig-out/coverage zig build docs-serve # read the API documentation locally zig build example < file # relay stdin, describing each image found python3 tools/oracle.py generate tests/fixtures # regenerate the fixtures nix build .#zig-sixel # build and test the package ``` After changing `build.zig.zon`, regenerate `build.zig.zon.nix` with: ```console nix develop -c zon2nix --17 --nix=build.zig.zon.nix build.zig.zon ``` ## License MIT. The project follows the [REUSE](https://reuse.software/) specification. ## References cited - Chiesa, Chris F. "All About SIXELs." DECUS VAX SIG Symposium Tape VAX90B1, 29 September 1990. . - Digital Equipment Corporation. _VT300/VT340 Programmer Reference Manual, Volume 2: Graphics Programming._ 2nd ed., EK-VT3XX-GP-002, May 1988. Chapter 14, "Sixel Graphics." - Heckbert, Paul. "Color Image Quantization for Frame Buffer Display." _Computer Graphics_ (SIGGRAPH '82) 16, no. 3 (1982): 297–307. - Saito, Hayaki. _libsixel._ Version 1.10.5. . - Shuford, Richard S., ed. "Sixel Graphics News." Shuford's terminal archive. .