diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/kitty/graphics_pixel.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/kitty/graphics_pixel.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/kitty/graphics_pixel.zig | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/kitty/graphics_pixel.zig b/vendor/libghostty-vt/src/terminal/kitty/graphics_pixel.zig new file mode 100644 index 00000000..1f446873 --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/kitty/graphics_pixel.zig @@ -0,0 +1,235 @@ +//! Pixel-buffer operations for the Kitty graphics protocol: format +//! conversion to RGBA, background fill, and rectangle composition. +//! These are the primitives behind animation frame loading (a=f) and +//! frame composition (a=c). See graphics_animation.zig for the +//! animation model built on top of them. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const wuffs = @import("wuffs"); +const command = @import("graphics_command.zig"); + +/// Convert pixel data in the given format to a freshly allocated RGBA +/// buffer. The caller owns the result; the input is not freed. +/// +/// The input length must be a multiple of the format's bytes per +/// pixel; image loading validates data length against the image +/// dimensions before storing it. +pub fn rgbaFromFormat( + alloc: Allocator, + format: command.Transmission.Format, + data: []const u8, +) Allocator.Error![]u8 { + const result = switch (format) { + .rgba => return try alloc.dupe(u8, data), + .rgb => wuffs.swizzle.rgbToRgba(alloc, data), + .gray => wuffs.swizzle.gToRgba(alloc, data), + .gray_alpha => wuffs.swizzle.gaToRgba(alloc, data), + + // PNG is decoded to RGBA during image loading. + .png => unreachable, + }; + + return result catch |err| switch (err) { + error.OutOfMemory => error.OutOfMemory, + // These are fixed, supported swizzles; wuffs cannot fail to + // prepare them and nothing else in the conversion errors. + error.WuffsError, error.Overflow => unreachable, + }; +} + +/// Fill an RGBA buffer with the given background color. +pub fn fillBackground( + data: []u8, + bg: command.AnimationFrameLoading.Background, +) void { + const raw: u32 = @bitCast(bg); + if (raw == 0) { + @memset(data, 0); + return; + } + var i: usize = 0; + while (i + 4 <= data.len) : (i += 4) { + data[i + 0] = bg.r; + data[i + 1] = bg.g; + data[i + 2] = bg.b; + data[i + 3] = bg.a; + } +} + +/// Compose the `src` rectangle (src_width x src_height RGBA pixels) +/// onto the `dst` canvas (dst_width x dst_height RGBA pixels) with +/// its top-left corner at (x, y). Portions of the rectangle outside +/// the canvas are silently clipped, matching Kitty's a=f behavior. +pub fn composeRect( + dst: []u8, + dst_width: u32, + dst_height: u32, + src: []const u8, + src_width: u32, + src_height: u32, + x: u32, + y: u32, + mode: command.CompositionMode, +) void { + if (x >= dst_width or y >= dst_height) return; + const width: usize = @min(src_width, dst_width - x); + const height: usize = @min(src_height, dst_height - y); + for (0..height) |row| { + const dst_off = ((y + row) * dst_width + x) * 4; + const src_off = row * @as(usize, src_width) * 4; + composeRow( + dst[dst_off..][0 .. width * 4], + src[src_off..][0 .. width * 4], + mode, + ); + } +} + +/// Compose a width x height rectangle between two full-size RGBA +/// canvases sharing the same `canvas_width` stride, reading from +/// (src_x, src_y) in `src` and writing at (dst_x, dst_y) in `dst`. +/// The caller must have validated that both rectangles are within +/// bounds; a=c reports out-of-bounds rectangles as errors rather +/// than clipping. +pub fn composeCanvasRect( + dst: []u8, + src: []const u8, + canvas_width: u32, + width: u32, + height: u32, + src_x: u32, + src_y: u32, + dst_x: u32, + dst_y: u32, + mode: command.CompositionMode, +) void { + for (0..height) |row| { + const dst_off = ((dst_y + row) * @as(usize, canvas_width) + dst_x) * 4; + const src_off = ((src_y + row) * @as(usize, canvas_width) + src_x) * 4; + composeRow( + dst[dst_off..][0 .. @as(usize, width) * 4], + src[src_off..][0 .. @as(usize, width) * 4], + mode, + ); + } +} + +fn composeRow(dst: []u8, src: []const u8, mode: command.CompositionMode) void { + switch (mode) { + .overwrite => @memcpy(dst, src), + .alpha_blend => wuffs.swizzle.rgbaSrcOver(dst, src), + } +} + +test "rgba conversion" { + const testing = std.testing; + const alloc = testing.allocator; + + { + const rgb = [_]u8{ 1, 2, 3, 4, 5, 6 }; + const result = try rgbaFromFormat(alloc, .rgb, &rgb); + defer alloc.free(result); + try testing.expectEqualSlices(u8, &.{ 1, 2, 3, 255, 4, 5, 6, 255 }, result); + } + + { + const gray = [_]u8{ 7, 8 }; + const result = try rgbaFromFormat(alloc, .gray, &gray); + defer alloc.free(result); + try testing.expectEqualSlices(u8, &.{ 7, 7, 7, 255, 8, 8, 8, 255 }, result); + } + + { + const ga = [_]u8{ 7, 100, 8, 200 }; + const result = try rgbaFromFormat(alloc, .gray_alpha, &ga); + defer alloc.free(result); + try testing.expectEqualSlices(u8, &.{ 7, 7, 7, 100, 8, 8, 8, 200 }, result); + } + + { + const rgba = [_]u8{ 1, 2, 3, 4 }; + const result = try rgbaFromFormat(alloc, .rgba, &rgba); + defer alloc.free(result); + try testing.expectEqualSlices(u8, &rgba, result); + } +} + +test "fill background" { + var buf: [8]u8 = undefined; + fillBackground(&buf, .{ .r = 1, .g = 2, .b = 3, .a = 4 }); + try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3, 4, 1, 2, 3, 4 }, &buf); + + fillBackground(&buf, .{}); + try std.testing.expectEqualSlices(u8, &(.{0} ** 8), &buf); +} + +test "compose rect overwrite with clipping" { + // 2x2 canvas, compose a 2x1 rect at (1, 1): only the first pixel + // of the rect fits, the rest clips off the right edge. + var dst = [_]u8{0} ** 16; + const src = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8 }; + composeRect(&dst, 2, 2, &src, 2, 1, 1, 1, .overwrite); + + var expect = [_]u8{0} ** 16; + expect[12] = 1; + expect[13] = 2; + expect[14] = 3; + expect[15] = 4; + try std.testing.expectEqualSlices(u8, &expect, &dst); +} + +test "compose rect entirely out of bounds" { + var dst = [_]u8{9} ** 16; + const src = [_]u8{1} ** 4; + composeRect(&dst, 2, 2, &src, 1, 1, 2, 0, .overwrite); + composeRect(&dst, 2, 2, &src, 1, 1, 0, 2, .overwrite); + try std.testing.expectEqualSlices(u8, &(.{9} ** 16), &dst); +} + +test "alpha blend source-over semantics" { + const testing = std.testing; + + // Opaque source overwrites exactly. + { + var dst = [4]u8{ 10, 20, 30, 40 }; + composeRect(&dst, 1, 1, &.{ 100, 110, 120, 255 }, 1, 1, 0, 0, .alpha_blend); + try testing.expectEqualSlices(u8, &.{ 100, 110, 120, 255 }, &dst); + } + + // Fully transparent source leaves the destination untouched + // (like Kitty; see the module doc on rounding). + { + var dst = [4]u8{ 10, 20, 30, 40 }; + composeRect(&dst, 1, 1, &.{ 100, 110, 120, 0 }, 1, 1, 0, 0, .alpha_blend); + try testing.expectEqualSlices(u8, &.{ 10, 20, 30, 40 }, &dst); + } + + // 50% source over opaque destination. + { + var dst = [4]u8{ 0, 0, 0, 255 }; + composeRect(&dst, 1, 1, &.{ 255, 255, 255, 128 }, 1, 1, 0, 0, .alpha_blend); + try testing.expectEqual(@as(u8, 255), dst[3]); + try testing.expectEqual(@as(u8, 128), dst[0]); + } + + // Blending over a transparent destination yields the source + // exactly, matching Kitty: wuffs passes the source through + // untouched when the destination is transparent. + { + var dst = [4]u8{ 0, 0, 0, 0 }; + composeRect(&dst, 1, 1, &.{ 200, 100, 50, 128 }, 1, 1, 0, 0, .alpha_blend); + try testing.expectEqualSlices(u8, &.{ 200, 100, 50, 128 }, &dst); + } +} + +test "compose canvas rect" { + // 3x1 canvas: copy pixel at x=0 onto x=2. + var canvas = [_]u8{ 1, 2, 3, 4, 0, 0, 0, 0, 9, 9, 9, 9 }; + composeCanvasRect(&canvas, &canvas, 3, 1, 1, 0, 0, 2, 0, .overwrite); + try std.testing.expectEqualSlices( + u8, + &.{ 1, 2, 3, 4, 0, 0, 0, 0, 1, 2, 3, 4 }, + &canvas, + ); +} |
