// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Decoding sixel data that is already in memory. const std = @import("std"); const Allocator = std.mem.Allocator; const Canvas = @import("Canvas.zig"); const Image = @import("Image.zig"); const Parser = @import("Parser.zig"); const introducer = @import("introducer.zig"); const palette = @import("palette.zig"); pub const Options = struct { limits: Canvas.Limits = .{}, /// See `Parser.Options.registers`. registers: u16 = palette.max_registers, }; pub const Error = Allocator.Error || error{ /// The data starts with ESC or `0x90` but not with a sixel introducer. InvalidIntroducer, /// The data draws nothing and gives no size, so there is no image to /// return: z2d has no surface with no pixels. EmptyImage, }; /// Decodes one sixel image from `bytes` into an image the caller owns. /// /// `bytes` may be a whole control string, introducer and all, /// /// ESC P 0;1;0 q "1;1;64;64 #0;2;0;0;0 ... ESC \ /// /// in which case the introducer's parameters apply, or just the data that /// would come after the `q`, in which case the defaults do. Decoding stops at /// the first string terminator, abort, or escape sequence, and the /// termination of the result says which it was; data with none of those ends /// with the slice, as `end_of_data`. Anything after the end is ignored. Both /// the 7-bit and the 8-bit forms of DCS and ST are recognized here, since a /// slice handed over as sixel data is not UTF-8 text. pub fn decode(gpa: Allocator, bytes: []const u8, options: Options) Error!Image { var params: introducer.Params = .{}; var body = bytes; if (bytes.len > 0 and introducer.isStart(bytes[0], true)) { switch (introducer.match(bytes, true)) { .sixel => |m| { params = m.params; body = bytes[m.len..]; }, .not_sixel, .need_more => return error.InvalidIntroducer, } } var parser: Parser = .init(gpa, params, .{ .limits = options.limits, .registers = options.registers, .c1 = true, }); defer parser.deinit(); const n = try parser.feed(body); const termination: Image.Termination = if (n == body.len) .end_of_data else switch (body[n]) { introducer.st_c1 => .st, introducer.esc => if (n + 1 < body.len and body[n + 1] == '\\') .st else .interrupted, 0x18, 0x1a => .aborted, else => .interrupted, }; return (try parser.finish(termination)) orelse error.EmptyImage; } const testing = std.testing; test "a whole control string" { var img = try decode(testing.allocator, "\x1bP0;1;0q\"1;1;2;2#1;2;100;0;0~~\x1b\\", .{}); defer img.deinit(testing.allocator); try testing.expectEqual(Image.Termination.st, img.termination); try testing.expect(img.transparent); try testing.expectEqual(@as(u32, 2), img.width()); try testing.expectEqual(@as(u32, 6), img.height()); } test "just the data, and the 8-bit forms" { var a = try decode(testing.allocator, "#1~", .{}); defer a.deinit(testing.allocator); try testing.expectEqual(Image.Termination.end_of_data, a.termination); try testing.expect(!a.transparent); var b = try decode(testing.allocator, "\x90q~\x9c", .{}); defer b.deinit(testing.allocator); try testing.expectEqual(Image.Termination.st, b.termination); } test "how the data ended" { var a = try decode(testing.allocator, "~\x18~~", .{}); defer a.deinit(testing.allocator); try testing.expectEqual(Image.Termination.aborted, a.termination); try testing.expectEqual(@as(u32, 1), a.width()); var b = try decode(testing.allocator, "~\x1b[0m", .{}); defer b.deinit(testing.allocator); try testing.expectEqual(Image.Termination.interrupted, b.termination); var c = try decode(testing.allocator, "~\x1b", .{}); defer c.deinit(testing.allocator); try testing.expectEqual(Image.Termination.interrupted, c.termination); } test "errors" { try testing.expectError(error.InvalidIntroducer, decode(testing.allocator, "\x1bP$q", .{})); try testing.expectError(error.InvalidIntroducer, decode(testing.allocator, "\x1bP0;1", .{})); try testing.expectError(error.EmptyImage, decode(testing.allocator, "\x1bPq\x1b\\", .{})); try testing.expectError(error.EmptyImage, decode(testing.allocator, "", .{})); }