summaryrefslogtreecommitdiff
path: root/src/pdf.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pdf.zig')
-rw-r--r--src/pdf.zig156
1 files changed, 156 insertions, 0 deletions
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());