//! ExtGState - Extended Graphics State for PDF //! //! Provides transparency (alpha/opacity) support through Extended Graphics State objects. //! PDF uses ExtGState dictionaries to define opacity values that can be referenced //! in content streams. //! //! Reference: PDF 1.4 Spec, Section 4.3.4 "Graphics State Parameter Dictionaries" const std = @import("std"); /// Extended Graphics State definition. /// Used for transparency and other advanced graphics state parameters. pub const ExtGState = struct { /// Fill opacity (0.0 = transparent, 1.0 = opaque) fill_opacity: f32 = 1.0, /// Stroke opacity (0.0 = transparent, 1.0 = opaque) stroke_opacity: f32 = 1.0, const Self = @This(); /// Creates an ExtGState with the specified opacities. pub fn init(fill_opacity: f32, stroke_opacity: f32) Self { return .{ .fill_opacity = std.math.clamp(fill_opacity, 0.0, 1.0), .stroke_opacity = std.math.clamp(stroke_opacity, 0.0, 1.0), }; } /// Creates an ExtGState with uniform opacity for both fill and stroke. pub fn uniform(opacity: f32) Self { const clamped = std.math.clamp(opacity, 0.0, 1.0); return .{ .fill_opacity = clamped, .stroke_opacity = clamped, }; } /// Checks if this state has any transparency (opacity < 1.0). pub fn hasTransparency(self: *const Self) bool { return self.fill_opacity < 1.0 or self.stroke_opacity < 1.0; } /// Checks if this state is equal to another. pub fn eql(self: *const Self, other: *const Self) bool { return self.fill_opacity == other.fill_opacity and self.stroke_opacity == other.stroke_opacity; } /// Generates a unique name for this state based on opacity values. /// Format: "GSa{fill}s{stroke}" where values are 0-100. pub fn getName(self: *const Self, buf: []u8) []const u8 { const fill_pct: u32 = @intFromFloat(self.fill_opacity * 100); const stroke_pct: u32 = @intFromFloat(self.stroke_opacity * 100); return std.fmt.bufPrint(buf, "GSa{d}s{d}", .{ fill_pct, stroke_pct }) catch "GS"; } /// Generates the PDF dictionary content for this state. pub fn writePdfDict(self: *const Self, writer: anytype) !void { try writer.writeAll("<< /Type /ExtGState "); try writer.print("/ca {d:.3} ", .{self.fill_opacity}); // Non-stroking (fill) alpha try writer.print("/CA {d:.3} ", .{self.stroke_opacity}); // Stroking alpha try writer.writeAll(">>"); } }; /// Registry for Extended Graphics States used in a document. /// Maintains a unique list of states to avoid duplicates. pub const ExtGStateRegistry = struct { states: std.ArrayListUnmanaged(ExtGState), allocator: std.mem.Allocator, const Self = @This(); pub fn init(allocator: std.mem.Allocator) Self { return .{ .states = .{}, .allocator = allocator, }; } pub fn deinit(self: *Self) void { self.states.deinit(self.allocator); } /// Registers an ExtGState and returns its index. /// If an equivalent state already exists, returns its index without adding a duplicate. pub fn register(self: *Self, state: ExtGState) !usize { // Check for existing equivalent state for (self.states.items, 0..) |existing, i| { if (existing.eql(&state)) { return i; } } // Add new state try self.states.append(self.allocator, state); return self.states.items.len - 1; } /// Returns all registered states. pub fn getStates(self: *const Self) []const ExtGState { return self.states.items; } /// Returns the number of registered states. pub fn count(self: *const Self) usize { return self.states.items.len; } }; // ============================================================================= // Tests // ============================================================================= test "ExtGState init" { const state = ExtGState.init(0.5, 0.75); try std.testing.expectApproxEqAbs(@as(f32, 0.5), state.fill_opacity, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 0.75), state.stroke_opacity, 0.001); } test "ExtGState clamping" { const state = ExtGState.init(-0.5, 1.5); try std.testing.expectApproxEqAbs(@as(f32, 0.0), state.fill_opacity, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 1.0), state.stroke_opacity, 0.001); } test "ExtGState hasTransparency" { const fully_opaque = ExtGState.init(1.0, 1.0); try std.testing.expect(!fully_opaque.hasTransparency()); const semi = ExtGState.init(0.5, 1.0); try std.testing.expect(semi.hasTransparency()); } test "ExtGStateRegistry deduplication" { const allocator = std.testing.allocator; var registry = ExtGStateRegistry.init(allocator); defer registry.deinit(); const idx1 = try registry.register(ExtGState.init(0.5, 0.5)); const idx2 = try registry.register(ExtGState.init(0.5, 0.5)); // Same const idx3 = try registry.register(ExtGState.init(0.75, 0.75)); // Different try std.testing.expectEqual(@as(usize, 0), idx1); try std.testing.expectEqual(@as(usize, 0), idx2); // Should be same index try std.testing.expectEqual(@as(usize, 1), idx3); // New index try std.testing.expectEqual(@as(usize, 2), registry.count()); }