This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
pdfzig is a Zig PDF library and CLI tool that uses PDFium to work with PDF files. It supports rendering pages to PNG/JPEG images, extracting text, extracting embedded images, extracting attachments, visual diff comparison, and displaying PDF metadata. The project is structured as both a library (@import("pdfzig")) and a CLI tool.
zig build # Build the executable
zig build run -- <args> # Build and run with arguments
zig build test # Run unit tests
zig build generate-golden-files # Generate golden test files for visual/text comparison
zig build generate-golden-files -Dclean # Delete and regenerate all golden files
zig build clean # Remove build artifacts and caches (zig-out/, .zig-cache/, test-cache/)
zig build fmt # Check source code formatting
zig build fmt-fix # Fix source code formatting
zig build all # Build for all supported platforms (cross-compile)
zig build all -Ddownload-pdfium # Build for all platforms and download matching PDFium libsCross-compilation targets for zig build all:
- macOS: x86_64, aarch64
- Linux: x86_64, aarch64, arm (gnueabihf)
- Windows: x86_64, x86, aarch64
Outputs are placed in zig-out/<target-triple>/ (e.g., zig-out/x86_64-linux-gnu/pdfzig).
Run the built executable directly:
./zig-out/bin/pdfzig download_pdfium # Download PDFium library (auto-runs on first use)
./zig-out/bin/pdfzig render document.pdf
./zig-out/bin/pdfzig extract_text document.pdf
./zig-out/bin/pdfzig extract_images document.pdf ./output
./zig-out/bin/pdfzig extract_attachments document.pdf
./zig-out/bin/pdfzig visual_diff doc1.pdf doc2.pdf
./zig-out/bin/pdfzig visual_diff -o ./diffs --colors rgb doc1.pdf doc2.pdf
./zig-out/bin/pdfzig visual_diff -o ./diffs --invert doc1.pdf doc2.pdf
./zig-out/bin/pdfzig info document.pdf
./zig-out/bin/pdfzig info --json document.pdf # JSON output with per-page dimensions
./zig-out/bin/pdfzig rotate 90 document.pdf # Rotate all pages 90° clockwise
./zig-out/bin/pdfzig rotate -p 1-3 180 document.pdf # Rotate pages 1-3 by 180°
./zig-out/bin/pdfzig rotate -o rotated.pdf 270 doc.pdf # Output to different file
./zig-out/bin/pdfzig delete -p 1 document.pdf # Delete first page
./zig-out/bin/pdfzig delete -p 2-5 -o trimmed.pdf doc.pdf # Delete pages 2-5, save to new file
./zig-out/bin/pdfzig add document.pdf # Add empty page at end
./zig-out/bin/pdfzig add document.pdf image.png # Add page with image
./zig-out/bin/pdfzig attach document.pdf file.xml # Attach file to PDF
./zig-out/bin/pdfzig detach -i 0 document.pdf # Remove first attachment
./zig-out/bin/pdfzig -link /path/to/libpdfium.dylib info doc.pdf # Use specific PDFium library- src/root.zig - Library root, re-exports all public modules for
@import("pdfzig") - src/main.zig - CLI entry point with subcommand dispatch
- src/cli_parsing.zig - CLI argument parsing utilities and shared types
- src/cli/ - CLI command implementations (arg parsing, stdout/stderr, process.exit)
- src/pdfzig/ - Pure library functions (error-returning, no stdout/stderr, no process.exit)
- src/pdf/ - Library-independent PDF metadata module (MetaData struct, XMP parsing, PDF/A detection)
- src/pdfium/ - PDFium bindings and library management (memory-based loading)
- src/pdfcontent/ - PDF content generation (images, text formatting)
-
src/main.zig - CLI entry point with subcommand parsing (render, extract_text, extract_images, extract_attachments, visual_diff, info, rotate, mirror, delete, add, create, attach, detach, download_pdfium). Global option
--link <path>loads PDFium from a specific path. -
src/pdf/metadata.zig - Library-independent metadata extraction combining PDFium and XMP parsing:
MetaData- Generic metadata struct with standard PDF metadata and PDF/A conformanceparseInfo()- Main API that loads entire PDF into memory, extracts metadata from PDFium and XMP, returns combined resultparsePdfA()- Extract only PDF/A conformance from PDF byte streamPdfAConformance- PDF/A conformance level (part 1-4, level a/b/u/e/f) with custom formatter
-
src/pdf/loader.zig - PDF file loading utilities:
loadPdfFile()- Load entire PDF file into memory buffer
-
src/pdf/xmp.zig - XMP metadata parser for PDF/A conformance detection:
extractPdfAConformance()- Parse PDF byte stream to find XMP packet and extract PDF/A conformance- Supports both element syntax (
<pdfaid:part>1</pdfaid:part>) and attribute syntax (pdfaid:part="1") - No XML library dependency - uses simple string matching (PDF/A spec requires XMP to be uncompressed/unencrypted)
-
src/pdfium/pdfium.zig - Idiomatic Zig bindings for PDFium. Key types:
Document- PDF document handle with metadata, attachment access, page deletion, and save functionality. Owns the PDF buffer for the document's lifetime (loaded viaFPDF_LoadMemDocument). The buffer is freed inclose().Page- Page handle with rendering, rotation, and object iterationTextPage- Text extraction with UTF-16LE to UTF-8 conversionBitmap- BGRA bitmap for renderingImageObject/ImageObjectIterator- Embedded image extractionAttachment/AttachmentIterator- Embedded file attachment accessExtendedMetadata- PDFium metadata with document properties (page count, PDF version, encryption)extractMetadataFromMemory()- Extract metadata from PDF in memory buffer
-
src/pdfium/loader.zig - Runtime dynamic library loading infrastructure:
PdfiumLibstruct with function pointers for all PDFium APIs- Uses
FPDF_LoadMemDocumentexclusively - all PDFs loaded into memory first - Version detection from filename pattern
libpdfium_v{BUILD}.{ext} findBestPdfiumLibrary()- finds highest version in executable directory
-
src/pdfium/downloader.zig - PDFium download and extraction:
- Native Zig HTTP via
std.http.Client - Native gzip decompression via
std.compress.flate.Decompress - Native tar extraction via
std.tar.pipeToFileSystem - SHA256 hash verification from GitHub API
- Native Zig HTTP via
-
src/pdfcontent/images.zig - Image I/O using zigimg (PNG) and zstbi (JPEG). Handles BGRA→RGBA/RGB conversion. Supports filename templates with
{num},{num0},{basename},{ext}variables. Also providesaddImageToPage()for adding images to PDF pages. -
src/pdfcontent/textfmt.zig - Text formatting and PDF text content generation. Provides
addTextToPage()andaddJsonToPage()for adding text content to PDF pages. -
src/pdfzig/shared.zig - Library shared utilities (error-returning, no process.exit, no stdout/stderr):
openDocument()- Open PDF with optional passwordloadPage()- Load page from document (1-based)createDirectory()- Create output directorygeneratePageContent()- Finalize page transformationsextensionLower()- Get lowercase file extension using stack bufferhasXmlExtension()- Check if filename has XML-related extension (allocation-free)addFileContentToPage()- Add image/text/JSON content to page based on file extension
-
src/pdfzig/rotate.zig -
rotatePages()- Rotate pages in a PDF document -
src/pdfzig/mirror.zig -
mirrorPages()- Mirror pages horizontally or vertically -
src/pdfzig/delete.zig -
deletePages()- Delete pages from a PDF document -
src/pdfzig/info.zig -
getInfo(allocator, *Document)- Get PDF metadata from an open document, returnsMetaData -
src/pdfzig/extract_text.zig -
extractText()- Extract text content from PDF pages -
src/pdfzig/render.zig -
renderPageToBitmap()- Render a page to a BGRA bitmap at given DPI -
src/pdfzig/attach.zig -
attachFile(allocator, *Document, name, content)- Attach content bytes to PDF document -
src/cli/shared.zig - CLI shared utilities (process.exit, stdout/stderr). Re-exports library shared functions and owns CLI-only utilities:
TempFileContext/setupTempFile()/completeTempFile()- Temp file management for in-place editingexitWithError()/exitWithErrorMsg()- Print error and exit with code 1requireInputPath()- Validate input path or exit with erroropenDocumentOrExit()- Open PDF or exit on errorloadPageOrExit()- Load page or exit on errorsetupTempFileForInPlaceEdit()/completeTempFileEdit()- Temp file with exit on errorgeneratePageContentOrExit()/generatePageContentWithNumOrExit()- Generate content or exitreportSaveSuccess()- Report save success if output differs from input
-
src/cli/*.zig - CLI command implementations (one per command: rotate, mirror, delete, info, extract_text, render, extract_images, extract_attachments, visual_diff, add, create, attach, detach, download_pdfium). Each has a
run()function with arg parsing and user-facing output.
- PDFium - Downloaded at runtime from bblanchon/pdfium-binaries. Dynamically loaded via
std.DynLib. Library namedlibpdfium_v{BUILD}.dylib/so/dll. - zigimg - PNG encoding
- zstbi - JPEG encoding (stb_image_write bindings)
- PDFium is NOT linked at build time
- On first use, if no library found, auto-downloads latest from GitHub
- Multiple versions can coexist; highest version is selected
- Library installed to same directory as executable
- Downloads verified via SHA256 hash from GitHub release API
- src/pdfzig/info_test.zig - Integration tests using real PDFs from py-pdf/sample-files
- src/pdfzig/extract_attachments_test.zig - Tests using ZUGFeRD invoice PDFs from ZUGFeRD/corpus
- Tests auto-download PDFs to
test-cache/directory (gitignored) on first run - All HTTP downloads use native Zig (no curl dependency)
Tests PDF operations by comparing output against reference files (golden files).
Module Structure:
-
src/test_visual_compare.zig - Pixel-level PNG comparison with tolerance
PixelDifference- Struct tracking max delta, diff pixel count, average deltacomparePixels()- Compare two RGBA pixel arrays with per-channel delta calculationcomparePngFiles()- Load PNGs via zigimg, decode to RGBA, compare pixels (NOT encoded bytes)- Supports RGB24, RGBA32, Grayscale8 pixel formats with automatic conversion
-
src/test_golden_files.zig - Golden file generation for all PDF operations
- Configuration constants:
TARGET_PIXEL_COUNT = 50_000,PIXEL_TOLERANCE = 5 createExpectedTestFiles()- Main entry point, iterates all PDFs intest-files/input/calculateTargetDimensions()- Calculates bitmap size preserving aspect ratio (width × height = 50,000 pixels)- Operation generators:
createExpectedTestFilesInfo()- Runspdfzig info, captures plaintext output to.txtcreateExpectedTestFilesInfoJson()- Runspdfzig info --json, captures JSON to.jsoncreateExpectedTestFilesRenderPageBitmaps()- Renders pages to PNG at 50k pixelscreateExpectedTestFilesRotate90()- Rotates 90°, renders to PNGcreateExpectedTestFilesRotate180()- Rotates 180°, renders to PNGcreateExpectedTestFilesRotate270()- Rotates 270°, renders to PNGcreateExpectedTestFilesMirrorHorizontal()- Mirrors horizontally, renders to PNGcreateExpectedTestFilesMirrorVertical()- Mirrors vertically, renders to PNG
- Configuration constants:
-
src/test_golden_files_test.zig - Tests comparing operations against golden files
- Test per operation: render, rotate (90°/180°/270°), mirror (horizontal/vertical), info (text & JSON)
- Each test: performs operation → writes temp PNG → compares pixels with golden file
- Passes if
PixelDifference.max_delta ≤ PIXEL_TOLERANCE - Text outputs compared via string equality
-
src/build_golden_files_helper.zig - Build helper executable
- Runs during
zig build generate-golden-files - Parses
--cleanflag to optionally delete existing golden files first - Calls
test_golden_files.createExpectedTestFiles()
- Runs during
Directory Structure:
test-files/
├── input/ # Test PDFs
│ ├── 1Page.pdf
│ └── 7Pages.pdf
└── expected/ # Golden files (checked into git)
├── 1Page/
│ ├── info.txt # Info command plaintext output
│ ├── info.json # Info command JSON output
│ ├── render-page-bitmaps/ # Basic rendering
│ │ └── page-1.png # 50k pixels, aspect-ratio preserved
│ ├── rotate-90/ # After 90° rotation
│ │ └── page-1.png
│ ├── rotate-180/ # After 180° rotation
│ │ └── page-1.png
│ ├── rotate-270/ # After 270° rotation
│ │ └── page-1.png
│ ├── mirror-horizontal/ # After horizontal mirror
│ │ └── page-1.png
│ └── mirror-vertical/ # After vertical mirror
│ └── page-1.png
└── 7Pages/
├── info.txt
├── info.json
├── render-page-bitmaps/
│ ├── page-1.png
│ ├── page-2.png
│ └── ... (all 7 pages)
├── rotate-90/
│ └── ... (rotated versions)
├── rotate-180/
│ └── ... (rotated versions)
├── rotate-270/
│ └── ... (rotated versions)
├── mirror-horizontal/
│ └── ... (mirrored versions)
└── mirror-vertical/
└── ... (mirrored versions)
Why Pixel-Level Comparison:
- PDFs contain creation timestamps → byte-by-byte comparison fails
- PNG encoding may vary (compression settings, metadata)
- Pixel comparison with tolerance handles anti-aliasing differences
- 50k pixel resolution naturally averages out minor rendering variations
Usage:
# Generate golden files for first time (or after adding operations)
zig build generate-golden-files
# Delete and regenerate all golden files
zig build generate-golden-files -Dclean
# Run tests (golden files must exist)
zig build testAdding New Operations:
- Add generator function in
test_golden_files.zig - Call it from
createExpectedTestFiles() - Add corresponding test in
test_golden_files_test.zig - Run
zig build generate-golden-filesto create reference files - Commit golden files to git
- Memory-first loading: All PDF operations load the entire file into memory first using
FPDF_LoadMemDocument. This enables buffer reuse between PDFium and XMP parsing, and provides a foundation for future optimizations.FPDF_LoadDocumenthas been removed. - Document memory management: The
Documentstruct owns the PDF buffer for the document's lifetime. PDFium'sFPDF_LoadMemDocumentreferences the buffer without copying, so the buffer must remain valid untilFPDF_CloseDocumentis called. TheDocument.close()method frees both the PDFium handle and the owned buffer. - PDF/A detection: Automatically detects PDF/A conformance by parsing XMP metadata directly from the PDF byte stream. Uses simple string matching (no XML library) since PDF/A requires XMP to be uncompressed and unencrypted.
- Library-independent metadata: The
src/pdf/module provides a genericMetaDatastruct that combines PDFium metadata with XMP-derived PDF/A conformance, abstracting implementation details. - PDFium outputs BGRA; conversion to RGBA (PNG) or RGB (JPEG) happens in pdfcontent/images.zig
- PDFium uses UTF-16LE for text; conversion to UTF-8 is in pdfium/pdfium.zig
- Page numbers in CLI are 1-based; PDFium API uses 0-based internally
- Multi-resolution output uses
-O DPI:FORMAT:QUALITY:TEMPLATEsyntax (can be repeated) - Zig 0.15 uses
.ccalling convention (not.C) - Uses
std.Io.Writerandstd.Io.Reader(new Zig 0.15 I/O interfaces) - Uses
std.array_list.Managed(T).init(allocator)(notstd.ArrayList(T).init(allocator)which was deprecated in Zig 0.15)
- ALWAYS use
std.array_list.Managed(T).init(allocator)instead of deprecatedstd.ArrayList(T).init(allocator) - ALWAYS use
std.fs.File.stdout()instead of deprecatedstd.io.getStdOut() - Return all errors
- Always pad markdown tables in README.md to align vertical lines
- When a command is changed or added, update README.md and CLAUDE.md
- Encode simple request parameters in the URL path and avoid JSON body payloads
- Use JSON for complex requests or responses
- Don't create git commits automatically. Ask the user to create a commit.