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.
Files changed (222) hide show
  1. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/Makefile +72 -13
  2. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/NOTICE +1 -1
  3. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/PKG-INFO +91 -38
  4. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/README.md +90 -37
  5. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/TESTING.md +222 -10
  6. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/THIRD_PARTY_LICENSES +2 -2
  7. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/changelog.md +222 -0
  8. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/pyproject.toml +10 -5
  9. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/scripts/assert_skips.py +19 -0
  10. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/scripts/licenses.py +3 -3
  11. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/scripts/measure_gate.py +11 -11
  12. pdf_tooling-0.3.0/scripts/reap_shims.py +217 -0
  13. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/__init__.py +5 -4
  14. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/__main__.py +2 -2
  15. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/pdfium_raster.py +35 -6
  16. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/pdfium_text.py +39 -7
  17. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/pdfplumber_text.py +53 -14
  18. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/pikepdf_structure.py +83 -35
  19. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/pypdf_structure.py +103 -25
  20. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/reportlab_compose.py +3 -3
  21. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/soffice_office.py +2 -2
  22. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/subprocess_util.py +5 -5
  23. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/tesseract_ocr.py +2 -2
  24. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_compose.py +8 -8
  25. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_compress.py +30 -10
  26. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_create.py +11 -11
  27. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_decrypt.py +7 -7
  28. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_delete.py +20 -6
  29. pdf_tooling-0.3.0/src/pdf_tooling/cli/cmd_doctor.py +185 -0
  30. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_encrypt.py +11 -10
  31. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_extract.py +19 -5
  32. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_info.py +75 -22
  33. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_linearize.py +21 -6
  34. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_merge.py +23 -8
  35. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_meta_get.py +25 -8
  36. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_meta_set.py +21 -6
  37. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_ocr.py +30 -10
  38. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_office.py +15 -9
  39. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_permissions.py +6 -5
  40. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_rasterize.py +29 -16
  41. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_reorder.py +20 -6
  42. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_repair.py +21 -6
  43. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_rotate.py +20 -6
  44. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_split.py +20 -6
  45. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_stamp.py +22 -7
  46. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_tables.py +20 -7
  47. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_text.py +22 -9
  48. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_version.py +4 -4
  49. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_watermark.py +22 -7
  50. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/common.py +261 -19
  51. pdf_tooling-0.3.0/src/pdf_tooling/cli/deprecated.py +52 -0
  52. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/main.py +10 -9
  53. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/password.py +16 -12
  54. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/errors.py +67 -6
  55. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/models.py +35 -1
  56. pdf_tooling-0.3.0/src/pdf_tooling/ops/batch.py +315 -0
  57. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/compose.py +75 -15
  58. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/crypto.py +29 -70
  59. pdf_tooling-0.3.0/src/pdf_tooling/ops/document_password.py +211 -0
  60. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/inspect.py +40 -16
  61. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/merge.py +91 -75
  62. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/metadata.py +59 -15
  63. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/ocr.py +167 -103
  64. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/office.py +55 -43
  65. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/optimize.py +207 -54
  66. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/overlay.py +184 -139
  67. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/pagerange.py +2 -2
  68. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/pages.py +171 -77
  69. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/procpool.py +87 -2
  70. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/raster.py +120 -29
  71. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/split.py +126 -116
  72. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/textract.py +138 -34
  73. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/output/__init__.py +4 -4
  74. pdf_tooling-0.3.0/src/pdf_tooling/output/json.py +103 -0
  75. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/output/logging.py +10 -10
  76. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/__init__.py +11 -11
  77. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/compose.py +6 -6
  78. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/ocr.py +7 -7
  79. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/office.py +8 -8
  80. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/raster.py +14 -5
  81. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/structure.py +91 -20
  82. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ports/text.py +26 -9
  83. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/__init__.py +6 -6
  84. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/atomic.py +59 -17
  85. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/confirm.py +2 -2
  86. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/naming.py +5 -5
  87. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/paths.py +149 -4
  88. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/policy.py +1 -1
  89. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/atomic_harness.py +4 -4
  90. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/conftest.py +62 -7
  91. pdf_tooling-0.3.0/tests/dryreal.py +160 -0
  92. pdf_tooling-0.3.0/tests/fixtures/provenance/pdf_tooling-0.1.1-py3-none-any.whl.provenance.json +1 -0
  93. pdf_tooling-0.3.0/tests/fixtures/provenance/pdf_tooling-0.1.1.tar.gz.provenance.json +1 -0
  94. pdf_tooling-0.3.0/tests/fixtures/provenance/pdf_tooling-0.2.0-py3-none-any.whl.provenance.json +1 -0
  95. pdf_tooling-0.3.0/tests/fixtures/provenance/pdf_tooling-0.2.0.tar.gz.provenance.json +1 -0
  96. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/fs_snapshot.py +22 -0
  97. pdf_tooling-0.3.0/tests/golden/envelope_keys.json +660 -0
  98. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/golden/meta_get.json +3 -0
  99. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_atomic_crash.py +4 -4
  100. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_compose_roundtrip.py +3 -3
  101. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_cross_filesystem.py +1 -1
  102. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_crypto_roundtrip.py +153 -12
  103. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_ocr.py +128 -11
  104. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_office.py +138 -25
  105. pdf_tooling-0.3.0/tests/integration/test_office_orphan_probe.py +618 -0
  106. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_or7_bulk_destructive.py +3 -3
  107. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_or7_engine_absent.py +3 -3
  108. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_out_dir_planning.py +7 -10
  109. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_overlay_preservation.py +1 -1
  110. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_pages_cli.py +4 -4
  111. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_purity_primitive.py +2 -2
  112. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_rasterize_cli.py +4 -4
  113. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_rasterize_signals.py +29 -21
  114. pdf_tooling-0.3.0/tests/integration/test_render_pool_start_method.py +853 -0
  115. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_split_merge_atomicity.py +5 -5
  116. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_split_merge_roundtrip.py +5 -5
  117. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/registry.py +132 -22
  118. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/samples_guard.py +6 -6
  119. pdf_tooling-0.3.0/tests/seam_sitecustomize/sitecustomize.py +87 -0
  120. pdf_tooling-0.3.0/tests/seams.py +1022 -0
  121. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_assert_skips.py +100 -0
  122. pdf_tooling-0.3.0/tests/test_batch_continuation.py +856 -0
  123. pdf_tooling-0.3.0/tests/test_brand_surfaces.py +383 -0
  124. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_changelog_history.py +23 -11
  125. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_cli_contract.py +904 -14
  126. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_cli_spine.py +176 -40
  127. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_coverage_policy.py +2 -2
  128. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_derived_dimensions.py +6 -6
  129. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_docs_antirot.py +34 -14
  130. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_docstring_pointers.py +5 -5
  131. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_doctor.py +21 -9
  132. pdf_tooling-0.3.0/tests/test_engine_boundary.py +361 -0
  133. pdf_tooling-0.3.0/tests/test_engine_hiding_shim.py +743 -0
  134. pdf_tooling-0.3.0/tests/test_envelope_contract.py +1200 -0
  135. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_gate_budget.py +792 -12
  136. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_gate_parity.py +109 -10
  137. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_honesty_claims.py +189 -2
  138. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_import_boundaries.py +592 -158
  139. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_info.py +22 -18
  140. pdf_tooling-0.3.0/tests/test_license_metadata_policy.py +553 -0
  141. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_license_policy.py +1 -1
  142. pdf_tooling-0.3.0/tests/test_message_hygiene.py +300 -0
  143. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_pagerange.py +12 -12
  144. pdf_tooling-0.3.0/tests/test_password_file_contract.py +738 -0
  145. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_password_leaks.py +115 -44
  146. pdf_tooling-0.3.0/tests/test_pypi_provenance.py +893 -0
  147. pdf_tooling-0.3.0/tests/test_read_seams.py +1185 -0
  148. pdf_tooling-0.3.0/tests/test_rename_completeness.py +503 -0
  149. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_samples.py +42 -42
  150. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_secret_leak_sweeps.py +2 -2
  151. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_testdata.py +4 -4
  152. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_usage_envelope.py +25 -25
  153. pdf_tooling-0.3.0/tests/test_workflow_supply_chain.py +305 -0
  154. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_atomic_writer.py +262 -6
  155. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_compose.py +27 -27
  156. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_confirm.py +7 -7
  157. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_create.py +4 -4
  158. pdf_tooling-0.3.0/tests/unit/test_dryreal.py +524 -0
  159. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_merge.py +6 -6
  160. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_meta_group.py +7 -7
  161. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_metadata.py +7 -7
  162. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_name_template.py +2 -2
  163. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_optimize.py +14 -14
  164. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_output_flags.py +2 -2
  165. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_overlay.py +12 -12
  166. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_pages.py +12 -12
  167. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_password_hint.py +10 -10
  168. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_password_resolution.py +7 -7
  169. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_ports_registry.py +6 -6
  170. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_procpool.py +1 -1
  171. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_raster.py +53 -36
  172. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_registry.py +19 -19
  173. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_safety_paths.py +5 -5
  174. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_secret.py +1 -1
  175. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_split.py +8 -8
  176. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_subprocess_util.py +2 -2
  177. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_tempnames.py +1 -1
  178. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_textract.py +20 -20
  179. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_verb_help_content.py +54 -3
  180. pdf_tooling-0.2.0/src/pdf_toolkit/cli/cmd_doctor.py +0 -136
  181. pdf_tooling-0.2.0/src/pdf_toolkit/output/json.py +0 -50
  182. pdf_tooling-0.2.0/tests/dryreal.py +0 -83
  183. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/.gitignore +0 -0
  184. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/CONTRIBUTING.md +0 -0
  185. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/LICENSE +0 -0
  186. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/scripts/assert_artifacts.py +0 -0
  187. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/scripts/gate_parity.py +0 -0
  188. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/adapters/__init__.py +0 -0
  189. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/__init__.py +0 -0
  190. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/cmd_meta.py +0 -0
  191. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/cli/exit_codes.py +0 -0
  192. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/ops/__init__.py +0 -0
  193. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/output/table.py +0 -0
  194. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/_faults.py +0 -0
  195. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/safety/tempnames.py +0 -0
  196. {pdf_tooling-0.2.0/src/pdf_toolkit → pdf_tooling-0.3.0/src/pdf_tooling}/secret.py +0 -0
  197. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/__init__.py +0 -0
  198. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/_model.py +0 -0
  199. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_01.py +0 -0
  200. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_02.py +0 -0
  201. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_03.py +0 -0
  202. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_04.py +0 -0
  203. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_05.py +0 -0
  204. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_06.py +0 -0
  205. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_09.py +0 -0
  206. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/acceptance/audit_pdf_23.py +0 -0
  207. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/corpus.py +0 -0
  208. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/golden/README.md +0 -0
  209. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/golden/tables_lines.json +0 -0
  210. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/golden/text_layout.json +0 -0
  211. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/helpers/engine_hiding.py +0 -0
  212. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/helpers/pdfstream.py +0 -0
  213. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_optimize_cli.py +0 -0
  214. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_samples_guard_fires.py +0 -0
  215. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_split_merge_cli.py +0 -0
  216. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/integration/test_text_tables_cli.py +0 -0
  217. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/pagetree.py +0 -0
  218. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/pdfium_text.py +0 -0
  219. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_acceptance_audit.py +0 -0
  220. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/test_corpus.py +0 -0
  221. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_golden.py +0 -0
  222. {pdf_tooling-0.2.0 → pdf_tooling-0.3.0}/tests/unit/test_samples_guard.py +0 -0
