Phase 3 - Images: - JPEG parser with direct DCT passthrough (no re-encoding) - PNG metadata extraction (full embedding pending) - Page.image() for drawing images at position - Page.imageFit() for auto-scaling with aspect ratio - Pdf.addJpegImage() / addJpegImageFromFile() - XObject generation in OutputProducer New modules: - src/images/mod.zig - Image module exports - src/images/image_info.zig - ImageInfo struct - src/images/jpeg.zig - JPEG parser - src/images/png.zig - PNG metadata parser New example: - examples/image_demo.zig - Image embedding demo Stats: - 66 unit tests passing - 4 working examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
235 lines
6.9 KiB
Zig
235 lines
6.9 KiB
Zig
//! zpdf - PDF generation library for Zig
|
|
//!
|
|
//! A pure Zig library for creating PDF documents with zero dependencies.
|
|
//! Based on fpdf2 (Python) architecture.
|
|
//!
|
|
//! ## Quick Start
|
|
//!
|
|
//! ```zig
|
|
//! const zpdf = @import("zpdf");
|
|
//!
|
|
//! pub fn main() !void {
|
|
//! var pdf = zpdf.Pdf.init(allocator, .{});
|
|
//! defer pdf.deinit();
|
|
//!
|
|
//! var page = try pdf.addPage(.{});
|
|
//! try page.setFont(.helvetica_bold, 24);
|
|
//! try page.drawText(50, 750, "Hello, PDF!");
|
|
//!
|
|
//! try pdf.save("hello.pdf");
|
|
//! }
|
|
//! ```
|
|
|
|
const std = @import("std");
|
|
|
|
// =============================================================================
|
|
// Module Re-exports
|
|
// =============================================================================
|
|
|
|
/// Main PDF document facade
|
|
pub const pdf = @import("pdf.zig");
|
|
pub const Pdf = pdf.Pdf;
|
|
|
|
/// Page representation
|
|
pub const page = @import("page.zig");
|
|
pub const Page = page.Page;
|
|
pub const Align = page.Align;
|
|
pub const Border = page.Border;
|
|
pub const CellPosition = page.Page.CellPosition;
|
|
|
|
/// Content stream (low-level PDF operators)
|
|
pub const content_stream = @import("content_stream.zig");
|
|
pub const ContentStream = content_stream.ContentStream;
|
|
pub const RenderStyle = content_stream.RenderStyle;
|
|
pub const LineCap = content_stream.LineCap;
|
|
pub const LineJoin = content_stream.LineJoin;
|
|
pub const TextRenderMode = content_stream.TextRenderMode;
|
|
|
|
/// Graphics (colors, etc.)
|
|
pub const graphics = @import("graphics/mod.zig");
|
|
pub const Color = graphics.Color;
|
|
|
|
/// Fonts
|
|
pub const fonts = @import("fonts/mod.zig");
|
|
pub const Font = fonts.Font;
|
|
pub const FontFamily = fonts.FontFamily;
|
|
pub const FontState = fonts.FontState;
|
|
|
|
/// Objects (base types, page sizes, units)
|
|
pub const objects = @import("objects/mod.zig");
|
|
pub const PageSize = objects.PageSize;
|
|
pub const Orientation = objects.Orientation;
|
|
pub const Unit = objects.Unit;
|
|
|
|
/// Output (PDF generation)
|
|
pub const output = @import("output/mod.zig");
|
|
pub const OutputProducer = output.OutputProducer;
|
|
|
|
/// Images (JPEG, PNG)
|
|
pub const images = @import("images/mod.zig");
|
|
pub const ImageInfo = images.ImageInfo;
|
|
pub const ImageFormat = images.ImageFormat;
|
|
|
|
// =============================================================================
|
|
// Backwards Compatibility - Old API (Document)
|
|
// =============================================================================
|
|
|
|
/// Legacy Document type (use Pdf instead for new code).
|
|
/// Provided for backwards compatibility with existing code.
|
|
pub const Document = struct {
|
|
inner: Pdf,
|
|
|
|
const Self = @This();
|
|
|
|
/// Creates a new empty PDF document.
|
|
pub fn init(allocator: std.mem.Allocator) Self {
|
|
return .{
|
|
.inner = Pdf.init(allocator, .{}),
|
|
};
|
|
}
|
|
|
|
/// Frees all resources.
|
|
pub fn deinit(self: *Self) void {
|
|
self.inner.deinit();
|
|
}
|
|
|
|
/// Adds a new page with a standard size.
|
|
pub fn addPage(self: *Self, size: PageSize) !*Page {
|
|
return try self.inner.addPage(.{ .size = size });
|
|
}
|
|
|
|
/// Adds a new page with custom dimensions (in points).
|
|
pub fn addPageCustom(self: *Self, width: f32, height: f32) !*Page {
|
|
return try self.inner.addPageCustom(width, height);
|
|
}
|
|
|
|
/// Renders the document to a byte buffer.
|
|
pub fn render(self: *Self, allocator: std.mem.Allocator) ![]u8 {
|
|
_ = allocator; // Use inner allocator
|
|
return try self.inner.render();
|
|
}
|
|
|
|
/// Saves the document to a file.
|
|
pub fn saveToFile(self: *Self, path: []const u8) !void {
|
|
return try self.inner.save(path);
|
|
}
|
|
};
|
|
|
|
// =============================================================================
|
|
// Tests
|
|
// =============================================================================
|
|
|
|
test "zpdf re-exports" {
|
|
// Test that all types are accessible
|
|
_ = Pdf;
|
|
_ = Page;
|
|
_ = ContentStream;
|
|
_ = Color;
|
|
_ = Font;
|
|
_ = PageSize;
|
|
_ = Document;
|
|
}
|
|
|
|
test "Document backwards compatibility" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var doc = Document.init(allocator);
|
|
defer doc.deinit();
|
|
|
|
var pg = try doc.addPage(.a4);
|
|
try pg.setFont(.helvetica_bold, 24);
|
|
try pg.drawText(50, 750, "Hello");
|
|
try pg.drawLine(50, 740, 200, 740);
|
|
|
|
const data = try doc.render(allocator);
|
|
defer allocator.free(data);
|
|
|
|
try std.testing.expect(std.mem.startsWith(u8, data, "%PDF-1.4"));
|
|
}
|
|
|
|
test "new Pdf API" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var zpdf_doc = Pdf.init(allocator, .{
|
|
.page_size = .a4,
|
|
.orientation = .portrait,
|
|
});
|
|
defer zpdf_doc.deinit();
|
|
|
|
zpdf_doc.setTitle("Test Document");
|
|
zpdf_doc.setAuthor("zpdf");
|
|
|
|
var pg = try zpdf_doc.addPage(.{});
|
|
try pg.setFont(.helvetica_bold, 24);
|
|
pg.setFillColor(Color.blue);
|
|
try pg.drawText(50, 750, "Hello zpdf!");
|
|
|
|
pg.setStrokeColor(Color.red);
|
|
try pg.setLineWidth(2);
|
|
try pg.drawLine(50, 740, 200, 740);
|
|
|
|
pg.setFillColor(Color.light_gray);
|
|
try pg.fillRect(50, 600, 150, 100);
|
|
|
|
const data = try zpdf_doc.output();
|
|
defer allocator.free(data);
|
|
|
|
try std.testing.expect(std.mem.startsWith(u8, data, "%PDF-1.4"));
|
|
try std.testing.expect(std.mem.indexOf(u8, data, "/Title (Test Document)") != null);
|
|
}
|
|
|
|
test "Color types" {
|
|
const red = Color.rgb(255, 0, 0);
|
|
const floats = red.toRgbFloats();
|
|
try std.testing.expectApproxEqAbs(@as(f32, 1.0), floats.r, 0.01);
|
|
try std.testing.expectApproxEqAbs(@as(f32, 0.0), floats.g, 0.01);
|
|
try std.testing.expectApproxEqAbs(@as(f32, 0.0), floats.b, 0.01);
|
|
|
|
const hex_color = Color.hex(0xFF8000);
|
|
try std.testing.expectEqual(@as(u8, 255), hex_color.rgb_val.r);
|
|
try std.testing.expectEqual(@as(u8, 128), hex_color.rgb_val.g);
|
|
try std.testing.expectEqual(@as(u8, 0), hex_color.rgb_val.b);
|
|
}
|
|
|
|
test "Font metrics" {
|
|
const font = Font.helvetica;
|
|
try std.testing.expectEqualStrings("Helvetica", font.pdfName());
|
|
|
|
const width = font.stringWidth("Hello", 12.0);
|
|
try std.testing.expect(width > 0);
|
|
}
|
|
|
|
test "ContentStream operators" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var cs = ContentStream.init(allocator);
|
|
defer cs.deinit();
|
|
|
|
try cs.saveState();
|
|
try cs.setLineWidth(2.0);
|
|
try cs.setStrokeColorRgb(1.0, 0.0, 0.0);
|
|
try cs.moveTo(100, 200);
|
|
try cs.lineTo(300, 200);
|
|
try cs.stroke();
|
|
try cs.restoreState();
|
|
|
|
const content = cs.getContent();
|
|
try std.testing.expect(std.mem.indexOf(u8, content, "q\n") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, content, "Q\n") != null);
|
|
try std.testing.expect(std.mem.indexOf(u8, content, "1.000 0.000 0.000 RG") != null);
|
|
}
|
|
|
|
// Import all module tests
|
|
comptime {
|
|
_ = @import("content_stream.zig");
|
|
_ = @import("page.zig");
|
|
_ = @import("pdf.zig");
|
|
_ = @import("graphics/color.zig");
|
|
_ = @import("fonts/type1.zig");
|
|
_ = @import("objects/base.zig");
|
|
_ = @import("output/producer.zig");
|
|
_ = @import("images/mod.zig");
|
|
_ = @import("images/image_info.zig");
|
|
_ = @import("images/jpeg.zig");
|
|
_ = @import("images/png.zig");
|
|
}
|