mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
terminal/kitty-gfx: png decoding
This commit is contained in:
@ -15,7 +15,7 @@ const log = std.log.scoped(.kitty_gfx);
|
|||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// - delete
|
// - delete
|
||||||
// - zlib deflate compression
|
// - shared memory transmit
|
||||||
// (not exhaustive, almost every op is ignoring additional config)
|
// (not exhaustive, almost every op is ignoring additional config)
|
||||||
|
|
||||||
/// Execute a Kitty graphics command against the given terminal. This
|
/// Execute a Kitty graphics command against the given terminal. This
|
||||||
@ -244,7 +244,7 @@ fn loadAndAddImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dump the image data before it is decompressed
|
// Dump the image data before it is decompressed
|
||||||
// img.debugDump() catch unreachable;
|
// loading.debugDump() catch unreachable;
|
||||||
|
|
||||||
// Validate and store our image
|
// Validate and store our image
|
||||||
var img = try loading.complete(alloc);
|
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.TemporaryFileNotInTempDir => r.message = "EINVAL: temporary file not in temp dir",
|
||||||
error.UnsupportedFormat => r.message = "EINVAL: unsupported format",
|
error.UnsupportedFormat => r.message = "EINVAL: unsupported format",
|
||||||
error.UnsupportedMedium => r.message = "EINVAL: unsupported medium",
|
error.UnsupportedMedium => r.message = "EINVAL: unsupported medium",
|
||||||
|
error.UnsupportedDepth => r.message = "EINVAL: unsupported pixel depth",
|
||||||
error.DimensionsRequired => r.message = "EINVAL: dimensions required",
|
error.DimensionsRequired => r.message = "EINVAL: dimensions required",
|
||||||
error.DimensionsTooLarge => r.message = "EINVAL: dimensions too large",
|
error.DimensionsTooLarge => r.message = "EINVAL: dimensions too large",
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
|||||||
|
|
||||||
const command = @import("graphics_command.zig");
|
const command = @import("graphics_command.zig");
|
||||||
const internal_os = @import("../../os/main.zig");
|
const internal_os = @import("../../os/main.zig");
|
||||||
|
const stb = @import("../../stb/main.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.kitty_gfx);
|
const log = std.log.scoped(.kitty_gfx);
|
||||||
|
|
||||||
@ -45,11 +46,7 @@ pub const LoadingImage = struct {
|
|||||||
.width = t.width,
|
.width = t.width,
|
||||||
.height = t.height,
|
.height = t.height,
|
||||||
.compression = t.compression,
|
.compression = t.compression,
|
||||||
.format = switch (t.format) {
|
.format = t.format,
|
||||||
.rgb => .rgb,
|
|
||||||
.rgba => .rgba,
|
|
||||||
else => unreachable,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -220,17 +217,21 @@ pub const LoadingImage = struct {
|
|||||||
pub fn complete(self: *LoadingImage, alloc: Allocator) !Image {
|
pub fn complete(self: *LoadingImage, alloc: Allocator) !Image {
|
||||||
const img = &self.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.
|
// Validate our dimensions.
|
||||||
if (img.width == 0 or img.height == 0) return error.DimensionsRequired;
|
if (img.width == 0 or img.height == 0) return error.DimensionsRequired;
|
||||||
if (img.width > max_dimension or img.height > max_dimension) return error.DimensionsTooLarge;
|
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
|
// Data length must be what we expect
|
||||||
const bpp: u32 = switch (img.format) {
|
const bpp: u32 = switch (img.format) {
|
||||||
.rgb => 3,
|
.rgb => 3,
|
||||||
.rgba => 4,
|
.rgba => 4,
|
||||||
|
.png => unreachable, // png should be decoded by here
|
||||||
};
|
};
|
||||||
const expected_len = img.width * img.height * bpp;
|
const expected_len = img.width * img.height * bpp;
|
||||||
const actual_len = self.data.items.len;
|
const actual_len = self.data.items.len;
|
||||||
@ -250,6 +251,31 @@ pub const LoadingImage = struct {
|
|||||||
return result;
|
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.
|
/// Decompress the data in-place.
|
||||||
fn decompress(self: *LoadingImage, alloc: Allocator) !void {
|
fn decompress(self: *LoadingImage, alloc: Allocator) !void {
|
||||||
return switch (self.image.compression) {
|
return switch (self.image.compression) {
|
||||||
@ -282,6 +308,44 @@ pub const LoadingImage = struct {
|
|||||||
// Make sure we note that our image is no longer compressed
|
// Make sure we note that our image is no longer compressed
|
||||||
self.image.compression = .none;
|
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.
|
/// Image represents a single fully loaded image.
|
||||||
@ -290,12 +354,10 @@ pub const Image = struct {
|
|||||||
number: u32 = 0,
|
number: u32 = 0,
|
||||||
width: u32 = 0,
|
width: u32 = 0,
|
||||||
height: u32 = 0,
|
height: u32 = 0,
|
||||||
format: Format = .rgb,
|
format: command.Transmission.Format = .rgb,
|
||||||
compression: command.Transmission.Compression = .none,
|
compression: command.Transmission.Compression = .none,
|
||||||
data: []const u8 = "",
|
data: []const u8 = "",
|
||||||
|
|
||||||
pub const Format = enum { rgb, rgba };
|
|
||||||
|
|
||||||
pub const Error = error{
|
pub const Error = error{
|
||||||
InvalidData,
|
InvalidData,
|
||||||
DecompressionFailed,
|
DecompressionFailed,
|
||||||
@ -305,6 +367,7 @@ pub const Image = struct {
|
|||||||
TemporaryFileNotInTempDir,
|
TemporaryFileNotInTempDir,
|
||||||
UnsupportedFormat,
|
UnsupportedFormat,
|
||||||
UnsupportedMedium,
|
UnsupportedMedium,
|
||||||
|
UnsupportedDepth,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn deinit(self: *Image, alloc: Allocator) void {
|
pub fn deinit(self: *Image, alloc: Allocator) void {
|
||||||
@ -317,31 +380,6 @@ pub const Image = struct {
|
|||||||
copy.data = "";
|
copy.data = "";
|
||||||
return copy;
|
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.
|
/// Easy base64 encoding function.
|
||||||
@ -586,3 +624,36 @@ test "image load: rgb, not compressed, regular file" {
|
|||||||
try testing.expect(img.compression == .none);
|
try testing.expect(img.compression == .none);
|
||||||
try tmp_dir.dir.access(path, .{});
|
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, .{});
|
||||||
|
}
|
||||||
|
BIN
src/terminal/kitty/testdata/image-png-none-50x76-2147483647-raw.data
vendored
Normal file
BIN
src/terminal/kitty/testdata/image-png-none-50x76-2147483647-raw.data
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 86 B |
Reference in New Issue
Block a user