//! Pdf - Main facade for PDF document creation //! //! This is the main entry point for creating PDF documents. //! Provides a high-level API similar to fpdf2's FPDF class. //! //! Based on: fpdf2/fpdf/fpdf.py (FPDF class) const std = @import("std"); const Page = @import("page.zig").Page; const ContentStream = @import("content_stream.zig").ContentStream; const Color = @import("graphics/color.zig").Color; const Font = @import("fonts/type1.zig").Font; const TrueTypeFont = @import("fonts/ttf.zig").TrueTypeFont; const PageSize = @import("objects/base.zig").PageSize; const Orientation = @import("objects/base.zig").Orientation; const Unit = @import("objects/base.zig").Unit; const OutputProducer = @import("output/producer.zig").OutputProducer; const PageData = @import("output/producer.zig").PageData; const ExtGStateData = @import("output/producer.zig").ExtGStateData; const GradientOutputData = @import("output/producer.zig").GradientOutputData; const producer_GradientType = @import("output/producer.zig").GradientType; const GradientData = @import("graphics/gradient.zig").GradientData; const page_GradientType = @import("graphics/gradient.zig").GradientType; const DocumentMetadata = @import("output/producer.zig").DocumentMetadata; const CompressionOptions = @import("output/producer.zig").CompressionOptions; const ImageInfo = @import("images/image_info.zig").ImageInfo; const jpeg = @import("images/jpeg.zig"); const png = @import("images/png.zig"); const images_mod = @import("images/mod.zig"); const Outline = @import("outline.zig").Outline; /// Configuration constants for zcatpdf pub const Config = struct { /// Maximum file size for image loading (default: 10MB) pub const max_image_file_size: usize = 10 * 1024 * 1024; /// Maximum decompression buffer size (default: 100MB) pub const max_decompression_size: usize = 100 * 1024 * 1024; }; /// A PDF document builder. /// /// Example usage: /// ```zig /// var pdf = Pdf.init(allocator, .{}); /// defer pdf.deinit(); /// /// var page = try pdf.addPage(.{}); /// try page.setFont(.helvetica_bold, 24); /// try page.drawText(50, 750, "Hello, World!"); /// /// try pdf.save("hello.pdf"); /// ``` pub const Pdf = struct { allocator: std.mem.Allocator, /// All pages in the document pages: std.ArrayListUnmanaged(Page), /// All images in the document images: std.ArrayListUnmanaged(ImageInfo), /// TrueType fonts loaded ttf_fonts: std.ArrayListUnmanaged(TrueTypeFont), /// TTF font data (raw bytes, need to keep alive) ttf_data: std.ArrayListUnmanaged([]u8), /// Document outline (bookmarks) outline: Outline, /// Document metadata title: ?[]const u8 = null, author: ?[]const u8 = null, subject: ?[]const u8 = null, creator: ?[]const u8 = null, /// Default page settings default_page_size: PageSize, default_orientation: Orientation, unit: Unit, /// Compression settings compression: CompressionOptions, const Self = @This(); /// Options for creating a PDF document. pub const Options = struct { /// Default page size page_size: PageSize = .a4, /// Default page orientation orientation: Orientation = .portrait, /// Unit of measurement for user coordinates unit: Unit = .pt, /// Compression options for PDF streams compression: CompressionOptions = .{}, }; /// Options for adding a page. pub const PageOptions = struct { /// Page size (uses document default if null) size: ?PageSize = null, /// Page orientation (uses document default if null) orientation: ?Orientation = null, }; // ========================================================================= // Initialization // ========================================================================= /// Creates a new PDF document. pub fn init(allocator: std.mem.Allocator, options: Options) Self { return .{ .allocator = allocator, .pages = .{}, .images = .{}, .ttf_fonts = .{}, .ttf_data = .{}, .outline = Outline.init(allocator), .default_page_size = options.page_size, .default_orientation = options.orientation, .unit = options.unit, .compression = options.compression, }; } /// Sets the compression level (0-12, where 0=disabled, 6=default, 12=max). pub fn setCompressionLevel(self: *Self, level: i32) void { if (level <= 0) { self.compression.enabled = false; } else { self.compression.enabled = true; self.compression.level = @min(level, 12); } } /// Enables or disables stream compression. pub fn setCompression(self: *Self, enabled: bool) void { self.compression.enabled = enabled; } /// Frees all resources. pub fn deinit(self: *Self) void { for (self.pages.items) |*page| { page.deinit(); } self.pages.deinit(self.allocator); // Free image data if owned for (self.images.items) |*img| { img.deinit(self.allocator); } self.images.deinit(self.allocator); // Free TTF fonts for (self.ttf_fonts.items) |*font| { font.deinit(); } self.ttf_fonts.deinit(self.allocator); // Free TTF raw data for (self.ttf_data.items) |data| { self.allocator.free(data); } self.ttf_data.deinit(self.allocator); self.outline.deinit(); } // ========================================================================= // Document Metadata // ========================================================================= /// Sets the document title. pub fn setTitle(self: *Self, title: []const u8) void { self.title = title; } /// Sets the document author. pub fn setAuthor(self: *Self, author: []const u8) void { self.author = author; } /// Sets the document subject. pub fn setSubject(self: *Self, subject: []const u8) void { self.subject = subject; } /// Sets the document creator. pub fn setCreator(self: *Self, creator: []const u8) void { self.creator = creator; } // ========================================================================= // Page Management // ========================================================================= /// Adds a new page to the document. pub fn addPage(self: *Self, options: PageOptions) !*Page { const size = options.size orelse self.default_page_size; const orientation = options.orientation orelse self.default_orientation; const dims = size.dimensions(); const width = if (orientation == .landscape) dims.height else dims.width; const height = if (orientation == .landscape) dims.width else dims.height; const page = Page.initCustom(self.allocator, width, height); try self.pages.append(self.allocator, page); return &self.pages.items[self.pages.items.len - 1]; } /// Adds a new page with custom dimensions (in the document's unit). pub fn addPageCustom(self: *Self, width: f32, height: f32) !*Page { const width_pt = self.unit.toPoints(width); const height_pt = self.unit.toPoints(height); const page = Page.initCustom(self.allocator, width_pt, height_pt); try self.pages.append(self.allocator, page); return &self.pages.items[self.pages.items.len - 1]; } /// Returns the number of pages. pub fn pageCount(self: *const Self) usize { return self.pages.items.len; } /// Returns a page by index (0-based). pub fn getPage(self: *Self, index: usize) ?*Page { if (index < self.pages.items.len) { return &self.pages.items[index]; } return null; } // ========================================================================= // Image Management // ========================================================================= /// Adds a JPEG image from raw data and returns its index. /// The image data is stored and will be embedded in the PDF. pub fn addJpegImage(self: *Self, jpeg_data: []const u8) !usize { const info = try jpeg.parse(jpeg_data); try self.images.append(self.allocator, info); return self.images.items.len - 1; } /// Adds a JPEG image from a file and returns its index. pub fn addJpegImageFromFile(self: *Self, path: []const u8) !usize { const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); const data = try file.readToEndAlloc(self.allocator, Config.max_image_file_size); var info = try jpeg.parse(data); // Mark as owned since we allocated the data info.data = data; info.owns_data = true; try self.images.append(self.allocator, info); return self.images.items.len - 1; } /// Adds a PNG image from raw data and returns its index. /// Supports PNG with alpha channel (transparency). pub fn addPngImage(self: *Self, png_data: []const u8) !usize { const info = try png.parse(self.allocator, png_data); try self.images.append(self.allocator, info); return self.images.items.len - 1; } /// Adds a PNG image from a file and returns its index. /// Supports PNG with alpha channel (transparency). pub fn addPngImageFromFile(self: *Self, path: []const u8) !usize { const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); const data = try file.readToEndAlloc(self.allocator, Config.max_image_file_size); defer self.allocator.free(data); const info = try png.parse(self.allocator, data); try self.images.append(self.allocator, info); return self.images.items.len - 1; } /// Adds an image from file, auto-detecting format (JPEG or PNG). pub fn addImageFromFile(self: *Self, path: []const u8) !usize { const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); const data = try file.readToEndAlloc(self.allocator, Config.max_image_file_size); const format = images_mod.detectFormat(data) orelse { self.allocator.free(data); return error.UnsupportedImageFormat; }; switch (format) { .jpeg => { var info = try jpeg.parse(data); info.data = data; info.owns_data = true; try self.images.append(self.allocator, info); }, .png => { defer self.allocator.free(data); const info = try png.parse(self.allocator, data); try self.images.append(self.allocator, info); }, } return self.images.items.len - 1; } /// Returns the ImageInfo for an image by index. pub fn getImage(self: *const Self, index: usize) ?*const ImageInfo { if (index < self.images.items.len) { return &self.images.items[index]; } return null; } /// Returns the number of images. pub fn imageCount(self: *const Self) usize { return self.images.items.len; } // ========================================================================= // TrueType Font Management // ========================================================================= /// Adds a TrueType font from raw data and returns its index. /// The font can then be used with page.setTtfFont(index, size). pub fn addTtfFont(self: *Self, ttf_data: []const u8) !usize { const font = try TrueTypeFont.parse(self.allocator, ttf_data); try self.ttf_fonts.append(self.allocator, font); return self.ttf_fonts.items.len - 1; } /// Adds a TrueType font from a file and returns its index. pub fn addTtfFontFromFile(self: *Self, path: []const u8) !usize { const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); const data = try file.readToEndAlloc(self.allocator, Config.max_image_file_size); try self.ttf_data.append(self.allocator, data); const font = try TrueTypeFont.parse(self.allocator, data); try self.ttf_fonts.append(self.allocator, font); return self.ttf_fonts.items.len - 1; } /// Returns the TrueTypeFont for a font by index. pub fn getTtfFont(self: *const Self, index: usize) ?*const TrueTypeFont { if (index < self.ttf_fonts.items.len) { return &self.ttf_fonts.items[index]; } return null; } /// Returns the number of TTF fonts loaded. pub fn ttfFontCount(self: *const Self) usize { return self.ttf_fonts.items.len; } // ========================================================================= // Bookmarks / Outline // ========================================================================= /// Adds a top-level bookmark pointing to a page. pub fn addBookmark(self: *Self, title: []const u8, page: usize) !void { try self.outline.addBookmark(title, page); } /// Adds a bookmark with a specific Y position on the page. pub fn addBookmarkAt(self: *Self, title: []const u8, page: usize, y: f32) !void { try self.outline.addBookmarkAt(title, page, y); } /// Adds a nested bookmark (child). pub fn addBookmarkChild(self: *Self, title: []const u8, page: usize, level: u8) !void { try self.outline.addChild(title, page, level); } /// Adds a nested bookmark with Y position. pub fn addBookmarkChildAt(self: *Self, title: []const u8, page: usize, y: f32, level: u8) !void { try self.outline.addChildAt(title, page, y, level); } /// Returns the number of bookmarks. pub fn bookmarkCount(self: *const Self) usize { return self.outline.count(); } // ========================================================================= // Output // ========================================================================= /// Renders the document to a byte buffer. pub fn render(self: *Self) ![]u8 { // Collect page data var page_data: std.ArrayListUnmanaged(PageData) = .{}; defer page_data.deinit(self.allocator); // Keep track of all slices to free them after generation var font_slices: std.ArrayListUnmanaged([]Font) = .{}; var extgstate_slices: std.ArrayListUnmanaged([]ExtGStateData) = .{}; var gradient_slices: std.ArrayListUnmanaged([]GradientOutputData) = .{}; defer { for (font_slices.items) |slice| { self.allocator.free(slice); } font_slices.deinit(self.allocator); for (extgstate_slices.items) |slice| { self.allocator.free(slice); } extgstate_slices.deinit(self.allocator); for (gradient_slices.items) |slice| { self.allocator.free(slice); } gradient_slices.deinit(self.allocator); } for (self.pages.items) |*page| { // Get fonts used - convert to owned slice var fonts: std.ArrayListUnmanaged(Font) = .{}; var iter = page.fonts_used.keyIterator(); while (iter.next()) |font| { try fonts.append(self.allocator, font.*); } const fonts_slice = try fonts.toOwnedSlice(self.allocator); try font_slices.append(self.allocator, fonts_slice); // Get ExtGStates used - convert to owned slice var extgstates: std.ArrayListUnmanaged(ExtGStateData) = .{}; for (page.getExtGStates()) |gs| { try extgstates.append(self.allocator, .{ .fill_opacity = gs.fill_opacity, .stroke_opacity = gs.stroke_opacity, }); } const extgstates_slice = try extgstates.toOwnedSlice(self.allocator); try extgstate_slices.append(self.allocator, extgstates_slice); // Get Gradients used - convert to output format var gradients: std.ArrayListUnmanaged(GradientOutputData) = .{}; for (page.getGradients()) |grad| { // Convert GradientData to GradientOutputData const start_color = grad.start_color.toRgbFloats(); const end_color = grad.end_color.toRgbFloats(); try gradients.append(self.allocator, .{ .gradient_type = if (grad.gradient_type == page_GradientType.linear) producer_GradientType.linear else producer_GradientType.radial, .coords = grad.coords, .start_color = .{ start_color.r, start_color.g, start_color.b }, .end_color = .{ end_color.r, end_color.g, end_color.b }, }); } const gradients_slice = try gradients.toOwnedSlice(self.allocator); try gradient_slices.append(self.allocator, gradients_slice); try page_data.append(self.allocator, .{ .width = page.width, .height = page.height, .content = page.getContent(), .fonts_used = fonts_slice, .links = page.getLinks(), .extgstates = extgstates_slice, .gradients = gradients_slice, }); } // Collect image pointers var image_ptrs: std.ArrayListUnmanaged(*const ImageInfo) = .{}; defer image_ptrs.deinit(self.allocator); for (self.images.items) |*img| { try image_ptrs.append(self.allocator, img); } // Generate PDF var producer = OutputProducer.initWithCompression(self.allocator, self.compression); defer producer.deinit(); return try producer.generateFull(page_data.items, .{ .title = self.title, .author = self.author, .subject = self.subject, .creator = self.creator, }, image_ptrs.items, self.outline.getItems()); } /// Saves the document to a file. pub fn save(self: *Self, path: []const u8) !void { const data = try self.render(); defer self.allocator.free(data); const file = try std.fs.cwd().createFile(path, .{}); defer file.close(); try file.writeAll(data); } /// Outputs the document and returns the bytes. pub fn output(self: *Self) ![]u8 { return try self.render(); } }; // ============================================================================= // Tests // ============================================================================= test "Pdf init" { const allocator = std.testing.allocator; var pdf = Pdf.init(allocator, .{}); defer pdf.deinit(); try std.testing.expectEqual(@as(usize, 0), pdf.pageCount()); } test "Pdf addPage" { const allocator = std.testing.allocator; var pdf = Pdf.init(allocator, .{}); defer pdf.deinit(); _ = try pdf.addPage(.{}); try std.testing.expectEqual(@as(usize, 1), pdf.pageCount()); _ = try pdf.addPage(.{ .size = .letter }); try std.testing.expectEqual(@as(usize, 2), pdf.pageCount()); } test "Pdf metadata" { const allocator = std.testing.allocator; var pdf = Pdf.init(allocator, .{}); defer pdf.deinit(); pdf.setTitle("Test Document"); pdf.setAuthor("Test Author"); try std.testing.expectEqualStrings("Test Document", pdf.title.?); try std.testing.expectEqualStrings("Test Author", pdf.author.?); } test "Pdf render" { const allocator = std.testing.allocator; var pdf = Pdf.init(allocator, .{}); defer pdf.deinit(); var page = try pdf.addPage(.{}); try page.setFont(.helvetica, 12); try page.drawText(100, 700, "Hello"); const output = try pdf.render(); defer allocator.free(output); try std.testing.expect(std.mem.startsWith(u8, output, "%PDF-1.4")); try std.testing.expect(std.mem.endsWith(u8, output, "%%EOF\n")); }