Releases: hank-ai/hankpdf
Release list
v0.4.0 — native-text glyph style on WordBox
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, andbaseline_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
v0.3.0 — production-safety hardening + shim removal
[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::DeprecationWarningto catch any remaining
shim usage before upgrading.
- Was:
Added
- Native-dep boot check.
hankpdfnow fails loud at startup with
a per-platform install hint when Tesseract / qpdf / OpenJPEG /
jbig2enc are missing or below the supported floor. Run
hankpdf --doctorfor the full report. Exit code 17
(E-ENV-MISSING). - Per-page worker memory caps with cooperative shutdown. Linux
usesRLIMIT_AS; Windows ≥ 8 uses Job Object self-assign via ctypes
(no pywin32). macOS attemptsRLIMIT_ASbut 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 sharedmultiprocessing.Eventand 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-mbor
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_workerswould exceed 70% of available host RAM. Distinct
fromMemoryCapExceededError(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 INTCLI flag. Per-worker cap override in
megabytes. Pass0to disable (test escape hatch).--correlation-id IDCLI flag. Format-validated
([A-Za-z0-9._:-]{1,64}). Threaded into
CompressReport.correlation_idand stamped onto every stderr line
via the existingcorr=prefix mechanism.--preserve-signaturesflag. Signed-PDF passthrough alternative
to--allow-signed-invalidation. Mutually exclusive.correlation_idkwarg oncompress_stream(). Validated against
[A-Za-z0-9._:-]{1,64}. Argument validation runs BEFORE the env
check, so bad formats produceValueErrornotEnvironmentError.- Decompression-bomb regression corpus. 3 deterministic fixtures
(huge_page_dimensions→ exit 16,xref_loop→ exit 13,
objstm_explosion→ exit 12) plus an on-demandlength_mismatch
generator. Each refusal exits with a pinned structured code in
under 30 s. WiresMAX_PAGE_AXIS_PT = 14400(200 inches) check in
_enforce_input_policyfor the huge-MediaBox fixture. signature_stateandsignature_invalidatedonCompressReport.
Schema v5 (additive — old consumers continue to work).worker_memory_cap_bytesandworker_peak_rss_max_byteson
CompressReport. Visibility into the cap that fired.PolicyDecisionenum — 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__.pyinto the newhankpdf.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 compressIf 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)
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 buildproduceshankpdf-0.2.2.tar.gz+hankpdf-0.2.2-py3-none-any.whl.hankpdf --helpopens with "HankPDF — compress scanned PDFs locally."
v0.2.1 — PyPI README refresh (install prompt visible on PyPI)
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(themetadata-actionindocker.ymlstrips thevprefix; 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
hankpdfpackage out of the image (PR #21). The retagged v0.2.0 image (digestsha256:2144c8c…) is signed and SLSA-attested. PyPIhankpdf 0.2.0was 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 buildproduceshankpdf-0.2.1.tar.gz+hankpdf-0.2.1-py3-none-any.whl.
v0.2.0 — distribution rename to hankpdf
Distribution rename: pdf-smasher → hankpdf. 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, CompressOptionsThe 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/hankpdfGHCR image name was always brandedhankpdfindependent of the repo slug — image-tag URLs in scripts and docs continue to work.CompressReport.schema_versionstays at4(wire format unchanged; this is build-metadata rename, not a wire-format change).- All runtime artifacts already used
hankpdf: env varHANKPDF_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 --versionand the legacypdf_smashershim work. - mypy strict + ruff clean.
uv buildproduceshankpdf-0.2.0.tar.gzandhankpdf-0.2.0-py3-none-any.whl.
v0.1.0 — first public release
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, default0.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.
--ocrnow means "ensure searchable" (fills gaps);--strip-text-layeropts out;--re-ocrforces. - 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_NOFOLLOWon 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 buildproducespdf_smasher-0.1.0.tar.gzandpdf_smasher-0.1.0-py3-none-any.whl.- GHCR image cosign-signed (Rekor tlog index
1396952604) and SLSA-attested.
jbig2-windows-v0.1.0
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 | iexExtract 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.