pdf-tooling 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/Makefile +72 -13
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/NOTICE +1 -1
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/PKG-INFO +91 -38
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/README.md +90 -37
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/TESTING.md +222 -10
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/THIRD_PARTY_LICENSES +2 -2
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/changelog.md +222 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/pyproject.toml +10 -5
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/scripts/assert_skips.py +19 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/scripts/licenses.py +3 -3
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/scripts/measure_gate.py +11 -11
- pdf_tooling-0.3.0/scripts/reap_shims.py +217 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/__init__.py +5 -4
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/__main__.py +2 -2
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/pdfium_raster.py +35 -6
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/pdfium_text.py +39 -7
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/pdfplumber_text.py +53 -14
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/pikepdf_structure.py +83 -35
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/pypdf_structure.py +103 -25
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/reportlab_compose.py +3 -3
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/soffice_office.py +2 -2
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/subprocess_util.py +5 -5
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/tesseract_ocr.py +2 -2
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_compose.py +8 -8
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_compress.py +30 -10
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_create.py +11 -11
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_decrypt.py +7 -7
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_delete.py +20 -6
- pdf_tooling-0.3.0/src/pdf_tooling/cli/cmd_doctor.py +185 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_encrypt.py +11 -10
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_extract.py +19 -5
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_info.py +75 -22
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_linearize.py +21 -6
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_merge.py +23 -8
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_meta_get.py +25 -8
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_meta_set.py +21 -6
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_ocr.py +30 -10
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_office.py +15 -9
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_permissions.py +6 -5
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_rasterize.py +29 -16
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_reorder.py +20 -6
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_repair.py +21 -6
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_rotate.py +20 -6
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_split.py +20 -6
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_stamp.py +22 -7
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_tables.py +20 -7
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_text.py +22 -9
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_version.py +4 -4
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_watermark.py +22 -7
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/common.py +261 -19
- pdf_tooling-0.3.0/src/pdf_tooling/cli/deprecated.py +52 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/main.py +10 -9
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/password.py +16 -12
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/errors.py +67 -6
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/models.py +35 -1
- pdf_tooling-0.3.0/src/pdf_tooling/ops/batch.py +315 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/compose.py +75 -15
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/crypto.py +29 -70
- pdf_tooling-0.3.0/src/pdf_tooling/ops/document_password.py +211 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/inspect.py +40 -16
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/merge.py +91 -75
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/metadata.py +59 -15
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/ocr.py +167 -103
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/office.py +55 -43
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/optimize.py +207 -54
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/overlay.py +184 -139
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/pagerange.py +2 -2
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/pages.py +171 -77
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/procpool.py +87 -2
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/raster.py +120 -29
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/split.py +126 -116
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/textract.py +138 -34
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/output/__init__.py +4 -4
- pdf_tooling-0.3.0/src/pdf_tooling/output/json.py +103 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/output/logging.py +10 -10
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/__init__.py +11 -11
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/compose.py +6 -6
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/ocr.py +7 -7
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/office.py +8 -8
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/raster.py +14 -5
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/structure.py +91 -20
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/text.py +26 -9
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/__init__.py +6 -6
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/atomic.py +59 -17
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/confirm.py +2 -2
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/naming.py +5 -5
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/paths.py +149 -4
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/policy.py +1 -1
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/atomic_harness.py +4 -4
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/conftest.py +62 -7
- pdf_tooling-0.3.0/tests/dryreal.py +160 -0
- pdf_tooling-0.3.0/tests/fixtures/provenance/pdf_tooling-0.1.1-py3-none-any.whl.provenance.json +1 -0
- pdf_tooling-0.3.0/tests/fixtures/provenance/pdf_tooling-0.1.1.tar.gz.provenance.json +1 -0
- pdf_tooling-0.3.0/tests/fixtures/provenance/pdf_tooling-0.2.0-py3-none-any.whl.provenance.json +1 -0
- pdf_tooling-0.3.0/tests/fixtures/provenance/pdf_tooling-0.2.0.tar.gz.provenance.json +1 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/fs_snapshot.py +22 -0
- pdf_tooling-0.3.0/tests/golden/envelope_keys.json +660 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/golden/meta_get.json +3 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_atomic_crash.py +4 -4
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_compose_roundtrip.py +3 -3
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_cross_filesystem.py +1 -1
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_crypto_roundtrip.py +153 -12
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_ocr.py +128 -11
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_office.py +138 -25
- pdf_tooling-0.3.0/tests/integration/test_office_orphan_probe.py +618 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_or7_bulk_destructive.py +3 -3
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_or7_engine_absent.py +3 -3
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_out_dir_planning.py +7 -10
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_overlay_preservation.py +1 -1
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_pages_cli.py +4 -4
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_purity_primitive.py +2 -2
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_rasterize_cli.py +4 -4
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_rasterize_signals.py +29 -21
- pdf_tooling-0.3.0/tests/integration/test_render_pool_start_method.py +853 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_split_merge_atomicity.py +5 -5
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_split_merge_roundtrip.py +5 -5
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/registry.py +132 -22
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/samples_guard.py +6 -6
- pdf_tooling-0.3.0/tests/seam_sitecustomize/sitecustomize.py +87 -0
- pdf_tooling-0.3.0/tests/seams.py +1022 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_assert_skips.py +100 -0
- pdf_tooling-0.3.0/tests/test_batch_continuation.py +856 -0
- pdf_tooling-0.3.0/tests/test_brand_surfaces.py +383 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_changelog_history.py +23 -11
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_cli_contract.py +904 -14
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_cli_spine.py +176 -40
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_coverage_policy.py +2 -2
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_derived_dimensions.py +6 -6
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_docs_antirot.py +34 -14
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_docstring_pointers.py +5 -5
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_doctor.py +21 -9
- pdf_tooling-0.3.0/tests/test_engine_boundary.py +361 -0
- pdf_tooling-0.3.0/tests/test_engine_hiding_shim.py +743 -0
- pdf_tooling-0.3.0/tests/test_envelope_contract.py +1200 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_gate_budget.py +792 -12
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_gate_parity.py +109 -10
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_honesty_claims.py +189 -2
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_import_boundaries.py +592 -158
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_info.py +22 -18
- pdf_tooling-0.3.0/tests/test_license_metadata_policy.py +553 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_license_policy.py +1 -1
- pdf_tooling-0.3.0/tests/test_message_hygiene.py +300 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_pagerange.py +12 -12
- pdf_tooling-0.3.0/tests/test_password_file_contract.py +738 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_password_leaks.py +115 -44
- pdf_tooling-0.3.0/tests/test_pypi_provenance.py +893 -0
- pdf_tooling-0.3.0/tests/test_read_seams.py +1185 -0
- pdf_tooling-0.3.0/tests/test_rename_completeness.py +503 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_samples.py +42 -42
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_secret_leak_sweeps.py +2 -2
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_testdata.py +4 -4
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_usage_envelope.py +25 -25
- pdf_tooling-0.3.0/tests/test_workflow_supply_chain.py +305 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_atomic_writer.py +262 -6
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_compose.py +27 -27
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_confirm.py +7 -7
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_create.py +4 -4
- pdf_tooling-0.3.0/tests/unit/test_dryreal.py +524 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_merge.py +6 -6
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_meta_group.py +7 -7
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_metadata.py +7 -7
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_name_template.py +2 -2
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_optimize.py +14 -14
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_output_flags.py +2 -2
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_overlay.py +12 -12
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_pages.py +12 -12
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_password_hint.py +10 -10
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_password_resolution.py +7 -7
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_ports_registry.py +6 -6
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_procpool.py +1 -1
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_raster.py +53 -36
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_registry.py +19 -19
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_safety_paths.py +5 -5
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_secret.py +1 -1
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_split.py +8 -8
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_subprocess_util.py +2 -2
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_tempnames.py +1 -1
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_textract.py +20 -20
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_verb_help_content.py +54 -3
- pdf_tooling-0.2.0/src/pdf_toolkit/cli/cmd_doctor.py +0 -136
- pdf_tooling-0.2.0/src/pdf_toolkit/output/json.py +0 -50
- pdf_tooling-0.2.0/tests/dryreal.py +0 -83
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/.gitignore +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/CONTRIBUTING.md +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/LICENSE +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/scripts/assert_artifacts.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/scripts/gate_parity.py +0 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/__init__.py +0 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/__init__.py +0 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_meta.py +0 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/exit_codes.py +0 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/__init__.py +0 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/output/table.py +0 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/_faults.py +0 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/tempnames.py +0 -0
- {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/secret.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/__init__.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/_model.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_01.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_02.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_03.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_04.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_05.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_06.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_09.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_23.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/corpus.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/golden/README.md +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/golden/tables_lines.json +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/golden/text_layout.json +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/helpers/engine_hiding.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/helpers/pdfstream.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_optimize_cli.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_samples_guard_fires.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_split_merge_cli.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_text_tables_cli.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/pagetree.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/pdfium_text.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_acceptance_audit.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_corpus.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_golden.py +0 -0
- {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_samples_guard.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pdf-
|
|
1
|
+
# pdf-tooling — developer entry points.
|
|
2
2
|
#
|
|
3
3
|
# `make ci` is a SUBSET of CI, run with the same commands -- it does not
|
|
4
4
|
# predict CI. What runs locally, what does not, and why is declared in
|
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
|
|
10
10
|
.DEFAULT_GOAL := help
|
|
11
11
|
SHELL := /bin/bash
|
|
12
|
+
# PDF-34 / B-231: `-o pipefail` ONLY, and the narrowness is deliberate and
|
|
13
|
+
# asserted by a test. Without this, a recipe line that pipes a failing
|
|
14
|
+
# command into a reader inherits the READER's status, so `docs-gate`'s arms 1
|
|
15
|
+
# and 3 discarded pytest's verdict and the target exited 0 on a red suite --
|
|
16
|
+
# a gate that cannot fail is not a gate, eight lines after this file says so.
|
|
17
|
+
# `-e` is excluded because make already checks each recipe line's status, so
|
|
18
|
+
# it buys nothing here and would change four `;`-chained recipes; `-u` is
|
|
19
|
+
# excluded because `ARGS`/`PYTEST_ARGS` and several `$$`-shell variables
|
|
20
|
+
# expand empty BY DESIGN. `-c` stays last: make appends the command to it.
|
|
21
|
+
.SHELLFLAGS := -o pipefail -c
|
|
12
22
|
|
|
13
23
|
ARGS ?=
|
|
14
24
|
PYTEST_ARGS ?=
|
|
@@ -31,7 +41,7 @@ endif
|
|
|
31
41
|
.PHONY: help build install run doctor test test-e2e cover fmt fmt-check lint \
|
|
32
42
|
typecheck vulncheck sast secret-scan licenses samples-scratch samples-check \
|
|
33
43
|
samples-gate engines-gate licenses-check artifacts-check gate-timing \
|
|
34
|
-
docs-gate ci clean
|
|
44
|
+
docs-gate shim-reap ci clean
|
|
35
45
|
|
|
36
46
|
help: ## Show this help
|
|
37
47
|
@grep -hE '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
|
|
@@ -40,14 +50,14 @@ help: ## Show this help
|
|
|
40
50
|
build: ## Build the sdist and wheel into dist/
|
|
41
51
|
uv build
|
|
42
52
|
|
|
43
|
-
install: ## Install the CLI onto your PATH as `
|
|
53
|
+
install: ## Install the CLI onto your PATH as `pdftooling`
|
|
44
54
|
uv tool install --force .
|
|
45
55
|
|
|
46
56
|
run: ## Run the CLI: make run ARGS="version -o json"
|
|
47
|
-
uv run
|
|
57
|
+
uv run pdftooling $(ARGS)
|
|
48
58
|
|
|
49
59
|
doctor: ## Report which engines resolved (arrives with the engine-ports work; exits 2 until then)
|
|
50
|
-
uv run
|
|
60
|
+
uv run pdftooling doctor
|
|
51
61
|
|
|
52
62
|
test: ## Run the test suite
|
|
53
63
|
$(UV_RUN) pytest $(PYTEST_ARGS)
|
|
@@ -94,7 +104,7 @@ cover: ## Run the suite under coverage against the project's floor
|
|
|
94
104
|
echo "$$$$" > "$$lock/pid"; \
|
|
95
105
|
trap 'rm -rf "$$lock"' EXIT INT TERM; \
|
|
96
106
|
COVERAGE_FILE=$(CURDIR)/.coverage \
|
|
97
|
-
$(UV_RUN) pytest --cov=
|
|
107
|
+
$(UV_RUN) pytest --cov=pdf_tooling --cov-report=term-missing --cov-fail-under=85 $(PYTEST_ARGS)
|
|
98
108
|
|
|
99
109
|
fmt: ## Format the tree
|
|
100
110
|
uv run ruff format .
|
|
@@ -327,14 +337,28 @@ export DOCS_GATE_ENGINES_ASSERT
|
|
|
327
337
|
# measured wall clock ever argues for inclusion, that is a number for the PM,
|
|
328
338
|
# not a decision taken in this recipe (decision.md §5 R-1).
|
|
329
339
|
#
|
|
330
|
-
# THE ARMS THAT CANNOT ALWAYS RUN SAY SO.
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
#
|
|
340
|
+
# THE ARMS THAT CANNOT ALWAYS RUN SAY SO. MEASURED at PDF-34 HEAD by running
|
|
341
|
+
# the thing in each condition, because the previous figures here ("two" and
|
|
342
|
+
# "four") were BOTH wrong and nothing checked them -- a stale count in the
|
|
343
|
+
# comment above a skip census is the same defect this target exists to end:
|
|
344
|
+
# FIVE arms read the maintainer's planning tree (`PDF_TOOLKIT_PLANNING_DIR`);
|
|
345
|
+
# recipe: PDF_TOOLKIT_PLANNING_DIR=/nonexistent make docs-gate
|
|
346
|
+
# ELEVEN arms read git history deeper than a shallow checkout, in TWO classes
|
|
347
|
+
# -- 10 against MINIMUM_HISTORY_DEPTH, plus 1 that cannot check a depth
|
|
348
|
+
# precondition against a checkout never given the depth to check it;
|
|
349
|
+
# recipe: run the three arm-3 files inside `git clone --depth 1`.
|
|
350
|
+
# `ci.yml`'s `test` job has neither, so in CI all SIXTEEN skip -- and a skipped
|
|
351
|
+
# arm is NEVER agreement. `-rs` prints every skip reason and the epilogue below
|
|
352
|
+
# repeats the count, so "it ran" and "it could not run" can never be read as
|
|
353
|
+
# the same green (X-153).
|
|
354
|
+
#
|
|
355
|
+
# PDF-34 D3: `DOCS_GATE_STRICT=1` turns that sentence into an exit code. Unset
|
|
356
|
+
# (the default) is byte-for-byte today's behaviour -- skips are printed and
|
|
357
|
+
# counted and the target still exits 0 -- because the five planning arms
|
|
358
|
+
# LEGITIMATELY cannot run in CI, which checks out this repository alone. The
|
|
359
|
+
# cadence that CAN see both trees runs it strict, where zero arms may skip.
|
|
336
360
|
define DOCS_GATE_EPILOGUE
|
|
337
|
-
import re, sys
|
|
361
|
+
import os, re, sys
|
|
338
362
|
text = sys.stdin.read()
|
|
339
363
|
sys.stdout.write(text)
|
|
340
364
|
# pytest AGGREGATES identical skip reasons as `SKIPPED [N] <reason>`, so
|
|
@@ -352,6 +376,21 @@ if skipped:
|
|
|
352
376
|
print(" A SKIPPED ARM IS NOT AGREEMENT. Re-run with PDF_TOOLKIT_PLANNING_DIR")
|
|
353
377
|
print(" pointed at the planning tree, and in a full (non-shallow) clone, to")
|
|
354
378
|
print(" turn these into real comparisons. `make ci` does not run this target.")
|
|
379
|
+
# PDF-34 D3. For as long as this epilogue has existed it has PRINTED
|
|
380
|
+
# "A SKIPPED ARM IS NOT AGREEMENT" and then exited 0 anyway -- the rule stated
|
|
381
|
+
# in prose, by the gate, about itself, with nothing enforcing it. Strict mode
|
|
382
|
+
# is that sentence as an exit code, and it is opt-in for one measured reason:
|
|
383
|
+
# CI checks out this repository alone, so the five planning arms skip there for
|
|
384
|
+
# a reason that is not a defect, and a strict CI run would fail honestly-shaped
|
|
385
|
+
# but wrongly. The Tier-2 cadence runs where BOTH trees exist, so zero arms may
|
|
386
|
+
# skip and any skip is real news.
|
|
387
|
+
if skipped and os.environ.get("DOCS_GATE_STRICT", "").strip() not in ("", "0", "false", "no"):
|
|
388
|
+
print("")
|
|
389
|
+
print(" DOCS_GATE_STRICT=1: %d skipped arm(s) in %d class(es) is a FAILURE."
|
|
390
|
+
% (arms, len(skipped)))
|
|
391
|
+
print(" A SKIPPED ARM IS NOT AGREEMENT -- and under this posture that is an")
|
|
392
|
+
print(" exit code, not a paragraph. Classes above name what could not run.")
|
|
393
|
+
sys.exit(1)
|
|
355
394
|
endef
|
|
356
395
|
export DOCS_GATE_EPILOGUE
|
|
357
396
|
|
|
@@ -374,3 +413,23 @@ ci: fmt-check lint typecheck cover licenses sast vulncheck ## Run the full local
|
|
|
374
413
|
clean: ## Remove build, cache and coverage artefacts
|
|
375
414
|
rm -rf dist build .pytest_cache .ruff_cache .mypy_cache htmlcov .coverage coverage.xml .scratch .make-cover.lock
|
|
376
415
|
rm -f .coverage.*
|
|
416
|
+
|
|
417
|
+
# PDF-46 D5. The engine-hiding shim used to leak one directory per pytest
|
|
418
|
+
# process -- and under the project's default parallel mode, one per xdist
|
|
419
|
+
# worker plus one for the controller, which is the multiplier that made this
|
|
420
|
+
# worth fixing. `tests/conftest.py` now reclaims its own at interpreter exit,
|
|
421
|
+
# which fixes the leak going FORWARD and deliberately removes nothing that was
|
|
422
|
+
# already there: the standing residue is the sentinel's recorded evidence, and
|
|
423
|
+
# an engineer deleting another agent's evidence mid-cycle is the same class of
|
|
424
|
+
# act as editing the ledger.
|
|
425
|
+
#
|
|
426
|
+
# So this target LISTS by default and the destructive path needs a word typed.
|
|
427
|
+
# It is a prerequisite of NOTHING -- not `clean`, not `test`, not `cover`, not
|
|
428
|
+
# `ci`, not `engines-gate`, not `docs-gate` -- and that is asserted by parsing
|
|
429
|
+
# this file (tests/test_engine_hiding_shim.py), not trusted. A reaper that had
|
|
430
|
+
# quietly become part of `clean` or `ci` would delete a concurrent session's
|
|
431
|
+
# LIVE shim, and the symptom would look like a flake.
|
|
432
|
+
SHIM_REAP_ARGS ?=
|
|
433
|
+
|
|
434
|
+
shim-reap: ## List stale engine-hiding shim directories left by earlier runs; removes only with CONFIRM=1
|
|
435
|
+
$(UV_RUN) python scripts/reap_shims.py $(if $(CONFIRM),--confirm) $(SHIM_REAP_ARGS)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pdf-tooling
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: One safe, permissively-licensed command-line tool for the common PDF chores.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ArmandoHerra/pdf-tooling
|
|
6
6
|
Project-URL: Repository, https://github.com/ArmandoHerra/pdf-tooling
|
|
@@ -38,9 +38,9 @@ Provides-Extra: html
|
|
|
38
38
|
Requires-Dist: weasyprint<70,>=69.0; extra == 'html'
|
|
39
39
|
Description-Content-Type: text/markdown
|
|
40
40
|
|
|
41
|
-
# pdf-
|
|
41
|
+
# pdf-tooling
|
|
42
42
|
|
|
43
|
-
An Apache-2.0 PDF toolkit CLI in Python. One safe command-line tool (`
|
|
43
|
+
An Apache-2.0 PDF toolkit CLI in Python. One safe command-line tool (`pdftooling`) for the common PDF chores — `merge`, `split`, `extract`, `delete`, `rotate`, `reorder`, `rasterize`, `compose`, `create`, `text`, `tables`, `compress`, `repair`, `linearize`, `encrypt`, `decrypt`, `permissions`, `meta`, `watermark`, `stamp`, `ocr` and `convert`, plus `doctor`, `info` and `version` — built on a permissively licensed engine stack (pypdf, pypdfium2, reportlab, pikepdf, pdfplumber, Tesseract, LibreOffice) with nothing AGPL or GPL on the call graph.
|
|
44
44
|
|
|
45
45
|
Safety is first-class: a global `--dry-run`, no-clobber by default, atomic write-to-temp-then-rename, and inputs that are never mutated unless you ask for `--in-place`.
|
|
46
46
|
|
|
@@ -48,12 +48,6 @@ Safety is first-class: a global `--dry-run`, no-clobber by default, atomic write
|
|
|
48
48
|
|
|
49
49
|
- **Website:** https://armandoherra.github.io/pdf-tooling/ — the public landing page (source in `website/`).
|
|
50
50
|
|
|
51
|
-
## This is not `pdftk`
|
|
52
|
-
|
|
53
|
-
`pdf-toolkit` shares no code with `pdftk`, is not a fork of it, and is not a drop-in replacement for it. `pdftk` is GPL-licensed; this project is **Apache-2.0**, and its engine policy forbids anything under AGPL, GPL or LGPL from appearing as an import, an optional extra, or a `subprocess` fallback. The console script is named `pdftoolkit` precisely so the two cannot be confused on your `PATH`.
|
|
54
|
-
|
|
55
|
-
The PyPI **distribution** to install is **`pdf-tooling`**. The distribution named `pdftoolkit` on PyPI is an unrelated GPL-3.0 project and is not this software. The names this project owns, and which of them moved, are the contract in [Naming](#naming).
|
|
56
|
-
|
|
57
51
|
## Naming
|
|
58
52
|
|
|
59
53
|
The names below are this project's contract. They differ deliberately, and a reader
|
|
@@ -62,31 +56,36 @@ citing any of them should cite the table rather than the prose around it.
|
|
|
62
56
|
| Kind | Name |
|
|
63
57
|
|---|---|
|
|
64
58
|
| PyPI distribution | `pdf-tooling` |
|
|
65
|
-
| Repository | `
|
|
66
|
-
| Import package | `
|
|
67
|
-
| Console
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
59
|
+
| Repository | `pdf-tooling` |
|
|
60
|
+
| Import package | `pdf_tooling` |
|
|
61
|
+
| Console script | `pdftooling` |
|
|
62
|
+
| Aliases | `pdf-tooling`, plus deprecated `pdftoolkit` and `pdf-toolkit` until `v1.0.0` |
|
|
63
|
+
|
|
64
|
+
**Why the names differ.** The PyPI distribution is `pdf-tooling` because `pdf-toolkit`
|
|
65
|
+
sits too close to names already on PyPI, and the distribution called `pdftoolkit` there
|
|
66
|
+
is an unrelated GPL-3.0 project that is not this software. The repository followed the
|
|
67
|
+
distribution; the import package and the console script followed it in turn, once the
|
|
68
|
+
deprecation window below made moving a published surface safe.
|
|
69
|
+
|
|
70
|
+
**The deprecation window.** `pdftoolkit` and `pdf-toolkit` remain installed and fully
|
|
71
|
+
functional through `v1.0.0` — same behaviour, same exit codes — each printing one line
|
|
72
|
+
on stderr naming `pdftooling` as the replacement. The distribution named `pdftoolkit` on
|
|
73
|
+
PyPI is an unrelated GPL-3.0 project; this project's deprecated `pdftoolkit` console
|
|
74
|
+
script — and this warning with it — is removed at `v1.0.0`.
|
|
78
75
|
|
|
79
76
|
**Release history, so the install lines above can be read against it.** `v0.1.0` was
|
|
80
77
|
git-install-only and was never published to PyPI under either name; `v0.1.1` is the
|
|
81
78
|
first published release, as `pdf-tooling`; `v0.2.0` is the first published under the
|
|
82
|
-
renamed repository.
|
|
79
|
+
renamed repository; `v0.3.0` is the first release to ship the renamed `pdftooling`
|
|
80
|
+
console script and `pdf_tooling` import package, with `pdftoolkit`/`pdf-toolkit`/
|
|
81
|
+
`pdf_toolkit` deprecated behind it.
|
|
83
82
|
|
|
84
83
|
## Getting Started
|
|
85
84
|
|
|
86
85
|
```bash
|
|
87
86
|
uv tool install pdf-tooling
|
|
88
87
|
# or: pip install pdf-tooling
|
|
89
|
-
|
|
88
|
+
pdftooling --help
|
|
90
89
|
```
|
|
91
90
|
|
|
92
91
|
### From source
|
|
@@ -96,7 +95,7 @@ pdftoolkit --help
|
|
|
96
95
|
git clone https://github.com/ArmandoHerra/pdf-tooling.git
|
|
97
96
|
cd pdf-tooling
|
|
98
97
|
uv sync
|
|
99
|
-
uv run
|
|
98
|
+
uv run pdftooling --help
|
|
100
99
|
```
|
|
101
100
|
|
|
102
101
|
`uv sync` installs the runtime stack *and* the development tooling, so there is no separate bootstrap step.
|
|
@@ -120,18 +119,18 @@ The complete top-level roster, by family:
|
|
|
120
119
|
|
|
121
120
|
That table is asserted against the live command tree by set-inclusion, so a verb
|
|
122
121
|
shipped tomorrow turns the check red with no author action — but
|
|
123
|
-
`uv run
|
|
122
|
+
`uv run pdftooling --help` remains the authoritative list.
|
|
124
123
|
|
|
125
124
|
```bash
|
|
126
|
-
uv run
|
|
127
|
-
uv run
|
|
128
|
-
uv run
|
|
129
|
-
uv run
|
|
130
|
-
uv run
|
|
131
|
-
uv run
|
|
125
|
+
uv run pdftooling --help # the full verb tree; always the authoritative list
|
|
126
|
+
uv run pdftooling doctor # which engines resolved, and how
|
|
127
|
+
uv run pdftooling merge a.pdf b.pdf -O merged.pdf
|
|
128
|
+
uv run pdftooling rotate report.pdf --pages 2-4 --angle 90 -O rotated.pdf
|
|
129
|
+
uv run pdftooling compress report.pdf -O small.pdf
|
|
130
|
+
uv run pdftooling --version # tool, Python and engine versions on one line
|
|
132
131
|
```
|
|
133
132
|
|
|
134
|
-
`uv run
|
|
133
|
+
`uv run pdftooling --help` is the authoritative list of what is actually available at any moment — if a verb is not printed there, it does not exist yet.
|
|
135
134
|
|
|
136
135
|
## Output contract
|
|
137
136
|
|
|
@@ -145,9 +144,42 @@ Rendered payloads go to **stdout**; diagnostics, warnings and progress go to **s
|
|
|
145
144
|
|
|
146
145
|
Errors are the one deliberate asymmetry: with `-o table` an error is a one-line `error: …` on stderr, but with `-o json`/`-o ndjson` it is an object on **stdout**, so a machine consumer reading stdout never has to also read stderr to learn that the run failed. That holds for **every** failure you can reach, an unknown flag and a missing argument included: those are usage errors (exit 2) carrying the same envelope, not a human `Usage:` block.
|
|
147
146
|
|
|
148
|
-
**A command group does not take the global block.** `meta` groups `meta get` and `meta set`, and the global flags are declared at the root and on every verb, never on a group — so `
|
|
147
|
+
**A command group does not take the global block.** `meta` groups `meta get` and `meta set`, and the global flags are declared at the root and on every verb, never on a group — so `pdftooling meta -o json` is a usage error (exit 2) rather than a run. It names the two positions that do work: `pdftooling -o json meta get FILE` (before the group) and `pdftooling meta get FILE -o json` (after the subcommand).
|
|
148
|
+
|
|
149
|
+
**Global flags with no command at all** — `pdftooling -o json` — are an incomplete invocation and exit 2 as well, pointing at `--help`. `pdftooling` on its own, with no arguments, still prints help and exits 0.
|
|
150
|
+
|
|
151
|
+
### The collection key
|
|
152
|
+
|
|
153
|
+
Every `-o json` envelope that carries a collection of rows carries them under `items`. `doctor` and `info` additionally publish that same list under a name of their own, and both names are frozen: `doctor`'s `ports` is pinned by the plan's own `pdftooling doctor -o json | jq '.ports[] | select(.available == false)'` example, and `info`'s `documents` is its shipped shape. Neither is being renamed. The alias was added beside them so that a single `jq` expression works against every verb.
|
|
154
|
+
|
|
155
|
+
| Verb | Primary key | Universal alias | Relationship |
|
|
156
|
+
|---|---|---|---|
|
|
157
|
+
| every verb that returns an operation result | `items` | `items` | the same list |
|
|
158
|
+
| `info` | `documents` | `items` | the same list |
|
|
159
|
+
| `doctor` | `ports` | `items` | the same list |
|
|
160
|
+
|
|
161
|
+
`meta get` publishes a single document's report rather than a collection, so it has no row collection and names none.
|
|
162
|
+
|
|
163
|
+
Every `-o json` envelope also carries `exit_code`, `warnings` and `duration_ms`, on every verb. `warnings` is a list and is `[]` when empty — never `null`, never absent. `exit_code` is the code the process exits with for the run the envelope describes, and `duration_ms` is `0` on the verbs whose output must be reproducible byte-for-byte.
|
|
164
|
+
|
|
165
|
+
### A batch reports every input, and never denies a file it wrote
|
|
166
|
+
|
|
167
|
+
A multi-input run writing into `--out-dir` records **every input by name, in command-line order**, in the payload's collection — the inputs that succeeded and the input that failed alike. A failing input is recorded, the run continues, and the run exits `1` at the end; each row carries its own `ok`, `exit_code` and `message`. A row that succeeded names its artifact in `output`, and that path is on disk.
|
|
149
168
|
|
|
150
|
-
**
|
|
169
|
+
**This is a behaviour change on a failure path, inside the pre-`1.0.0` window.** A multi-input `--out-dir` failure used to emit the error envelope — `{"schema_version": 1, "error": {…, "path": null}}`, with no collection at all — in place of the operation envelope, so the input that succeeded went unreported and the input that failed went unnamed even while its sibling's artifact sat on disk. A script that parsed such a failure by reading `payload["error"]` now finds `payload["items"]` instead. **`schema_version` stays `1`**: no key is renamed, removed, retyped or repurposed, and the error envelope itself is unchanged.
|
|
170
|
+
|
|
171
|
+
Failures that are properties of the invocation rather than of an input stay run-scoped and keep the error envelope: a usage error, a directory where a file was expected, a nonexistent input, a missing engine, and the refusals the safety gate raises. A missing engine fails identically for every input, so it stays a single accurate diagnosis carrying its own exit code rather than becoming a row per input that also changes what the process exits with. A single-input run likewise reports that item's own code, which is what keeps the per-input codes distinguishable at all.
|
|
172
|
+
|
|
173
|
+
### `schema_version` is `1`, and this is what would move it
|
|
174
|
+
|
|
175
|
+
`schema_version` is `1` and stays `1` for as long as the envelope changes only by **addition**. Adding a key never moves it; nor does adding a verb, an item field, or an output format. It increments on the first change a consumer that reads keys by name cannot survive, which is exactly this list:
|
|
176
|
+
|
|
177
|
+
- a published key is **renamed**;
|
|
178
|
+
- a published key is **removed**;
|
|
179
|
+
- a published key's **type** changes — including `[]` becoming `null`, or a scalar becoming an object;
|
|
180
|
+
- a published key's **meaning** changes while its name and type stay the same.
|
|
181
|
+
|
|
182
|
+
An increment is **coupled to a major version bump**; they move together or not at all.
|
|
151
183
|
|
|
152
184
|
The structured shapes and the exit-code table below are **public API from v1.0.0**. Breaking either requires a major version bump and a `schema_version` increment. Pre-1.0 releases are explicitly still moving.
|
|
153
185
|
|
|
@@ -200,15 +232,36 @@ No secure erasure is claimed, anywhere. A resolved password is held in a buffer
|
|
|
200
232
|
|
|
201
233
|
`encrypt --in-place` needs one more word from you, and it is the one place where the safety default and the security default point in opposite directions. The `.bak` sidecar is a copy of the **original**, so an in-place encryption would leave plaintext sitting next to the ciphertext, silently. So it refuses (exit 5) unless you pass `--no-backup` (keep no plaintext copy) or `-y` (keep it, knowingly).
|
|
202
234
|
|
|
235
|
+
### The permission vocabularies
|
|
236
|
+
|
|
237
|
+
`pdftooling info -o json` and `pdftooling permissions -o json` are **both public output**, and they spell some of their permission tokens differently. That is a documented divergence, not a defect to be tidied: both spellings shipped, both are `schema_version: 1` public API, and renaming either would break a published contract. So the crossing is published here instead. **`--allow` accepts the left column only** — the right column is `info`'s output spelling and is not an input token.
|
|
238
|
+
|
|
239
|
+
| `--allow` / `permissions` | `info` | `ISO 32000-1 Table 22` bit |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `print` | `print` | `3` |
|
|
242
|
+
| `modify` | `modify` | `4` |
|
|
243
|
+
| `copy` | `copy` | `5` |
|
|
244
|
+
| `annotate` | `annotate` | `6` |
|
|
245
|
+
| `forms` | `fill-forms` | `9` |
|
|
246
|
+
| `accessibility` | `extract-accessibility` | `10` |
|
|
247
|
+
| `assemble` | `assemble` | `11` |
|
|
248
|
+
| `print-highres` | `print-high-resolution` | `12` |
|
|
249
|
+
|
|
250
|
+
The tokens spelled identically on both surfaces need no translation; the ones that diverge are the same bit under a different name. Without this table, a consumer that runs both verbs against the same encrypted document sees phantom differences on every file. The table is derived from the source constants by a test, so it cannot rot away from what the tool actually emits.
|
|
251
|
+
|
|
203
252
|
**Permission bits are advisory.** They are a request to the reader, not a lock: only cooperating readers honour them, any reader holding the file may ignore every bit, and a reader that can display a page can extract it. Encryption protects the content; the bits on their own protect nothing. `--allow` takes a comma-separated, repeatable list from `print`, `print-highres`, `copy`, `modify`, `annotate`, `forms`, `assemble`, `accessibility`, plus the exclusive `all` and `none`; omitting it grants nothing. `accessibility` is granted whatever you ask for — PDF 2.0 deprecated that bit and conforming readers always permit it — and `permissions` reports what the document actually grants rather than what was requested. **`print-highres` also grants `print`**: a reader permitted to print at full resolution may obviously print at low resolution, and the format has no spelling for “high but not low”, so asking for the one token grants two. That is the format's behaviour rather than this tool's choice, and it is disclosed at the flag that causes it instead of being left for `permissions` to reveal afterwards.
|
|
204
253
|
|
|
254
|
+
### `--password-file` is global: honoured or refused, never silently ignored
|
|
255
|
+
|
|
256
|
+
`--password-file` (and its resolution siblings — the `PDF_TOOLKIT_PASSWORD` environment variable and the interactive prompt) is declared on **every** verb, because any of them may meet a password-protected input — including the report-only ones. A verb that can open an encrypted document uses the resolved password there, on the SAME first-hit-wins chain `encrypt`/`decrypt` already document above; a verb that structurally cannot use one (it takes no document operand, or it already declares its own dedicated password flag) refuses it up front, at exit **2**, naming the flag rather than accepting it and doing nothing — the same "declared but silently inert" shape this tool refuses for every other global flag (see the safety contract above). There is no hand-maintained list of which verbs fall into which group here: run the verb's own `--help` to see the flag declared, or try it — a verb that cannot honour it says so immediately, before any document is opened.
|
|
257
|
+
|
|
205
258
|
`decrypt` round-trips the **page tree** byte for byte: the decoded content streams, the page dictionaries and every embedded image's raw bytes come back identical. The whole file does not, and nothing here claims it does — `/ID`, `/Encrypt`, the trailer, the cross-reference table and object numbering all legitimately change on any resave.
|
|
206
259
|
|
|
207
260
|
## OCR and Office conversion
|
|
208
261
|
|
|
209
|
-
`ocr` and `convert` are the two verbs that depend on a system binary rather than a Python wheel — the two verbs `
|
|
262
|
+
`ocr` and `convert` are the two verbs that depend on a system binary rather than a Python wheel — the two verbs `pdftooling doctor` can legitimately report as unavailable.
|
|
210
263
|
|
|
211
|
-
`ocr` drives the **tesseract** binary. For every selected page it renders the page, recognises a text-only layer, and overlays that layer on the **original** page object — the page's own image is never re-rendered, and a byte-level check proves the image stream is identical before and after. `--skip-text-pages` leaves a page that already has extractable text untouched (no render, no OCR call). This build ships whatever tessdata language packs the host has installed; `--lang` is validated against exactly that list (`
|
|
264
|
+
`ocr` drives the **tesseract** binary. For every selected page it renders the page, recognises a text-only layer, and overlays that layer on the **original** page object — the page's own image is never re-rendered, and a byte-level check proves the image stream is identical before and after. `--skip-text-pages` leaves a page that already has extractable text untouched (no render, no OCR call). This build ships whatever tessdata language packs the host has installed; `--lang` is validated against exactly that list (`pdftooling doctor`), and a pack that is not installed exits 3 with an install hint naming it. No accuracy or confidence claim is made anywhere in this tool — `ocr` is described here by its engine, not by a quality promise.
|
|
212
265
|
|
|
213
266
|
`convert` drives headless **LibreOffice** (`soffice`) to turn an office document into a PDF. Each invocation gets its own isolated LibreOffice profile directory and converts into a private scratch location first — LibreOffice never writes to the destination directly, and the destination is only touched through this tool's one write chokepoint. An exit 0 from `soffice` having produced no output file is treated as a failure here, not a success, because that is a real and well-known LibreOffice failure mode. `--timeout` bounds one conversion; on expiry the whole process group is killed, so no `soffice.bin` daemon is left running.
|
|
214
267
|
|
|
@@ -238,4 +291,4 @@ If a sweep ever records nothing open, this section still stands and reads *no op
|
|
|
238
291
|
|
|
239
292
|
## License
|
|
240
293
|
|
|
241
|
-
Apache-2.0 — see `LICENSE` and `NOTICE`. `THIRD_PARTY_LICENSES` is generated from the resolved environment and ships in both the sdist and the wheel.
|
|
294
|
+
Apache-2.0 — see `LICENSE` and `NOTICE`. `THIRD_PARTY_LICENSES` is generated from the resolved environment and ships in both the sdist and the wheel. `pdftk` is GPL-licensed; this project is **Apache-2.0**, and its engine policy forbids anything under AGPL, GPL or LGPL from appearing as an import, an optional extra, or a `subprocess` fallback.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# pdf-
|
|
1
|
+
# pdf-tooling
|
|
2
2
|
|
|
3
|
-
An Apache-2.0 PDF toolkit CLI in Python. One safe command-line tool (`
|
|
3
|
+
An Apache-2.0 PDF toolkit CLI in Python. One safe command-line tool (`pdftooling`) for the common PDF chores — `merge`, `split`, `extract`, `delete`, `rotate`, `reorder`, `rasterize`, `compose`, `create`, `text`, `tables`, `compress`, `repair`, `linearize`, `encrypt`, `decrypt`, `permissions`, `meta`, `watermark`, `stamp`, `ocr` and `convert`, plus `doctor`, `info` and `version` — built on a permissively licensed engine stack (pypdf, pypdfium2, reportlab, pikepdf, pdfplumber, Tesseract, LibreOffice) with nothing AGPL or GPL on the call graph.
|
|
4
4
|
|
|
5
5
|
Safety is first-class: a global `--dry-run`, no-clobber by default, atomic write-to-temp-then-rename, and inputs that are never mutated unless you ask for `--in-place`.
|
|
6
6
|
|
|
@@ -8,12 +8,6 @@ Safety is first-class: a global `--dry-run`, no-clobber by default, atomic write
|
|
|
8
8
|
|
|
9
9
|
- **Website:** https://armandoherra.github.io/pdf-tooling/ — the public landing page (source in `website/`).
|
|
10
10
|
|
|
11
|
-
## This is not `pdftk`
|
|
12
|
-
|
|
13
|
-
`pdf-toolkit` shares no code with `pdftk`, is not a fork of it, and is not a drop-in replacement for it. `pdftk` is GPL-licensed; this project is **Apache-2.0**, and its engine policy forbids anything under AGPL, GPL or LGPL from appearing as an import, an optional extra, or a `subprocess` fallback. The console script is named `pdftoolkit` precisely so the two cannot be confused on your `PATH`.
|
|
14
|
-
|
|
15
|
-
The PyPI **distribution** to install is **`pdf-tooling`**. The distribution named `pdftoolkit` on PyPI is an unrelated GPL-3.0 project and is not this software. The names this project owns, and which of them moved, are the contract in [Naming](#naming).
|
|
16
|
-
|
|
17
11
|
## Naming
|
|
18
12
|
|
|
19
13
|
The names below are this project's contract. They differ deliberately, and a reader
|
|
@@ -22,31 +16,36 @@ citing any of them should cite the table rather than the prose around it.
|
|
|
22
16
|
| Kind | Name |
|
|
23
17
|
|---|---|
|
|
24
18
|
| PyPI distribution | `pdf-tooling` |
|
|
25
|
-
| Repository | `
|
|
26
|
-
| Import package | `
|
|
27
|
-
| Console
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
| Repository | `pdf-tooling` |
|
|
20
|
+
| Import package | `pdf_tooling` |
|
|
21
|
+
| Console script | `pdftooling` |
|
|
22
|
+
| Aliases | `pdf-tooling`, plus deprecated `pdftoolkit` and `pdf-toolkit` until `v1.0.0` |
|
|
23
|
+
|
|
24
|
+
**Why the names differ.** The PyPI distribution is `pdf-tooling` because `pdf-toolkit`
|
|
25
|
+
sits too close to names already on PyPI, and the distribution called `pdftoolkit` there
|
|
26
|
+
is an unrelated GPL-3.0 project that is not this software. The repository followed the
|
|
27
|
+
distribution; the import package and the console script followed it in turn, once the
|
|
28
|
+
deprecation window below made moving a published surface safe.
|
|
29
|
+
|
|
30
|
+
**The deprecation window.** `pdftoolkit` and `pdf-toolkit` remain installed and fully
|
|
31
|
+
functional through `v1.0.0` — same behaviour, same exit codes — each printing one line
|
|
32
|
+
on stderr naming `pdftooling` as the replacement. The distribution named `pdftoolkit` on
|
|
33
|
+
PyPI is an unrelated GPL-3.0 project; this project's deprecated `pdftoolkit` console
|
|
34
|
+
script — and this warning with it — is removed at `v1.0.0`.
|
|
38
35
|
|
|
39
36
|
**Release history, so the install lines above can be read against it.** `v0.1.0` was
|
|
40
37
|
git-install-only and was never published to PyPI under either name; `v0.1.1` is the
|
|
41
38
|
first published release, as `pdf-tooling`; `v0.2.0` is the first published under the
|
|
42
|
-
renamed repository.
|
|
39
|
+
renamed repository; `v0.3.0` is the first release to ship the renamed `pdftooling`
|
|
40
|
+
console script and `pdf_tooling` import package, with `pdftoolkit`/`pdf-toolkit`/
|
|
41
|
+
`pdf_toolkit` deprecated behind it.
|
|
43
42
|
|
|
44
43
|
## Getting Started
|
|
45
44
|
|
|
46
45
|
```bash
|
|
47
46
|
uv tool install pdf-tooling
|
|
48
47
|
# or: pip install pdf-tooling
|
|
49
|
-
|
|
48
|
+
pdftooling --help
|
|
50
49
|
```
|
|
51
50
|
|
|
52
51
|
### From source
|
|
@@ -56,7 +55,7 @@ pdftoolkit --help
|
|
|
56
55
|
git clone https://github.com/ArmandoHerra/pdf-tooling.git
|
|
57
56
|
cd pdf-tooling
|
|
58
57
|
uv sync
|
|
59
|
-
uv run
|
|
58
|
+
uv run pdftooling --help
|
|
60
59
|
```
|
|
61
60
|
|
|
62
61
|
`uv sync` installs the runtime stack *and* the development tooling, so there is no separate bootstrap step.
|
|
@@ -80,18 +79,18 @@ The complete top-level roster, by family:
|
|
|
80
79
|
|
|
81
80
|
That table is asserted against the live command tree by set-inclusion, so a verb
|
|
82
81
|
shipped tomorrow turns the check red with no author action — but
|
|
83
|
-
`uv run
|
|
82
|
+
`uv run pdftooling --help` remains the authoritative list.
|
|
84
83
|
|
|
85
84
|
```bash
|
|
86
|
-
uv run
|
|
87
|
-
uv run
|
|
88
|
-
uv run
|
|
89
|
-
uv run
|
|
90
|
-
uv run
|
|
91
|
-
uv run
|
|
85
|
+
uv run pdftooling --help # the full verb tree; always the authoritative list
|
|
86
|
+
uv run pdftooling doctor # which engines resolved, and how
|
|
87
|
+
uv run pdftooling merge a.pdf b.pdf -O merged.pdf
|
|
88
|
+
uv run pdftooling rotate report.pdf --pages 2-4 --angle 90 -O rotated.pdf
|
|
89
|
+
uv run pdftooling compress report.pdf -O small.pdf
|
|
90
|
+
uv run pdftooling --version # tool, Python and engine versions on one line
|
|
92
91
|
```
|
|
93
92
|
|
|
94
|
-
`uv run
|
|
93
|
+
`uv run pdftooling --help` is the authoritative list of what is actually available at any moment — if a verb is not printed there, it does not exist yet.
|
|
95
94
|
|
|
96
95
|
## Output contract
|
|
97
96
|
|
|
@@ -105,9 +104,42 @@ Rendered payloads go to **stdout**; diagnostics, warnings and progress go to **s
|
|
|
105
104
|
|
|
106
105
|
Errors are the one deliberate asymmetry: with `-o table` an error is a one-line `error: …` on stderr, but with `-o json`/`-o ndjson` it is an object on **stdout**, so a machine consumer reading stdout never has to also read stderr to learn that the run failed. That holds for **every** failure you can reach, an unknown flag and a missing argument included: those are usage errors (exit 2) carrying the same envelope, not a human `Usage:` block.
|
|
107
106
|
|
|
108
|
-
**A command group does not take the global block.** `meta` groups `meta get` and `meta set`, and the global flags are declared at the root and on every verb, never on a group — so `
|
|
107
|
+
**A command group does not take the global block.** `meta` groups `meta get` and `meta set`, and the global flags are declared at the root and on every verb, never on a group — so `pdftooling meta -o json` is a usage error (exit 2) rather than a run. It names the two positions that do work: `pdftooling -o json meta get FILE` (before the group) and `pdftooling meta get FILE -o json` (after the subcommand).
|
|
108
|
+
|
|
109
|
+
**Global flags with no command at all** — `pdftooling -o json` — are an incomplete invocation and exit 2 as well, pointing at `--help`. `pdftooling` on its own, with no arguments, still prints help and exits 0.
|
|
110
|
+
|
|
111
|
+
### The collection key
|
|
112
|
+
|
|
113
|
+
Every `-o json` envelope that carries a collection of rows carries them under `items`. `doctor` and `info` additionally publish that same list under a name of their own, and both names are frozen: `doctor`'s `ports` is pinned by the plan's own `pdftooling doctor -o json | jq '.ports[] | select(.available == false)'` example, and `info`'s `documents` is its shipped shape. Neither is being renamed. The alias was added beside them so that a single `jq` expression works against every verb.
|
|
114
|
+
|
|
115
|
+
| Verb | Primary key | Universal alias | Relationship |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| every verb that returns an operation result | `items` | `items` | the same list |
|
|
118
|
+
| `info` | `documents` | `items` | the same list |
|
|
119
|
+
| `doctor` | `ports` | `items` | the same list |
|
|
120
|
+
|
|
121
|
+
`meta get` publishes a single document's report rather than a collection, so it has no row collection and names none.
|
|
122
|
+
|
|
123
|
+
Every `-o json` envelope also carries `exit_code`, `warnings` and `duration_ms`, on every verb. `warnings` is a list and is `[]` when empty — never `null`, never absent. `exit_code` is the code the process exits with for the run the envelope describes, and `duration_ms` is `0` on the verbs whose output must be reproducible byte-for-byte.
|
|
124
|
+
|
|
125
|
+
### A batch reports every input, and never denies a file it wrote
|
|
126
|
+
|
|
127
|
+
A multi-input run writing into `--out-dir` records **every input by name, in command-line order**, in the payload's collection — the inputs that succeeded and the input that failed alike. A failing input is recorded, the run continues, and the run exits `1` at the end; each row carries its own `ok`, `exit_code` and `message`. A row that succeeded names its artifact in `output`, and that path is on disk.
|
|
109
128
|
|
|
110
|
-
**
|
|
129
|
+
**This is a behaviour change on a failure path, inside the pre-`1.0.0` window.** A multi-input `--out-dir` failure used to emit the error envelope — `{"schema_version": 1, "error": {…, "path": null}}`, with no collection at all — in place of the operation envelope, so the input that succeeded went unreported and the input that failed went unnamed even while its sibling's artifact sat on disk. A script that parsed such a failure by reading `payload["error"]` now finds `payload["items"]` instead. **`schema_version` stays `1`**: no key is renamed, removed, retyped or repurposed, and the error envelope itself is unchanged.
|
|
130
|
+
|
|
131
|
+
Failures that are properties of the invocation rather than of an input stay run-scoped and keep the error envelope: a usage error, a directory where a file was expected, a nonexistent input, a missing engine, and the refusals the safety gate raises. A missing engine fails identically for every input, so it stays a single accurate diagnosis carrying its own exit code rather than becoming a row per input that also changes what the process exits with. A single-input run likewise reports that item's own code, which is what keeps the per-input codes distinguishable at all.
|
|
132
|
+
|
|
133
|
+
### `schema_version` is `1`, and this is what would move it
|
|
134
|
+
|
|
135
|
+
`schema_version` is `1` and stays `1` for as long as the envelope changes only by **addition**. Adding a key never moves it; nor does adding a verb, an item field, or an output format. It increments on the first change a consumer that reads keys by name cannot survive, which is exactly this list:
|
|
136
|
+
|
|
137
|
+
- a published key is **renamed**;
|
|
138
|
+
- a published key is **removed**;
|
|
139
|
+
- a published key's **type** changes — including `[]` becoming `null`, or a scalar becoming an object;
|
|
140
|
+
- a published key's **meaning** changes while its name and type stay the same.
|
|
141
|
+
|
|
142
|
+
An increment is **coupled to a major version bump**; they move together or not at all.
|
|
111
143
|
|
|
112
144
|
The structured shapes and the exit-code table below are **public API from v1.0.0**. Breaking either requires a major version bump and a `schema_version` increment. Pre-1.0 releases are explicitly still moving.
|
|
113
145
|
|
|
@@ -160,15 +192,36 @@ No secure erasure is claimed, anywhere. A resolved password is held in a buffer
|
|
|
160
192
|
|
|
161
193
|
`encrypt --in-place` needs one more word from you, and it is the one place where the safety default and the security default point in opposite directions. The `.bak` sidecar is a copy of the **original**, so an in-place encryption would leave plaintext sitting next to the ciphertext, silently. So it refuses (exit 5) unless you pass `--no-backup` (keep no plaintext copy) or `-y` (keep it, knowingly).
|
|
162
194
|
|
|
195
|
+
### The permission vocabularies
|
|
196
|
+
|
|
197
|
+
`pdftooling info -o json` and `pdftooling permissions -o json` are **both public output**, and they spell some of their permission tokens differently. That is a documented divergence, not a defect to be tidied: both spellings shipped, both are `schema_version: 1` public API, and renaming either would break a published contract. So the crossing is published here instead. **`--allow` accepts the left column only** — the right column is `info`'s output spelling and is not an input token.
|
|
198
|
+
|
|
199
|
+
| `--allow` / `permissions` | `info` | `ISO 32000-1 Table 22` bit |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| `print` | `print` | `3` |
|
|
202
|
+
| `modify` | `modify` | `4` |
|
|
203
|
+
| `copy` | `copy` | `5` |
|
|
204
|
+
| `annotate` | `annotate` | `6` |
|
|
205
|
+
| `forms` | `fill-forms` | `9` |
|
|
206
|
+
| `accessibility` | `extract-accessibility` | `10` |
|
|
207
|
+
| `assemble` | `assemble` | `11` |
|
|
208
|
+
| `print-highres` | `print-high-resolution` | `12` |
|
|
209
|
+
|
|
210
|
+
The tokens spelled identically on both surfaces need no translation; the ones that diverge are the same bit under a different name. Without this table, a consumer that runs both verbs against the same encrypted document sees phantom differences on every file. The table is derived from the source constants by a test, so it cannot rot away from what the tool actually emits.
|
|
211
|
+
|
|
163
212
|
**Permission bits are advisory.** They are a request to the reader, not a lock: only cooperating readers honour them, any reader holding the file may ignore every bit, and a reader that can display a page can extract it. Encryption protects the content; the bits on their own protect nothing. `--allow` takes a comma-separated, repeatable list from `print`, `print-highres`, `copy`, `modify`, `annotate`, `forms`, `assemble`, `accessibility`, plus the exclusive `all` and `none`; omitting it grants nothing. `accessibility` is granted whatever you ask for — PDF 2.0 deprecated that bit and conforming readers always permit it — and `permissions` reports what the document actually grants rather than what was requested. **`print-highres` also grants `print`**: a reader permitted to print at full resolution may obviously print at low resolution, and the format has no spelling for “high but not low”, so asking for the one token grants two. That is the format's behaviour rather than this tool's choice, and it is disclosed at the flag that causes it instead of being left for `permissions` to reveal afterwards.
|
|
164
213
|
|
|
214
|
+
### `--password-file` is global: honoured or refused, never silently ignored
|
|
215
|
+
|
|
216
|
+
`--password-file` (and its resolution siblings — the `PDF_TOOLKIT_PASSWORD` environment variable and the interactive prompt) is declared on **every** verb, because any of them may meet a password-protected input — including the report-only ones. A verb that can open an encrypted document uses the resolved password there, on the SAME first-hit-wins chain `encrypt`/`decrypt` already document above; a verb that structurally cannot use one (it takes no document operand, or it already declares its own dedicated password flag) refuses it up front, at exit **2**, naming the flag rather than accepting it and doing nothing — the same "declared but silently inert" shape this tool refuses for every other global flag (see the safety contract above). There is no hand-maintained list of which verbs fall into which group here: run the verb's own `--help` to see the flag declared, or try it — a verb that cannot honour it says so immediately, before any document is opened.
|
|
217
|
+
|
|
165
218
|
`decrypt` round-trips the **page tree** byte for byte: the decoded content streams, the page dictionaries and every embedded image's raw bytes come back identical. The whole file does not, and nothing here claims it does — `/ID`, `/Encrypt`, the trailer, the cross-reference table and object numbering all legitimately change on any resave.
|
|
166
219
|
|
|
167
220
|
## OCR and Office conversion
|
|
168
221
|
|
|
169
|
-
`ocr` and `convert` are the two verbs that depend on a system binary rather than a Python wheel — the two verbs `
|
|
222
|
+
`ocr` and `convert` are the two verbs that depend on a system binary rather than a Python wheel — the two verbs `pdftooling doctor` can legitimately report as unavailable.
|
|
170
223
|
|
|
171
|
-
`ocr` drives the **tesseract** binary. For every selected page it renders the page, recognises a text-only layer, and overlays that layer on the **original** page object — the page's own image is never re-rendered, and a byte-level check proves the image stream is identical before and after. `--skip-text-pages` leaves a page that already has extractable text untouched (no render, no OCR call). This build ships whatever tessdata language packs the host has installed; `--lang` is validated against exactly that list (`
|
|
224
|
+
`ocr` drives the **tesseract** binary. For every selected page it renders the page, recognises a text-only layer, and overlays that layer on the **original** page object — the page's own image is never re-rendered, and a byte-level check proves the image stream is identical before and after. `--skip-text-pages` leaves a page that already has extractable text untouched (no render, no OCR call). This build ships whatever tessdata language packs the host has installed; `--lang` is validated against exactly that list (`pdftooling doctor`), and a pack that is not installed exits 3 with an install hint naming it. No accuracy or confidence claim is made anywhere in this tool — `ocr` is described here by its engine, not by a quality promise.
|
|
172
225
|
|
|
173
226
|
`convert` drives headless **LibreOffice** (`soffice`) to turn an office document into a PDF. Each invocation gets its own isolated LibreOffice profile directory and converts into a private scratch location first — LibreOffice never writes to the destination directly, and the destination is only touched through this tool's one write chokepoint. An exit 0 from `soffice` having produced no output file is treated as a failure here, not a success, because that is a real and well-known LibreOffice failure mode. `--timeout` bounds one conversion; on expiry the whole process group is killed, so no `soffice.bin` daemon is left running.
|
|
174
227
|
|
|
@@ -198,4 +251,4 @@ If a sweep ever records nothing open, this section still stands and reads *no op
|
|
|
198
251
|
|
|
199
252
|
## License
|
|
200
253
|
|
|
201
|
-
Apache-2.0 — see `LICENSE` and `NOTICE`. `THIRD_PARTY_LICENSES` is generated from the resolved environment and ships in both the sdist and the wheel.
|
|
254
|
+
Apache-2.0 — see `LICENSE` and `NOTICE`. `THIRD_PARTY_LICENSES` is generated from the resolved environment and ships in both the sdist and the wheel. `pdftk` is GPL-licensed; this project is **Apache-2.0**, and its engine policy forbids anything under AGPL, GPL or LGPL from appearing as an import, an optional extra, or a `subprocess` fallback.
|