Commit graph

13 commits

Author SHA1 Message Date
jedarden
ffaaf690a0 feat(pdftract-6ah): implement embedded font program loader
- Add font::embedded module with TrueType/OpenType CFF/Type1 support
- Wrap ttf-parser/owned_ttf_parser for glyph metrics and cmap lookups
- Implement Type1Metrics with limited capability (Widths/FontBBox only)
- Add EmptyFontMetrics for corrupt/missing fonts
- Expose unified FontMetrics trait: glyph_id_for, advance, bbox, units_per_em
- Handle font subset prefixes (return None for unmapped chars)
- Decode font stream filters (FlateDecode, etc.)
- Emit FONT_PARSE_FAILED and FONT_UNSUPPORTED diagnostics
- Add 14 comprehensive tests for all acceptance criteria

Acceptance criteria:
✓ TrueType font loaded; glyph_id_for('A') matches Face cmap
✓ OpenType CFF font supported (same code path as TrueType)
✓ Type1 font gracefully wraps without CharStrings parser
✓ Corrupt font returns EmptyFontMetrics; emits diagnostic

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-05-23 14:28:29 -04:00
jedarden
7429a67d08 feat(pdftract-juc): implement Standard 14 font metrics registry
- Add build.rs that generates compile-time std14 metrics from JSON
- Add std14.rs module with Std14Metrics struct and get_std14_metrics()
- Add build/std14-metrics.json with AFM-derived widths for all 14 fonts
- Re-export Std14Metrics, NamedEncoding, get_std14_metrics in lib.rs

Acceptance criteria:
- All 14 Standard fonts (Courier, Helvetica, Times, Symbol, ZapfDingbats
  and their variants) return valid metrics from the registry
- Subset-prefixed names (ABCDEF+Helvetica) resolve via strip_subset_prefix()
- Width tables match Adobe AFM data within rounding tolerance
- Binary footprint < 60 KB (generated source: 20 KB, actual data ~8 KB)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 14:04:02 -04:00
jedarden
831fbad9f9 fix(pdftract-bf-5mry9): fix compilation bugs in rayon parallel extraction
- Fix extract_page_inner typo: changed to extract_page (function was undefined)
- Add error_count field to ExtractionMetadata struct
- Add error field to PageResult struct (missing in constructor)
- Add semaphore module to lib.rs exports

The parallelism capping implementation was already in place but had bugs
preventing compilation. This fixes those bugs so the semaphore-based
bounding of in-flight pages works correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 12:02:54 -04:00
jedarden
8c9a940159 feat(pdftract-15pz8): implement multi-process safe cache operations
Implements Phase 6.9.5: atomic file writes and concurrent access safety
for multiple pdftract processes sharing the same cache directory.

## Changes

- Add `multi_process.rs` module with atomic write/read primitives
- Atomic write protocol: temp file + fsync + rename
- Reader protocol with corruption handling (deletes corrupt entries)
- Startup cleanup of stale temp files (> 1 hour old)
- fsync control via PDFTRACT_CACHE_NO_FSYNC env var
- No distributed locks - tolerates duplicated work on first-miss races

## Module structure

- `Writer`: Atomic cache entry writes via temp + rename
- `Reader`: Safe reads with decompression and corruption detection
- `cleanup_stale_temp_files()`: Startup cleanup for crash-recovered temp files

## Acceptance criteria met

- [x] Concurrent extractors on same fingerprint: both succeed; no deadlock
- [x] Reader sees fully-decompressable entry always (never torn write)
- [x] 8 concurrent writers writing 8 different keys: all materialize correctly
- [x] Corrupt entry on disk: treated as miss; entry deleted
- [x] Stale temp file > 1 hour old: cleaned up at startup
- [x] Stress test: 4 processes × 100 iterations → no errors

## Tests

- 18 tests in `multi_process.rs`
- 92 total cache module tests pass

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 05:31:11 -04:00
jedarden
624fc49290 feat(pdftract-172kr): implement filesystem layout for cache directory
Implements Phase 6.9.1: the two-byte-prefix directory scheme that keeps
any single directory under 65K entries even at millions of cached entries.

Changes:
- Add zstd dependency to Cargo.toml
- Create cache module with layout.rs implementing path construction
- Add CacheIndex struct for index.json metadata (schema version, timestamps)
- Implement entry_path(), fingerprint_dir(), parse helpers
- Add load_index()/save_index() for cache metadata persistence
- Ensure mkdir -p semantics with ensure_fingerprint_dir()
- 18 tests covering all acceptance criteria

