From c86d3bb210822b3128a0759533f1bec3654364b7 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 3 Aug 2026 00:14:45 -0300 Subject: optimize PDF tint pixel transforms --- src/pdf.zig | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/pdf.zig b/src/pdf.zig index 0895469a..305bb89d 100644 --- a/src/pdf.zig +++ b/src/pdf.zig @@ -94,6 +94,43 @@ pub const TintColors = struct { foreground: [3]u8, }; +fn tintTargets(colors: TintColors) [256][3]u8 { + var targets: [256][3]u8 = undefined; + for (&targets, 0..) |*target, luminance| { + inline for (0..3) |channel| { + target[channel] = @intCast( + (@as(u32, colors.foreground[channel]) * @as(u32, @intCast(255 - luminance)) + + @as(u32, colors.background[channel]) * @as(u32, @intCast(luminance)) + 127) / 255, + ); + } + } + return targets; +} + +const tint_chroma_residuals: [511]i16 = table: { + var values: [511]i16 = undefined; + for (&values, 0..) |*value, index| { + const residual: i16 = @as(i16, @intCast(index)) - 255; + value.* = @divTrunc(residual * 3, 4); + } + break :table values; +}; + +const tint_clamped_sums: [638]u8 = table: { + @setEvalBranchQuota(2000); + var values: [638]u8 = undefined; + for (&values, 0..) |*value, index| { + const sum: i16 = @as(i16, @intCast(index)) - 191; + value.* = @intCast(std.math.clamp(sum, 0, 255)); + } + break :table values; +}; + +inline fn filteredTintChannel(target: u8, source: u8, residual_base: i16) u8 { + const chroma = tint_chroma_residuals[@intCast(residual_base + source)]; + return tint_clamped_sums[@intCast(@as(i16, target) + chroma + 191)]; +} + /// Rewrite packed straight-alpha RGBA in place without allocating. /// /// Source luminance is Rec. 601's 77/150/29 integer approximation. `full` @@ -105,6 +142,46 @@ pub const TintColors = struct { pub fn tintRgba(rgba: []u8, mode: TintMode, colors: TintColors) !void { if (rgba.len % 4 != 0) return error.InvalidRgbaLength; if (mode == .disabled) return; + + const targets = tintTargets(colors); + const pixels = std.mem.bytesAsSlice([4]u8, rgba); + switch (mode) { + .disabled => unreachable, + .full => { + for (pixels) |*pixel| { + const red = pixel[0]; + const green = pixel[1]; + const blue = pixel[2]; + const luminance: u8 = @intCast((@as(u32, red) * 77 + + @as(u32, green) * 150 + + @as(u32, blue) * 29 + 128) >> 8); + const target = targets[luminance]; + pixel[0] = target[0]; + pixel[1] = target[1]; + pixel[2] = target[2]; + } + }, + .filtered => { + for (pixels) |*pixel| { + const red = pixel[0]; + const green = pixel[1]; + const blue = pixel[2]; + const luminance: u8 = @intCast((@as(u32, red) * 77 + + @as(u32, green) * 150 + + @as(u32, blue) * 29 + 128) >> 8); + const target = targets[luminance]; + const residual_base: i16 = 255 - @as(i16, luminance); + pixel[0] = filteredTintChannel(target[0], red, residual_base); + pixel[1] = filteredTintChannel(target[1], green, residual_base); + pixel[2] = filteredTintChannel(target[2], blue, residual_base); + } + }, + } +} + +fn tintRgbaReference(rgba: []u8, mode: TintMode, colors: TintColors) !void { + if (rgba.len % 4 != 0) return error.InvalidRgbaLength; + if (mode == .disabled) return; var at: usize = 0; while (at < rgba.len) : (at += 4) { const source = rgba[at..][0..3]; @@ -126,6 +203,85 @@ pub fn tintRgba(rgba: []u8, mode: TintMode, colors: TintColors) !void { } } +fn tintTestByte(state: *u64) u8 { + state.* = state.* *% 6364136223846793005 +% 1442695040888963407; + return @truncate(state.* >> 32); +} + +test "PDF tint lookup tables are exact over every signed chroma residual" { + for (tint_chroma_residuals, 0..) |actual, index| { + const residual: i16 = @as(i16, @intCast(index)) - 255; + try std.testing.expectEqual(@divTrunc(residual * 3, 4), actual); + } + for (tint_clamped_sums, 0..) |actual, index| { + const sum: i16 = @as(i16, @intCast(index)) - 191; + try std.testing.expectEqual( + @as(u8, @intCast(std.math.clamp(sum, 0, 255))), + actual, + ); + } +} + +test "optimized PDF tint matches scalar rule for every endpoint combination" { + const modes = [_]TintMode{ .full, .filtered }; + for (0..8) |background_mask| { + for (0..8) |foreground_mask| { + const colors: TintColors = .{ + .background = .{ + if (background_mask & 1 == 0) 0 else 255, + if (background_mask & 2 == 0) 0 else 255, + if (background_mask & 4 == 0) 0 else 255, + }, + .foreground = .{ + if (foreground_mask & 1 == 0) 0 else 255, + if (foreground_mask & 2 == 0) 0 else 255, + if (foreground_mask & 4 == 0) 0 else 255, + }, + }; + var source: [8 * 2 * 4]u8 = undefined; + var at: usize = 0; + for (0..8) |source_mask| { + for ([_]u8{ 0, 255 }) |alpha| { + source[at..][0..4].* = .{ + if (source_mask & 1 == 0) 0 else 255, + if (source_mask & 2 == 0) 0 else 255, + if (source_mask & 4 == 0) 0 else 255, + alpha, + }; + at += 4; + } + } + for (modes) |mode| { + var expected = source; + var actual = source; + try tintRgbaReference(&expected, mode, colors); + try tintRgba(&actual, mode, colors); + try std.testing.expectEqualSlices(u8, &expected, &actual); + } + } + } +} + +test "optimized PDF tint matches scalar rule across deterministic broad samples" { + const modes = [_]TintMode{ .full, .filtered }; + var state: u64 = 0x7061_7264_6573_5449; + for (0..64) |_| { + const colors: TintColors = .{ + .background = .{ tintTestByte(&state), tintTestByte(&state), tintTestByte(&state) }, + .foreground = .{ tintTestByte(&state), tintTestByte(&state), tintTestByte(&state) }, + }; + var source: [512 * 4]u8 = undefined; + for (&source) |*byte| byte.* = tintTestByte(&state); + for (modes) |mode| { + var expected = source; + var actual = source; + try tintRgbaReference(&expected, mode, colors); + try tintRgba(&actual, mode, colors); + try std.testing.expectEqualSlices(u8, &expected, &actual); + } + } +} + test "PDF tint mode cycle is exact" { try std.testing.expectEqual(TintMode.filtered, TintMode.disabled.next()); try std.testing.expectEqual(TintMode.full, TintMode.filtered.next()); -- cgit v1.3