Skip to content

Releases: hank-ai/hankpdf

v0.4.0 — native-text glyph style on WordBox

Choose a tag to compare

@jackneil jackneil released this 30 Jun 20:52
934417f

Added

Native-text glyph style on WordBox. extract_native_word_boxes now recovers each native-text word's original glyph style via pdfium's font + text-matrix APIs and exposes it on WordBox:

  • font_name, font_flags, font_size_pt, font_weight, color, and baseline_y (from the text matrix, in raster pixels).

OCR word boxes leave them None — a rasterized scan has no font metadata. This lets a downstream consumer (e.g. a redactor) re-draw replacement text matched to the original family, size, colour and baseline instead of guessing from the bounding box.

Backward-compatible: the new fields default to None, so existing WordBox construction and equality are unaffected. Probing is best-effort — any glyph pdfium can't describe degrades to None, never breaks extraction.

🤖 Generated with Claude Code

v0.3.1

Choose a tag to compare

@jackneil jackneil released this 28 Jun 22:19
6324a30

Lower Python floor to >=3.11 (PEP-758 syntax fixes from #33). No behavior change; widens supported Python so downstream consumers can install on 3.11-3.13.

v0.3.0 — production-safety hardening + shim removal

Choose a tag to compare

@jackneil jackneil released this 04 May 12:12
794a57d

[0.3.0] - 2026-05-03

Removed (BREAKING)

  • pdf_smasher/ deprecation shim. The shim was a one-cycle soft landing
    for the v0.2.0 distribution rename. Migrate now:
    • Was: from pdf_smasher import compress
    • Now: from hankpdf import compress
    • Was: pdf_smasher.__version__
    • Now: hankpdf.__version__
    • Was: importlib.metadata.version("pdf-smasher")
    • Now: importlib.metadata.version("hankpdf")
      Run your test suite under
      PYTHONWARNINGS=error::DeprecationWarning to catch any remaining
      shim usage before upgrading.

Added

  • Native-dep boot check. hankpdf now fails loud at startup with
    a per-platform install hint when Tesseract / qpdf / OpenJPEG /
    jbig2enc are missing or below the supported floor. Run
    hankpdf --doctor for the full report. Exit code 17
    (E-ENV-MISSING).
  • Per-page worker memory caps with cooperative shutdown. Linux
    uses RLIMIT_AS; Windows ≥ 8 uses Job Object self-assign via ctypes
    (no pywin32). macOS attempts RLIMIT_AS but the kernel rejects it
    — falls back to the watchdog. Default cap formula:
    min(max(8 GB, 16 × input_size), 16 GB), further clamped by an
    aggregate-envelope check against
    psutil.virtual_memory().available × 0.7 / n_workers. A parent-side
    psutil RSS watchdog runs as a backstop; when it observes a worker
    exceeding the cap, it sets a shared multiprocessing.Event and ALL
    in-flight workers cooperatively drain at the next safe-write
    boundary (no SIGTERM mid-write — that path corrupts partial output
    streams). Override via --max-worker-memory-mb or
    CompressOptions.max_worker_memory_mb. Exit code 18 (E-MEM-CAP).
  • HostResourceError + exit code 19 (E-HOST-RESOURCE). Raised at
    startup when the aggregate-envelope check determines that
    cap × n_workers would exceed 70% of available host RAM. Distinct
    from MemoryCapExceededError (worker died from cap) — the host has
    insufficient memory before any worker spawns. Reduce --max-workers
    or free memory; jobs that previously OOM-killed the host now refuse
    cleanly at startup.
  • --max-worker-memory-mb INT CLI flag. Per-worker cap override in
    megabytes. Pass 0 to disable (test escape hatch).
  • --correlation-id ID CLI flag. Format-validated
    ([A-Za-z0-9._:-]{1,64}). Threaded into
    CompressReport.correlation_id and stamped onto every stderr line
    via the existing corr= prefix mechanism.
  • --preserve-signatures flag. Signed-PDF passthrough alternative
    to --allow-signed-invalidation. Mutually exclusive.
  • correlation_id kwarg on compress_stream(). Validated against
    [A-Za-z0-9._:-]{1,64}. Argument validation runs BEFORE the env
    check, so bad formats produce ValueError not EnvironmentError.
  • Decompression-bomb regression corpus. 3 deterministic fixtures
    (huge_page_dimensions → exit 16, xref_loop → exit 13,
    objstm_explosion → exit 12) plus an on-demand length_mismatch
    generator. Each refusal exits with a pinned structured code in
    under 30 s. Wires MAX_PAGE_AXIS_PT = 14400 (200 inches) check in
    _enforce_input_policy for the huge-MediaBox fixture.
  • signature_state and signature_invalidated on CompressReport.
    Schema v5 (additive — old consumers continue to work).
  • worker_memory_cap_bytes and worker_peak_rss_max_bytes on
    CompressReport.
    Visibility into the cap that fired.
  • PolicyDecision enum — public API (re-exported via
    hankpdf/__init__.py:__all__). Returned by _enforce_input_policy
    to flag passthrough requests instead of raising a sentinel exception.
  • _environment.py, sandbox/platform_caps.py,
    engine/per_page_gate.py
    — new internal modules; not part of the
    public API.

Changed

  • qpdf floor enforced at 11.6.3. Was documented in
    docs/ENVIRONMENT.md; now hard-checked at startup. Lower versions
    abort with exit 17.
  • OpenJPEG floor enforced at 2.5.4 (CVE-2025-54874). Probed via
    Pillow encode test.
  • Internal refactor: the per-page MRC gate moved out of
    hankpdf/__init__.py into the new hankpdf.engine.per_page_gate
    module. No behavior change.

Migration

# Was (0.2.x):
from pdf_smasher import compress

# Now (0.3.0):
from hankpdf import compress

If you may have lingering pdf_smasher imports, run your test suite
under PYTHONWARNINGS=error::DeprecationWarning against 0.2.x first
to flag every call site. The 0.3.0 wheel does not ship the shim.

# 0.2.x: signed PDF compressed by default (silently invalid signature).
hankpdf.compress(signed_input)  # signature destroyed, no warning
# 0.3.0: signed PDF refused by default; pick one:
hankpdf.compress(signed_input, options=CompressOptions(preserve_signatures=True))
hankpdf.compress(signed_input, options=CompressOptions(allow_signed_invalidation=True))

Dependencies

  • New runtime dep: psutil>=5.9,<8 (RSS watchdog).
  • New test-only dep: pyhanko>=0.27,<1 (signed-PDF fixture generation).

v0.2.2 — CTO terminology fix (PDF compressor, not shrinker)

Choose a tag to compare

@jackneil jackneil released this 29 Apr 14:37
18ffd1e

No code changes. Doc-only patch release; updates terminology on the PyPI project page, the GHCR image label, hankpdf --help, and all docs from "PDF shrinker" / "shrunk" to "PDF compressor" / "compressed."

Why

Per CTO direction: "shrinking" implies pixel resize / dimensional reduction, which is one operation in the pipeline (background-layer DPI downsampling) but not the product. The product is byte-level compression via codec changes (JBIG2 / JPEG / JPEG2000), MRC layering, and foreground/background segmentation.

What changed

11 files, one line each. User-facing surfaces:

  • PyPI project page (pyproject.toml [project].description)
  • GHCR image label (docker/Dockerfile org.opencontainers.image.description)
  • hankpdf --help (argparse description)
  • from hankpdf import __doc__ (module docstring)
  • README tagline + status line
  • Lead lines / section headers in docs/ARCHITECTURE.md, docs/SPEC.md, docs/PERFORMANCE.md, docs/THREAT_MODEL.md

tests/integration/test_ratio_gate.py:311 switched "shrink" → "reduce" (that comment was about page count, not bytes — neither shrink nor compress fit there).

CHANGELOG history and docs/superpowers/{plans,specs}/* deliberately preserved (frozen text per shipped version / write-time snapshots).

Install

pip install hankpdf
docker pull ghcr.io/hank-ai/hankpdf:0.2.2

Verify

hankpdf --doctor

Test plan

  • 393 tests pass on Linux / macOS / Windows CI (Python 3.14).
  • mypy strict + ruff + format clean.
  • uv build produces hankpdf-0.2.2.tar.gz + hankpdf-0.2.2-py3-none-any.whl.
  • hankpdf --help opens with "HankPDF — compress scanned PDFs locally."

v0.2.1 — PyPI README refresh (install prompt visible on PyPI)

Choose a tag to compare

@jackneil jackneil released this 29 Apr 00:57
e092b29

Doc-only patch release. No code changes. The PyPI project page (https://pypi.org/project/hankpdf/) now shows the "Install via Claude Code / Codex / any coding agent" prompt — a paste-into-agent block that walks a fresh-machine bootstrap on Windows / macOS / Linux from zero (no Python, no native deps) to hankpdf in.pdf -o out.pdf working in a terminal.

PyPI distribution metadata is immutable per version, so the README content shipped with hankpdf 0.2.0 was frozen on the PyPI page. To pick up the install prompt that landed in PR #22 after 0.2.0 was published, version bump.

What's actually new

  • README's new install-via-Claude prompt is now the canonical PyPI project description.
  • README GHCR tag references corrected: :v0.2.0:0.2.1 (the metadata-action in docker.yml strips the v prefix; published image tags are :0.2.1, :0.2, :latest, :sha-…).
  • v0.2.0 GHCR image was retagged 2026-04-29 to fix a Dockerfile bug that left the canonical hankpdf package out of the image (PR #21). The retagged v0.2.0 image (digest sha256:2144c8c…) is signed and SLSA-attested. PyPI hankpdf 0.2.0 was never affected (wheel was always built with both packages).

Install

pip install hankpdf
docker pull ghcr.io/hank-ai/hankpdf:0.2.1

Verify

hankpdf --doctor

Test plan

  • 393 tests pass on Linux / macOS / Windows CI (Python 3.14).
  • mypy strict + ruff + format clean.
  • uv build produces hankpdf-0.2.1.tar.gz + hankpdf-0.2.1-py3-none-any.whl.

v0.2.0 — distribution rename to hankpdf

Choose a tag to compare

@jackneil jackneil released this 29 Apr 00:21
a0e3cf6

Distribution rename: pdf-smasherhankpdf. The CLI, GHCR image, brand, and now the PyPI distribution + Python import package are all hankpdf. New users now just pip install hankpdf and run hankpdf — the inconsistency that prompted this rename is gone.

Migration

# was
pip install pdf-smasher

# now
pip install hankpdf
# was
from pdf_smasher import compress, CompressOptions

# now
from hankpdf import compress, CompressOptions

The pdf_smasher import package is preserved as a deprecation shim for one cycle. Existing from pdf_smasher import compress keeps working but emits a DeprecationWarning pointing at hankpdf. Removal scheduled for v0.3.0 — update your imports now.

pdf-smasher 0.1.0 on PyPI is yanked (per PEP 592). Existing pdf-smasher==0.1.0 pins still install with a warning; bare pip install pdf-smasher no longer resolves.

Wheel filename change (BREAKING for filename-pinned CI)

  • Old: pdf_smasher-0.1.0-py3-none-any.whl
  • New: hankpdf-0.2.0-py3-none-any.whl

Cosign verify (updated regex covers transition)

cosign verify ghcr.io/hank-ai/hankpdf:v0.2.0 \
  --certificate-identity-regexp 'https://github\.com/hank-ai/(hankpdf|pdf-smasher)/\.github/workflows/docker\.yml@refs/(heads|tags)/.+' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

The regex allows either repo path so the same recipe works against both v0.1.0 (signed under hank-ai/pdf-smasher during the brief detour) and v0.2.0+ (signed under hank-ai/hankpdf).

Local-dev cache reset

After pulling, clear cached PKG-INFO from the old dist name:

rm -rf .venv .mypy_cache .pytest_cache && uv sync

What's NOT changed

  • ghcr.io/hank-ai/hankpdf GHCR image name was always branded hankpdf independent of the repo slug — image-tag URLs in scripts and docs continue to work.
  • CompressReport.schema_version stays at 4 (wire format unchanged; this is build-metadata rename, not a wire-format change).
  • All runtime artifacts already used hankpdf: env var HANKPDF_PASSWORD, sidecar <basename>.hankpdf.json, log prefix [hankpdf], /etc/hankpdf/build-info.json.

Install

pip install hankpdf                            # PyPI (Python deps only — see README for native deps)
docker pull ghcr.io/hank-ai/hankpdf:v0.2.0     # zero host setup

Verify

hankpdf --doctor   # confirms native binaries (tesseract / qpdf / jbig2enc) are on PATH

Test plan for this release

  • 393 tests pass on Linux / macOS / Windows CI (Python 3.14), including 4 new install-smoke tests that build the wheel, install it into a clean venv, and verify both hankpdf --version and the legacy pdf_smasher shim work.
  • mypy strict + ruff clean.
  • uv build produces hankpdf-0.2.0.tar.gz and hankpdf-0.2.0-py3-none-any.whl.

v0.1.0 — first public release

Choose a tag to compare

@jackneil jackneil released this 28 Apr 20:04
eb24ecd

First public release of HankPDF (pdf-smasher on PyPI). Pre-1.0 SemVer applies — anything may break between minor versions until 1.0. CompressReport.schema_version = 4 is the wire-contract version for sidecar/JSON consumers.

Highlights

  • Per-page selective MRC gate (--per-page-min-image-fraction, default 0.30). Pages with no meaningful image content are emitted verbatim; whole-doc passthrough on text-only PDFs returns input bytes unchanged in <0.5 s. ~15× wall-time win on a 50-page text-only PDF; image-heavy inputs unchanged.
  • Native text-layer preservation by default — searchable inputs stay searchable, byte-faithful, no Tesseract noise. --ocr now means "ensure searchable" (fills gaps); --strip-text-layer opts out; --re-ocr forces.
  • Content-preservation verifier — OCR Levenshtein + tile SSIM + structural audit; opt in with --verify.
  • Weird-PDF robust — encrypted, signed (with explicit invalidation flags), corrupt xref, JBIG2-in, Form XObjects, CMYK, linearized, tagged, PDF/A-3-embedded.
  • Multi-format output — PDF (default), chunked PDF (--max-output-mb), per-page image export (JPEG / PNG / WebP).
  • Supply-chain hardened — POSIX O_NOFOLLOW on partial writes, cosign signing, SLSA build provenance attestation.

See CHANGELOG.md for the full feature list, including the BREAKING CLI defaults (--max-input-mb 2000 → 250, --max-pages unlimited → 10000).

Install

PyPI (Python package — also requires native deps; see docs/INSTALL.md):

pip install pdf-smasher

Docker (zero host setup; native deps baked in):

docker pull ghcr.io/hank-ai/hankpdf:v0.1.0

The Docker image is multi-arch (linux/amd64, linux/arm64), cosign-signed (keyless via GitHub OIDC), and carries a SLSA v1 build-provenance attestation. Verify before running in production:

cosign verify ghcr.io/hank-ai/hankpdf:v0.1.0 \
  --certificate-identity-regexp 'https://github\.com/hank-ai/(hankpdf|pdf-smasher)/\.github/workflows/docker\.yml@refs/(heads|tags)/.+' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Verify your install

hankpdf --doctor

Reports the version of every native binary it can find and NOT FOUND for any missing one. Run this before trusting any other output.

Test plan for this release

  • 389 tests pass on Linux / macOS / Windows CI (Python 3.14).
  • mypy strict + ruff clean.
  • uv build produces pdf_smasher-0.1.0.tar.gz and pdf_smasher-0.1.0-py3-none-any.whl.
  • GHCR image cosign-signed (Rekor tlog index 1396952604) and SLSA-attested.

jbig2-windows-v0.1.0

Choose a tag to compare

Prebuilt jbig2.exe for native Windows x64 - built from
agl/jbig2enc at commit
e3fcf0210efa4f644e458a51616d86098d47d8da via MSYS2 / MinGW-w64.

Install:

irm https://raw.githubusercontent.com/hank-ai/hankpdf/main/scripts/install_jbig2_windows.ps1 | iex

Extract jbig2-windows-x64.zip and place the directory on
your PATH, or run the installer script above to auto-install
to %LOCALAPPDATA%\hankpdf\bin and register it on your user
PATH.

The bundle ships jbig2.exe plus every non-system DLL it
depends on (Leptonica, libtiff, libpng, libjpeg-turbo,
libwebp, zlib, MinGW C/C++ runtimes). No other setup required.