Something went wrong. Try again.
A sans-I/O Zig library that reads and writes DEC sixel graphics as z2d surfaces.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Recognizing the device control string that starts a sixel image.//!//! The introducer is//!//! DCS P1 ; P2 ; P3 q//!//! where DCS is either the 8-bit C1 control `0x90` or the 7-bit `ESC P`, the//! three parameters are optional decimal numbers, and `q` is the final//! character that says the string is sixel data. Many other things are DCS//! too: DECRQSS is `ESC P $ q`, XTGETTCAP is `ESC P + q`, tmux wraps whole//! sequences in `ESC P tmux;`. What makes a DCS a sixel is that nothing but//! digits and semicolons comes between the `P` and the `q`; anything else//! means it is somebody else's and should be left alone.
const std = @import("std");
pub const esc = 0x1b;/// The 8-bit form of DCS.pub const dcs_c1 = 0x90;/// The 8-bit form of ST.pub const st_c1 = 0x9c;
/// The longest introducer this recognizes. Real ones are at most a dozen/// bytes; anything longer is treated as not being sixel, which bounds how far/// anything ever has to look ahead to decide.pub const max_len = 64;
/// The pixel aspect ratio, vertical to horizontal: a pixel `numerator` units/// tall for every `denominator` wide.////// Modern terminals draw sixel pixels square whatever this says, and so does/// this library: it is reported, not applied.pub const Aspect = struct { numerator: u32 = 2, denominator: u32 = 1,};
/// What the introducer's three parameters say.pub const Params = struct { /// From `P1`, the "macro parameter". Raster attributes in the data /// override it. aspect: Aspect = .{}, /// From `P2`: whether pixels the data never draws stay as they were /// (`1`) or are filled with the background color (`0` or `2`). A decoder /// with nothing underneath the image makes the first transparent. transparent: bool = false, /// `P3`, the horizontal grid size, which every terminal ignores. grid: u32 = 0,
pub fn fromP1(p1: ?u32) Aspect { // The table in the VT330/VT340 Programmer Reference Manual, Volume 2, // chapter 14. Values outside it are taken as the default. const n: u32 = switch (p1 orelse 0) { 2 => 5, 3, 4 => 3, 7, 8, 9 => 1, else => 2, }; return .{ .numerator = n, .denominator = 1 }; }};
pub const Match = union(enum) { /// A sixel introducer, `len` bytes long. sixel: struct { len: usize, params: Params }, /// Something else: an escape sequence, or a DCS that is not sixel. not_sixel, /// The bytes so far could still be either; more are needed to decide. need_more,};
/// Decides whether `bytes`, which start with ESC or `0x90`, begin a sixel/// introducer. `0x90` counts only when `c1` is set, since in UTF-8 text that/// byte is a continuation byte and not a control.pub fn match(bytes: []const u8, c1: bool) Match { std.debug.assert(bytes.len > 0); var i: usize = switch (bytes[0]) { esc => blk: { if (bytes.len < 2) return .need_more; if (bytes[1] != 'P') return .not_sixel; break :blk 2; }, dcs_c1 => if (c1) 1 else return .not_sixel, else => return .not_sixel, };
var values: [3]?u32 = .{ null, null, null }; var n: usize = 0; while (i < bytes.len) : (i += 1) { if (i >= max_len) return .not_sixel; switch (bytes[i]) { '0'...'9' => |d| if (n < values.len) { values[n] = (values[n] orelse 0) *| 10 +| (d - '0'); }, ';' => n +|= 1, 'q' => return .{ .sixel = .{ .len = i + 1, .params = .{ .aspect = Params.fromP1(values[0]), .transparent = (values[1] orelse 0) == 1, .grid = values[2] orelse 0, } } }, else => return .not_sixel, } } return if (i >= max_len) .not_sixel else .need_more;}
/// Whether `b` could start an introducer: where to stop when scanning text.pub fn isStart(b: u8, c1: bool) bool { return b == esc or (c1 and b == dcs_c1);}
const testing = std.testing;
fn expectSixel(bytes: []const u8, c1: bool, len: usize, params: Params) !void { const m = match(bytes, c1); try testing.expect(m == .sixel); try testing.expectEqual(len, m.sixel.len); try testing.expectEqual(params, m.sixel.params);}
test "sixel introducers" { try expectSixel("\x1bPq", false, 3, .{}); try expectSixel("\x1bPq#0", false, 3, .{}); try expectSixel("\x1bP0;1;0q", false, 8, .{ .transparent = true }); try expectSixel("\x1bP9;2q", false, 6, .{ .aspect = .{ .numerator = 1 } }); try expectSixel("\x1bP2q", false, 4, .{ .aspect = .{ .numerator = 5 } }); try expectSixel("\x1bP;1q", false, 5, .{ .transparent = true }); try expectSixel("\x1bP0;0;8q", false, 8, .{ .grid = 8 }); try expectSixel("\x90q", true, 2, .{}); // Numbers too large to mean anything saturate rather than overflow. try expectSixel("\x1bP99999999999999999999;1q", false, 25, .{ .transparent = true }); // A fourth parameter is not in the format, but costs nothing to allow. try expectSixel("\x1bP0;1;0;7q", false, 10, .{ .transparent = true });}
test "other sequences are not sixel" { try testing.expectEqual(Match.not_sixel, match("\x1b[0m", false)); try testing.expectEqual(Match.not_sixel, match("\x1b\\", false)); try testing.expectEqual(Match.not_sixel, match("\x1bP$q\"p\x1b\\", false)); try testing.expectEqual(Match.not_sixel, match("\x1bP+q544e\x1b\\", false)); try testing.expectEqual(Match.not_sixel, match("\x1bPtmux;\x1b\x1bPq", false)); try testing.expectEqual(Match.not_sixel, match("\x1bP1$r", false)); try testing.expectEqual(Match.not_sixel, match("\x90q", false)); const ones: [70]u8 = @splat('1'); try testing.expectEqual(Match.not_sixel, match("\x1bP" ++ ones ++ "q", false));}
test "a prefix needs more" { try testing.expectEqual(Match.need_more, match("\x1b", false)); try testing.expectEqual(Match.need_more, match("\x1bP", false)); try testing.expectEqual(Match.need_more, match("\x1bP0;1", false)); try testing.expectEqual(Match.need_more, match("\x90", true));}