Major features added since v0.5: - PNG support with alpha/transparency (soft masks) - FlateDecode compression via libdeflate-zig - Bookmarks/Outline for document navigation - Bezier curves, circles, ellipses, arcs - Transformations (rotate, scale, translate, skew) - Transparency/opacity (fill and stroke alpha) - Linear and radial gradients (Shading Patterns) - Code128 (1D) and QR Code (2D) barcodes - TrueType font parsing (metrics, glyph widths) - RC4 encryption module (40/128-bit) - AcroForms module (TextField, CheckBox) - SVG import (basic shapes and paths) - Template system (reusable layouts) - Markdown styling (bold, italic, links, headings, lists) Documentation: - README.md: Complete API reference with code examples - FUTURE_IMPROVEMENTS.md: Detailed roadmap for future development - CLAUDE.md: Updated to v1.0 release status Stats: - 125+ unit tests passing - 16 demo examples - 46 source files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
149 lines
5.3 KiB
Zig
149 lines
5.3 KiB
Zig
//! 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());
|
|
}
|