Skip to content

Latest commit

 

History

History
302 lines (245 loc) · 16.9 KB

File metadata and controls

302 lines (245 loc) · 16.9 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

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.

Build Commands

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 libs

Cross-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

Architecture

Directory Structure

  • 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)

Core Modules

  • 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 conformance
    • parseInfo() - Main API that loads entire PDF into memory, extracts metadata from PDFium and XMP, returns combined result
    • parsePdfA() - Extract only PDF/A conformance from PDF byte stream
    • PdfAConformance - 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 via FPDF_LoadMemDocument). The buffer is freed in close().
    • Page - Page handle with rendering, rotation, and object iteration
    • TextPage - Text extraction with UTF-16LE to UTF-8 conversion
    • Bitmap - BGRA bitmap for rendering
    • ImageObject / ImageObjectIterator - Embedded image extraction
    • Attachment / AttachmentIterator - Embedded file attachment access
    • ExtendedMetadata - 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:

    • PdfiumLib struct with function pointers for all PDFium APIs
    • Uses FPDF_LoadMemDocument exclusively - 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
  • 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 provides addImageToPage() for adding images to PDF pages.

  • src/pdfcontent/textfmt.zig - Text formatting and PDF text content generation. Provides addTextToPage() and addJsonToPage() 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 password
    • loadPage() - Load page from document (1-based)
    • createDirectory() - Create output directory
    • generatePageContent() - Finalize page transformations
    • extensionLower() - Get lowercase file extension using stack buffer
    • hasXmlExtension() - 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, returns MetaData

  • 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 editing
    • exitWithError() / exitWithErrorMsg() - Print error and exit with code 1
    • requireInputPath() - Validate input path or exit with error
    • openDocumentOrExit() - Open PDF or exit on error
    • loadPageOrExit() - Load page or exit on error
    • setupTempFileForInPlaceEdit() / completeTempFileEdit() - Temp file with exit on error
    • generatePageContentOrExit() / generatePageContentWithNumOrExit() - Generate content or exit
    • reportSaveSuccess() - 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.

Dependencies

  • PDFium - Downloaded at runtime from bblanchon/pdfium-binaries. Dynamically loaded via std.DynLib. Library named libpdfium_v{BUILD}.dylib/so/dll.
  • zigimg - PNG encoding
  • zstbi - JPEG encoding (stb_image_write bindings)

Runtime Library Loading

  • 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

Testing

Integration Tests

  • 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)

Golden File Testing Infrastructure

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 delta
    • comparePixels() - Compare two RGBA pixel arrays with per-channel delta calculation
    • comparePngFiles() - 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 in test-files/input/
    • calculateTargetDimensions() - Calculates bitmap size preserving aspect ratio (width × height = 50,000 pixels)
    • Operation generators:
      • createExpectedTestFilesInfo() - Runs pdfzig info, captures plaintext output to .txt
      • createExpectedTestFilesInfoJson() - Runs pdfzig info --json, captures JSON to .json
      • createExpectedTestFilesRenderPageBitmaps() - Renders pages to PNG at 50k pixels
      • createExpectedTestFilesRotate90() - Rotates 90°, renders to PNG
      • createExpectedTestFilesRotate180() - Rotates 180°, renders to PNG
      • createExpectedTestFilesRotate270() - Rotates 270°, renders to PNG
      • createExpectedTestFilesMirrorHorizontal() - Mirrors horizontally, renders to PNG
      • createExpectedTestFilesMirrorVertical() - Mirrors vertically, renders to PNG
  • 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 --clean flag to optionally delete existing golden files first
    • Calls test_golden_files.createExpectedTestFiles()

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 test

Adding New Operations:

  1. Add generator function in test_golden_files.zig
  2. Call it from createExpectedTestFiles()
  3. Add corresponding test in test_golden_files_test.zig
  4. Run zig build generate-golden-files to create reference files
  5. Commit golden files to git

Key Implementation Details

  • 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_LoadDocument has been removed.
  • Document memory management: The Document struct owns the PDF buffer for the document's lifetime. PDFium's FPDF_LoadMemDocument references the buffer without copying, so the buffer must remain valid until FPDF_CloseDocument is called. The Document.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 generic MetaData struct 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:TEMPLATE syntax (can be repeated)
  • Zig 0.15 uses .c calling convention (not .C)
  • Uses std.Io.Writer and std.Io.Reader (new Zig 0.15 I/O interfaces)
  • Uses std.array_list.Managed(T).init(allocator) (not std.ArrayList(T).init(allocator) which was deprecated in Zig 0.15)

Zig Code Rules

  • ALWAYS use std.array_list.Managed(T).init(allocator) instead of deprecated std.ArrayList(T).init(allocator)
  • ALWAYS use std.fs.File.stdout() instead of deprecated std.io.getStdOut()
  • Return all errors

Markdown Rules

  • Always pad markdown tables in README.md to align vertical lines

Documentation Rules

  • When a command is changed or added, update README.md and CLAUDE.md

HTTP routes

  • Encode simple request parameters in the URL path and avoid JSON body payloads
  • Use JSON for complex requests or responses

Version Control

  • Don't create git commits automatically. Ask the user to create a commit.