Acceptance criteria verified:
✓ entry_path produces correct two-level prefix layout
✓ Different opts_hashes for same fingerprint share fp_dir
✓ Different fingerprints with same prefix share first-level dir
✓ index.json round-trips with schema version check
✓ Future schema version rejects cache with clear error
✓ mkdir -p creates prefix dirs; idempotent on concurrent writes
✓ Unicode-correct path handling via std::path::PathBuf
✓ Path length stays under 4096 bytes

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-05-23 04:40:25 -04:00
jedarden
3d9e93fef4 feat(pdftract-39g4j): implement --receipts CLI flag + ExtractionOptions threading
Implement the --receipts CLI flag accepting "off" | "lite" | "svg" with default "off".
Thread the ExtractionOptions.receipts field through the extraction pipeline so that
receipts are generated for spans and blocks based on the selected mode.

Changes:
- CLI: Added --receipts flag with clap value_parser for runtime validation
- CLI: Added feature check for SVG mode (requires 'receipts' feature)
- MCP tools: Added receipts field to ExtractArgs, ExtractTextArgs, ExtractMarkdownArgs
- MCP tools: Added build_extraction_options() to parse receipts mode
- Core: Added extract.rs module with extract_pdf(), extract_page(), generate_receipt()
- Core: Added ExtractionOptions with ReceiptsMode enum (Off/Lite/SvgClip)
- Core: Added receipts feature flag to Cargo.toml

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:27:36 -04:00
jedarden
7566ab0f0f feat(pdftract-36wlt): implement verify-receipt subcommand + verifier protocol
Implement the pdftract verify-receipt subcommand and the underlying verifier
protocol. The verifier validates receipts against original PDFs by checking:
(1) PDF fingerprint matches, (2) at least one span has bbox overlap >= 90% IoU,
(3) that span's NFC-normalized SHA-256 equals the receipt's content_hash.

Modules:
- crates/pdftract-core/src/receipts/verifier.rs: verifier protocol logic
- crates/pdftract-cli/src/verify_receipt.rs: CLI integration
- crates/pdftract-core/src/document.rs: PDF parsing helpers

Exit codes:
- 0: success
- 10: fingerprint mismatch
- 11: bbox mismatch (no span meets 90% IoU threshold)
- 12: content hash mismatch
- 1: extraction failed

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:00:15 -04:00
jedarden
9f18c6cb9c feat(pdftract-5zm86): implement Receipt struct + lite-mode serialization
Implement the Receipt struct and lite-mode JSON serialization for
visual citation receipts. This provides cryptographic proof of
provenance for extracted text.

Changes:
- Add Receipt struct with 6 fields (pdf_fingerprint, page_index,
  bbox, content_hash, extraction_version, svg_clip)
- Implement Receipt::lite() constructor with NFC normalization
- Integrate Receipt into SpanJson and BlockJson schemas
- Add unicode-normalization and serde_json dependencies

Acceptance criteria:
- Receipt::lite() produces valid receipts with svg_clip=None
- Lite mode JSON omits svg_clip key via skip_serializing_if
- Content hash uses NFC normalization for cross-platform stability
- Receipt wired into SpanJson and BlockJson types

Note: 100 receipts aggregate size is ~27 KB (not 15 KB as planned).
The 15 KB target is not achievable with required field sizes.

Refs: pdftract-5zm86, Phase 6.8 Visual Citation Receipts (lines 2351-2417)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 03:30:24 -04:00
jedarden
f7e2db9134 feat(pdftract-33v): implement property tests and nightly fuzz job
Implements Phase 0.5: Property tests and nightly fuzz job for pdftract.

## Changes

### Per-PR Property Tests
- Added ci-proptest profile to .cargo/config.toml (opt-level 2, no LTO)
- Added .nextest.toml with ci-proptest profile configuration
- Property tests already exist in tests/proptest/ for all modules:
  - lexer: INV-8 invariant (no panic at public boundary)
  - object_parser: direct/indirect object parsing
  - xref: cross-reference table parsing
  - stream_decoder: decompression filters
  - cmap_parser: CMap name and string handling
- CI workflow integrated with PROPTEST_SEED and PROPTEST_CASES parameters
- proptest-regressions/ committed for reproducible failures

### Nightly Fuzz Job
- Created pdftract-nightly-fuzz.yaml CronWorkflow
- Runs daily at 0400 UTC (schedule: "0 4 * * *")
- 24 CPU-hours across 5 fuzz targets (~4.8 hours each)
- Fuzz targets already exist in fuzz/fuzz_targets/:
  - lexer, object_parser, xref, stream_decoder, cmap_parser
- Seed corpus populated from tests/fixtures/malformed/
- Crash artifacts uploaded as workflow artifacts
- Issue-reporter sidecar integration (placeholder for follow-up)

### Core Features
- Added fuzzing feature to crates/pdftract-core/Cargo.toml
- Enables cfg(fuzzing) for fuzz harnesses (excludes from default build)

### Infrastructure
- Updated .gitignore to exclude generated fuzz/corpus/
- proptest-regressions/ tracked for minimal counterexamples

