1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
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,
);
}
|