From a02fa4e705991f7940b1daae1c38ad331dbb235d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Aug 2023 15:09:42 -0700 Subject: [PATCH] terminal/kitty-gfx: png decoding --- src/terminal/kitty/graphics_exec.zig | 5 +- src/terminal/kitty/graphics_image.zig | 143 +++++++++++++----- .../image-png-none-50x76-2147483647-raw.data | Bin 0 -> 86 bytes 3 files changed, 110 insertions(+), 38 deletions(-) create mode 100644 src/terminal/kitty/testdata/image-png-none-50x76-2147483647-raw.data diff --git a/src/terminal/kitty/graphics_exec.zig b/src/terminal/kitty/graphics_exec.zig index c2e861d87..8af16a8a2 100644 --- a/src/terminal/kitty/graphics_exec.zig +++ b/src/terminal/kitty/graphics_exec.zig @@ -15,7 +15,7 @@ const log = std.log.scoped(.kitty_gfx); // TODO: // - delete -// - zlib deflate compression +// - shared memory transmit // (not exhaustive, almost every op is ignoring additional config) /// Execute a Kitty graphics command against the given terminal. This @@ -244,7 +244,7 @@ fn loadAndAddImage( } // Dump the image data before it is decompressed - // img.debugDump() catch unreachable; + // loading.debugDump() catch unreachable; // Validate and store our image var img = try loading.complete(alloc); @@ -270,6 +270,7 @@ fn encodeError(r: *Response, err: EncodeableError) void { error.TemporaryFileNotInTempDir => r.message = "EINVAL: temporary file not in temp dir", error.UnsupportedFormat => r.message = "EINVAL: unsupported format", error.UnsupportedMedium => r.message = "EINVAL: unsupported medium", + error.UnsupportedDepth => r.message = "EINVAL: unsupported pixel depth", error.DimensionsRequired => r.message = "EINVAL: dimensions required", error.DimensionsTooLarge => r.message = "EINVAL: dimensions too large", } diff --git a/src/terminal/kitty/graphics_image.zig b/src/terminal/kitty/graphics_image.zig index 89c01d3ae..77b46264c 100644 --- a/src/terminal/kitty/graphics_image.zig +++ b/src/terminal/kitty/graphics_image.zig @@ -6,6 +6,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const command = @import("graphics_command.zig"); const internal_os = @import("../../os/main.zig"); +const stb = @import("../../stb/main.zig"); const log = std.log.scoped(.kitty_gfx); @@ -45,11 +46,7 @@ pub const LoadingImage = struct { .width = t.width, .height = t.height, .compression = t.compression, - .format = switch (t.format) { - .rgb => .rgb, - .rgba => .rgba, - else => unreachable, - }, + .format = t.format, }, }; @@ -220,17 +217,21 @@ pub const LoadingImage = struct { pub fn complete(self: *LoadingImage, alloc: Allocator) !Image { const img = &self.image; + // Decompress the data if it is compressed. + try self.decompress(alloc); + + // Decode the png if we have to + if (img.format == .png) try self.decodePng(alloc); + // Validate our dimensions. if (img.width == 0 or img.height == 0) return error.DimensionsRequired; if (img.width > max_dimension or img.height > max_dimension) return error.DimensionsTooLarge; - // Decompress the data if it is compressed. - try self.decompress(alloc); - // Data length must be what we expect const bpp: u32 = switch (img.format) { .rgb => 3, .rgba => 4, + .png => unreachable, // png should be decoded by here }; const expected_len = img.width * img.height * bpp; const actual_len = self.data.items.len; @@ -250,6 +251,31 @@ pub const LoadingImage = struct { return result; } + /// Debug function to write the data to a file. This is useful for + /// capturing some test data for unit tests. + pub fn debugDump(self: LoadingImage) !void { + if (comptime builtin.mode != .Debug) @compileError("debugDump in non-debug"); + + var buf: [1024]u8 = undefined; + const filename = try std.fmt.bufPrint( + &buf, + "image-{s}-{s}-{d}x{d}-{}.data", + .{ + @tagName(self.image.format), + @tagName(self.image.compression), + self.image.width, + self.image.height, + self.image.id, + }, + ); + const cwd = std.fs.cwd(); + const f = try cwd.createFile(filename, .{}); + defer f.close(); + + const writer = f.writer(); + try writer.writeAll(self.data.items); + } + /// Decompress the data in-place. fn decompress(self: *LoadingImage, alloc: Allocator) !void { return switch (self.image.compression) { @@ -282,6 +308,44 @@ pub const LoadingImage = struct { // Make sure we note that our image is no longer compressed self.image.compression = .none; } + + /// Decode the data as PNG. This will also updated the image dimensions. + fn decodePng(self: *LoadingImage, alloc: Allocator) !void { + assert(self.image.format == .png); + + // Decode PNG + var width: c_int = 0; + var height: c_int = 0; + var bpp: c_int = 0; + const data = stb.stbi_load_from_memory( + self.data.items.ptr, + @intCast(self.data.items.len), + &width, + &height, + &bpp, + 0, + ) orelse return error.InvalidData; + defer stb.stbi_image_free(data); + const len: usize = @intCast(width * height * bpp); + + // Validate our bpp + if (bpp != 3 and bpp != 4) return error.UnsupportedDepth; + + // Replace our data + self.data.deinit(alloc); + self.data = .{}; + try self.data.ensureUnusedCapacity(alloc, len); + try self.data.appendSlice(alloc, data[0..len]); + + // Store updated image dimensions + self.image.width = @intCast(width); + self.image.height = @intCast(height); + self.image.format = switch (bpp) { + 3 => .rgb, + 4 => .rgba, + else => unreachable, // validated above + }; + } }; /// Image represents a single fully loaded image. @@ -290,12 +354,10 @@ pub const Image = struct { number: u32 = 0, width: u32 = 0, height: u32 = 0, - format: Format = .rgb, + format: command.Transmission.Format = .rgb, compression: command.Transmission.Compression = .none, data: []const u8 = "", - pub const Format = enum { rgb, rgba }; - pub const Error = error{ InvalidData, DecompressionFailed, @@ -305,6 +367,7 @@ pub const Image = struct { TemporaryFileNotInTempDir, UnsupportedFormat, UnsupportedMedium, + UnsupportedDepth, }; pub fn deinit(self: *Image, alloc: Allocator) void { @@ -317,31 +380,6 @@ pub const Image = struct { copy.data = ""; return copy; } - - /// Debug function to write the data to a file. This is useful for - /// capturing some test data for unit tests. - pub fn debugDump(self: Image) !void { - if (comptime builtin.mode != .Debug) @compileError("debugDump in non-debug"); - - var buf: [1024]u8 = undefined; - const filename = try std.fmt.bufPrint( - &buf, - "image-{s}-{s}-{d}x{d}-{}.data", - .{ - @tagName(self.format), - @tagName(self.compression), - self.width, - self.height, - self.id, - }, - ); - const cwd = std.fs.cwd(); - const f = try cwd.createFile(filename, .{}); - defer f.close(); - - const writer = f.writer(); - try writer.writeAll(self.data); - } }; /// Easy base64 encoding function. @@ -586,3 +624,36 @@ test "image load: rgb, not compressed, regular file" { try testing.expect(img.compression == .none); try tmp_dir.dir.access(path, .{}); } + +test "image load: png, not compressed, regular file" { + const testing = std.testing; + const alloc = testing.allocator; + + var tmp_dir = try internal_os.TempDir.init(); + defer tmp_dir.deinit(); + const data = @embedFile("testdata/image-png-none-50x76-2147483647-raw.data"); + try tmp_dir.dir.writeFile("image.data", data); + + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const path = try tmp_dir.dir.realpath("image.data", &buf); + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .png, + .medium = .file, + .compression = .none, + .width = 0, + .height = 0, + .image_id = 31, + } }, + .data = try testB64(alloc, path), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd); + defer loading.deinit(alloc); + var img = try loading.complete(alloc); + defer img.deinit(alloc); + try testing.expect(img.compression == .none); + try testing.expect(img.format == .rgb); + try tmp_dir.dir.access(path, .{}); +} diff --git a/src/terminal/kitty/testdata/image-png-none-50x76-2147483647-raw.data b/src/terminal/kitty/testdata/image-png-none-50x76-2147483647-raw.data new file mode 100644 index 0000000000000000000000000000000000000000..032cb07c722cfd7ee5dd701e3a7407ddcaafc565 GIT binary patch literal 86 zcmeAS@N?(olHy`uVBq!ia0y~yU@&4}VDMpNW?*0ty;