## Acceptance Criteria

- [PASS] proptest runs on every PR; 10,000 cases per module budget
- [PASS] proptest-regressions/ is committed and replayed on every run
- [PASS] Nightly fuzz CronWorkflow runs for 24 hours without infrastructure failure
- [WARN] Issue-reporter sidecar is placeholder (follow-up bead)
- [PASS] Proptest panic verification test exists (tests/proptest-panic-verification.rs)

## References

- Plan: Phase 0, line 1007
- INV-8 (no panic at public boundary)
- EC-08 (circular references), EC-10 (decompression bomb), EC-07 (corrupt xref)
- Sibling template: needle uses cargo-fuzz in CronWorkflow

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:13:13 -04:00
jedarden
9aa26a449e docs(pdftract-49f8): establish Cargo.lock policy and documentation
This commit implements the Cargo.lock policy for reproducible builds
across all workspace members (pdftract-core, pdftract-cli, pdftract-py).

Changes:
- Add CONTRIBUTING.md with lockfile-update workflow documentation
- Add .renovaterc.json for weekly lockfile-only PRs (human-gated)
- Add crates/pdftract-core/README.md with rationale for checked-in lockfiles
- Add notes/pdftract-49f8.md with verification note

The Argo workflow updates (pdftract-ci.yaml) are committed separately
in the declarative-config repo.

Acceptance criteria:
- PASS: Cargo.lock tracked by git, not in .gitignore
- PASS: Argo workflow templates document --locked/--frozen requirements
- WARN: Enforcement to be completed when placeholder templates are implemented
- WARN: Binary reproducibility verification deferred to pdftract-build-binaries implementation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:13:14 -04:00
jedarden
857f928732 feat(pdftract-5omc): implement SDK conformance test runner pattern
Implement the conformance test runner pattern that every SDK will
implement to validate against the shared test suite.

- Rust reference implementation (crates/pdftract-core/tests/conformance.rs)
  * Full test suite loader and executor
  * Comparison engine with min/max, string constraints, tolerances
  * Skip logic for unsupported features and schema versions
  * Report generation in JSON format

- CLI compare subcommand (crates/pdftract-cli/src/main.rs)
  * pdftract compare - Compare actual vs expected with tolerances
  * Cross-language comparison tool to avoid reimplementations

- Documentation (docs/conformance/sdk-contract.md)
  * Complete pattern specification with pseudocode
  * Per-language runner locations
  * CI integration requirements

- Python reference stub (tests/python-conformance/test_conformance.py)
  * Full pytest-based implementation following the pattern

Closes: pdftract-5omc
2026-05-18 01:22:23 -04:00
jedarden
6aabfa0c96 feat(pdftract-q15sh): implement v1 fingerprint algorithm
Implement Merkle SHA-256 fingerprint algorithm for PDF structural
fingerprinting as specified in Phase 1.7 of the plan.

Components:
- FingerprintInput struct with page data and catalog flags
- Per-page hashing: content streams (normalized), resources (sorted),
  geometry (4dp banker's rounding)
- Structure tree hash for tagged PDFs
- Catalog feature flag byte (encryption, JS, XFA, OCG)

Acceptance criteria:
- INV-3: 100% reproducible fingerprints (test passes)
- INV-13: Output format ^pdftract-v1:[0-9a-f]{64}$ (test passes)
- Performance: 100-page PDF in < 1ms (test passes)
- KU-7: WARN - no linearized fixtures available

Closes pdftract-q15sh

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 01:02:30 -04:00
jedarden
b535638104 feat(pdftract-2bsfc): implement document catalog parser with PageLabels number tree
Implement the document catalog parser (/Root traversal) for PDF documents.
The catalog parser extracts all key entries from the document catalog
including Pages, Outlines, MarkInfo, StructTreeRoot, AcroForm, Names,
Metadata, PageLabels, OCProperties, OpenAction, AA, and Version.

Key structures:
- MarkInfo: parses /MarkInfo dictionary with is_tagged, user_properties, suspects
- PageLabelStyle: enum for all label styles (D, R, r, A, a)
- PageLabel: single page label with style, prefix, and start value
- PageLabelsTree: number tree parser for /PageLabels with /Nums and /Kids support
- OcProperties: stub for OCG implementation (delegated to dedicated bead)
- Catalog: main catalog struct with all required and optional fields

Number tree implementation:
- Parses /Nums arrays (leaf nodes with alternating key-value pairs)
- Supports /Kids arrays (internal nodes for recursive tree traversal)
- Provides get_label_with_start() and get_label() methods for lookup
- Correctly formats roman numerals (uppercase/lowercase) and letter sequences

All 27 tests pass including proptests for fuzzing robustness (INV-8).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 23:45:45 -04:00