@@ -1,4 +1,4 @@
1
- # pdf-toolkit — developer entry points.
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 `pdftoolkit`
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 pdftoolkit $(ARGS)
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 pdftoolkit doctor
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=pdf_toolkit --cov-report=term-missing --cov-fail-under=85 $(PYTEST_ARGS)
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. Two of them read the maintainer's
331
- # planning tree (`PDF_TOOLKIT_PLANNING_DIR`) and four read git history deeper
332
- # than a shallow checkout. `ci.yml`'s `test` job has neither, so in CI those
333
- # arms SKIP -- and a skipped arm is NEVER agreement. `-rs` prints every skip
334
- # reason and the epilogue below repeats the count, so "it ran" and "it could
335
- # not run" can never be read as the same green (X-153).
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,4 +1,4 @@
1
- pdf-toolkit
1
+ pdf-tooling
2
2
  Copyright 2026 Armando Herra
3
3
 
4
4
  Licensed under the Apache License, Version 2.0. See LICENSE for the full text.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pdf-tooling
3
- Version: 0.2.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-toolkit
41
+ # pdf-tooling
42
42
 
43
- An Apache-2.0 PDF toolkit CLI in Python. One safe command-line tool (`pdftoolkit`) 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.
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 | `github.com/ArmandoHerra/pdf-tooling` |
66
- | Import package | `pdf_toolkit` |
67
- | Console scripts | `pdftoolkit` (canonical), `pdf-toolkit` (alias) |
68
-
69
- **Why the distribution is not `pdf-toolkit`.** That name sits too close to names already
70
- on PyPI, and the distribution called `pdftoolkit` there is an unrelated GPL-3.0 project
71
- that is not this software. The repository followed the distribution; the import package
72
- and both console scripts did not, because they are published surfaces and moving them
73
- would break an install that already works.
74
-
75
- **Both console scripts are supported.** `pdftoolkit` is canonical and `pdf-toolkit` is a
76
- documented alias; they resolve to the same entry point, and the alias is not deprecated
77
- by this rename.
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
- pdftoolkit --help
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 pdftoolkit --help
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 pdftoolkit --help` remains the authoritative list.
122
+ `uv run pdftooling --help` remains the authoritative list.
124
123
 
125
124
  ```bash
126
- uv run pdftoolkit --help # the full verb tree; always the authoritative list
127
- uv run pdftoolkit doctor # which engines resolved, and how
128
- uv run pdftoolkit merge a.pdf b.pdf -O merged.pdf
129
- uv run pdftoolkit rotate report.pdf --pages 2-4 --angle 90 -O rotated.pdf
130
- uv run pdftoolkit compress report.pdf -O small.pdf
131
- uv run pdftoolkit --version # tool, Python and engine versions on one line
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 pdftoolkit --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.
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 `pdftoolkit meta -o json` is a usage error (exit 2) rather than a run. It names the two positions that do work: `pdftoolkit -o json meta get FILE` (before the group) and `pdftoolkit meta get FILE -o json` (after the subcommand).
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
- **Global flags with no command at all** — `pdftoolkit -o json` — are an incomplete invocation and exit 2 as well, pointing at `--help`. `pdftoolkit` on its own, with no arguments, still prints help and exits 0.
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 `pdftoolkit doctor` can legitimately report as unavailable.
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 (`pdftoolkit 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.
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-toolkit
1
+ # pdf-tooling
2
2
 
3
- An Apache-2.0 PDF toolkit CLI in Python. One safe command-line tool (`pdftoolkit`) 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.
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 | `github.com/ArmandoHerra/pdf-tooling` |
26
- | Import package | `pdf_toolkit` |
27
- | Console scripts | `pdftoolkit` (canonical), `pdf-toolkit` (alias) |
28
-
29
- **Why the distribution is not `pdf-toolkit`.** That name sits too close to names already
30
- on PyPI, and the distribution called `pdftoolkit` there is an unrelated GPL-3.0 project
31
- that is not this software. The repository followed the distribution; the import package
32
- and both console scripts did not, because they are published surfaces and moving them
33
- would break an install that already works.
34
-
35
- **Both console scripts are supported.** `pdftoolkit` is canonical and `pdf-toolkit` is a
36
- documented alias; they resolve to the same entry point, and the alias is not deprecated
37
- by this rename.
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
- pdftoolkit --help
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 pdftoolkit --help
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 pdftoolkit --help` remains the authoritative list.
82
+ `uv run pdftooling --help` remains the authoritative list.
84
83
 
85
84
  ```bash
86
- uv run pdftoolkit --help # the full verb tree; always the authoritative list
87
- uv run pdftoolkit doctor # which engines resolved, and how
88
- uv run pdftoolkit merge a.pdf b.pdf -O merged.pdf
89
- uv run pdftoolkit rotate report.pdf --pages 2-4 --angle 90 -O rotated.pdf
90
- uv run pdftoolkit compress report.pdf -O small.pdf
91
- uv run pdftoolkit --version # tool, Python and engine versions on one line
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 pdftoolkit --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.
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 `pdftoolkit meta -o json` is a usage error (exit 2) rather than a run. It names the two positions that do work: `pdftoolkit -o json meta get FILE` (before the group) and `pdftoolkit meta get FILE -o json` (after the subcommand).
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
- **Global flags with no command at all** — `pdftoolkit -o json` — are an incomplete invocation and exit 2 as well, pointing at `--help`. `pdftoolkit` on its own, with no arguments, still prints help and exits 0.
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 `pdftoolkit doctor` can legitimately report as unavailable.
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 (`pdftoolkit 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.
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.