aspose-pdf-foss-for-python 0.1.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 (679) hide show
  1. aspose_pdf_foss_for_python-0.1.0/CHANGELOG.md +3433 -0
  2. aspose_pdf_foss_for_python-0.1.0/LICENSE +21 -0
  3. aspose_pdf_foss_for_python-0.1.0/MANIFEST.in +4 -0
  4. aspose_pdf_foss_for_python-0.1.0/PKG-INFO +2233 -0
  5. aspose_pdf_foss_for_python-0.1.0/README.md +2183 -0
  6. aspose_pdf_foss_for_python-0.1.0/fuzz/README.md +20 -0
  7. aspose_pdf_foss_for_python-0.1.0/fuzz/__init__.py +1 -0
  8. aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/README.md +5 -0
  9. aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/content/text.txt +1 -0
  10. aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/cos/minimal.pdf +28 -0
  11. aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/cos/truncated.pdf +3 -0
  12. aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/filters/asciihex.txt +1 -0
  13. aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/tokenizer/dictionary.txt +1 -0
  14. aspose_pdf_foss_for_python-0.1.0/fuzz/run.py +29 -0
  15. aspose_pdf_foss_for_python-0.1.0/fuzz/targets.py +114 -0
  16. aspose_pdf_foss_for_python-0.1.0/pyproject.toml +112 -0
  17. aspose_pdf_foss_for_python-0.1.0/scripts/build_agl_data.py +152 -0
  18. aspose_pdf_foss_for_python-0.1.0/scripts/build_cmap_data.py +507 -0
  19. aspose_pdf_foss_for_python-0.1.0/setup.cfg +4 -0
  20. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/__init__.py +367 -0
  21. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/_compat_surface.py +142 -0
  22. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/_export_files.py +108 -0
  23. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/_pending_signatures.py +222 -0
  24. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/_version.py +17 -0
  25. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/actions.py +273 -0
  26. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/annotations/__init__.py +1614 -0
  27. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/annotations/pdf3d.py +70 -0
  28. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/attachments.py +62 -0
  29. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/auto_links.py +375 -0
  30. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/block_extraction.py +223 -0
  31. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/cgm.py +7 -0
  32. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/clustering/__init__.py +13 -0
  33. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/clustering/hierarchical_agglomerative_clustering.py +151 -0
  34. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/color.py +241 -0
  35. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/destinations.py +164 -0
  36. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/document.py +5884 -0
  37. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/document_fonts.py +150 -0
  38. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/drawing.py +13 -0
  39. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/__init__.py +1 -0
  40. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/agl.py +243 -0
  41. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/annotation_cleanup.py +407 -0
  42. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/annotation_import.py +503 -0
  43. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/annotation_json.py +313 -0
  44. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/annotation_xfdf.py +405 -0
  45. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/annotation_xfdf_import.py +389 -0
  46. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/appearance.py +1361 -0
  47. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/attachment_zip.py +306 -0
  48. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/attachment_zip_import.py +387 -0
  49. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/auto_crop.py +101 -0
  50. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/auto_tag.py +1451 -0
  51. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/blank_pages.py +198 -0
  52. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/blank_split.py +73 -0
  53. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/bookmark_json.py +714 -0
  54. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/bookmark_split.py +167 -0
  55. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/ccitt.py +457 -0
  56. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/ccitt_tables.py +231 -0
  57. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/cert_chain.py +306 -0
  58. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/cff_outlines.py +928 -0
  59. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/cms.py +562 -0
  60. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/comparison_html.py +435 -0
  61. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/conformance.py +2748 -0
  62. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/contact_sheet.py +215 -0
  63. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/content_authoring.py +1347 -0
  64. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/content_bounds.py +67 -0
  65. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/content_isolation.py +180 -0
  66. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/content_stream_parser.py +2318 -0
  67. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/cos.py +416 -0
  68. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/__init__.py +81 -0
  69. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/agl/LICENSE-ADOBE-AGL.txt +28 -0
  70. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/agl/LICENSE-REPORTLAB.txt +31 -0
  71. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/agl/README.md +44 -0
  72. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/agl/glyphlist.json.zlib +0 -0
  73. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/LICENSE-ADOBE-CMAP.txt +28 -0
  74. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/LICENSE-ADOBE-MAPPING.txt +28 -0
  75. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/README.md +51 -0
  76. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/predefined_cmaps_CNS1.json.zlib +0 -0
  77. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/predefined_cmaps_GB1.json.zlib +0 -0
  78. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/predefined_cmaps_Japan1.json.zlib +0 -0
  79. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/predefined_cmaps_Korea1.json.zlib +0 -0
  80. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/predefined_cmaps_index.json.zlib +0 -0
  81. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/LICENSE-DEJAVU.txt +187 -0
  82. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/LICENSE-OFL.txt +102 -0
  83. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/README.md +47 -0
  84. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/dingbats.ttf.zlib +0 -0
  85. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/mono-bold.ttf.zlib +0 -0
  86. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/mono-bolditalic.ttf.zlib +0 -0
  87. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/mono-italic.ttf.zlib +0 -0
  88. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/mono-regular.ttf.zlib +0 -0
  89. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/sans-bold.ttf.zlib +0 -0
  90. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/sans-bolditalic.ttf.zlib +0 -0
  91. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/sans-italic.ttf.zlib +0 -0
  92. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/sans-regular.ttf.zlib +0 -0
  93. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/serif-bold.ttf.zlib +0 -0
  94. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/serif-bolditalic.ttf.zlib +0 -0
  95. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/serif-italic.ttf.zlib +0 -0
  96. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/serif-regular.ttf.zlib +0 -0
  97. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/symbol.ttf.zlib +0 -0
  98. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/number.py +78 -0
  99. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/types.py +156 -0
  100. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/xmp.py +1458 -0
  101. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/dct.py +854 -0
  102. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/dss.py +658 -0
  103. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/duplicate_pages.py +67 -0
  104. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/encryption.py +967 -0
  105. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/field_appearance.py +310 -0
  106. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/file_output.py +98 -0
  107. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/filters.py +958 -0
  108. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/flatten.py +388 -0
  109. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_authoring.py +609 -0
  110. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_inventory.py +211 -0
  111. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_resolver.py +829 -0
  112. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_subset.py +545 -0
  113. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_subset_cff.py +963 -0
  114. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_subset_type1.py +114 -0
  115. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_variations.py +159 -0
  116. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_batch.py +134 -0
  117. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_csv.py +267 -0
  118. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_csv_export.py +131 -0
  119. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_data.py +364 -0
  120. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_field_edit.py +306 -0
  121. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_fields.py +253 -0
  122. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_json.py +89 -0
  123. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_schema.py +246 -0
  124. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_validation.py +395 -0
  125. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/glyph_outlines.py +465 -0
  126. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/graphics_absorb.py +1061 -0
  127. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/grid_split.py +80 -0
  128. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/gvar.py +467 -0
  129. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/headings.py +273 -0
  130. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/image_export.py +875 -0
  131. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/image_import.py +219 -0
  132. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/image_resample.py +116 -0
  133. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/image_resolution.py +376 -0
  134. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/image_zip.py +339 -0
  135. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/imposition.py +233 -0
  136. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/incremental_update.py +477 -0
  137. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/inline_image.py +337 -0
  138. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/interleave.py +188 -0
  139. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/io.py +53 -0
  140. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/jbig2.py +186 -0
  141. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/jpeg2000.py +2124 -0
  142. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/jpeg_encoder.py +706 -0
  143. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/jpx.py +211 -0
  144. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/link_extraction.py +378 -0
  145. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/link_urls.py +252 -0
  146. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/link_validation.py +555 -0
  147. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/lists.py +307 -0
  148. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/logging.py +18 -0
  149. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/merge_bookmarks.py +180 -0
  150. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/optional_content.py +1323 -0
  151. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/page_count_split.py +60 -0
  152. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/page_labels.py +295 -0
  153. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/page_zip.py +219 -0
  154. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/parser_exceptions.py +125 -0
  155. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/pdf_matrix.py +90 -0
  156. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/pdf_parser_cos.py +1611 -0
  157. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/pdf_writer_cos.py +544 -0
  158. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/predefined_cmaps.py +635 -0
  159. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/primitives/__init__.py +5 -0
  160. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/primitives/matrix.py +31 -0
  161. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/pubsec.py +898 -0
  162. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/rasterized_copy.py +171 -0
  163. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/rasterizer.py +5483 -0
  164. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/redaction.py +140 -0
  165. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/revocation.py +495 -0
  166. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/rich_text.py +415 -0
  167. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/sanitize.py +412 -0
  168. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/sfnt.py +247 -0
  169. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/shading.py +2317 -0
  170. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/sign_field.py +689 -0
  171. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signature_appearance.py +302 -0
  172. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signature_profiles.py +206 -0
  173. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signature_revisions.py +476 -0
  174. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signature_validator.py +313 -0
  175. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signed_changes.py +208 -0
  176. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signing.py +288 -0
  177. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/simple_pdf.py +21416 -0
  178. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/spread_split.py +330 -0
  179. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/stamps.py +414 -0
  180. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/std_font_data.py +201 -0
  181. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/std_fonts.py +97 -0
  182. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/std_metrics.py +364 -0
  183. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/stream_output.py +22 -0
  184. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/stroke.py +228 -0
  185. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/svg_export.py +1162 -0
  186. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/symbol_encodings.py +260 -0
  187. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/tables.py +420 -0
  188. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_blocks.py +467 -0
  189. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_comparison.py +146 -0
  190. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_edit.py +1936 -0
  191. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_export.py +758 -0
  192. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_faces.py +275 -0
  193. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_highlight.py +197 -0
  194. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_layout.py +574 -0
  195. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_locate.py +423 -0
  196. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_metrics.py +86 -0
  197. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_outlines.py +150 -0
  198. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_region.py +476 -0
  199. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_selection.py +53 -0
  200. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_split.py +141 -0
  201. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/tiff_import.py +130 -0
  202. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/timestamp.py +327 -0
  203. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/toc.py +407 -0
  204. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/type1_outlines.py +471 -0
  205. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/viewer_preferences.py +275 -0
  206. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/visual_comparison.py +318 -0
  207. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/webp_encoder.py +107 -0
  208. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/woff.py +302 -0
  209. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/woff2.py +674 -0
  210. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/word_extraction.py +235 -0
  211. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/exceptions.py +148 -0
  212. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/facades.py +1569 -0
  213. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/font_registry.py +149 -0
  214. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/font_repository.py +311 -0
  215. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/font_substitution.py +115 -0
  216. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/forms.py +1688 -0
  217. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/generated/__init__.py +3 -0
  218. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/generated/document.py +14 -0
  219. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/generated/forms.py +12 -0
  220. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/generated/pdfa.py +12 -0
  221. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/generated/security.py +33 -0
  222. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/geometry.py +149 -0
  223. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/graphics.py +256 -0
  224. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/html.py +117 -0
  225. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/image_resolution.py +45 -0
  226. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/images.py +524 -0
  227. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/interactive.py +435 -0
  228. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/latex.py +14 -0
  229. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/layers.py +482 -0
  230. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/license_management.py +65 -0
  231. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/link_extraction.py +52 -0
  232. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/link_validation.py +29 -0
  233. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/lists.py +365 -0
  234. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/load_limits.py +173 -0
  235. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/load_options.py +63 -0
  236. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/logical_structure.py +34 -0
  237. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/lowcode.py +1598 -0
  238. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/markdown.py +38 -0
  239. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/ofd.py +7 -0
  240. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/optimization.py +51 -0
  241. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/outlines.py +271 -0
  242. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/page_labels.py +169 -0
  243. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/page_ranges.py +90 -0
  244. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/page_size.py +220 -0
  245. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/pages.py +1883 -0
  246. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/paths.py +306 -0
  247. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/pdf_consts.py +19 -0
  248. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/pdfa.py +285 -0
  249. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/pdfua.py +194 -0
  250. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/pdfx.py +267 -0
  251. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/permissions.py +340 -0
  252. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/presentation.py +170 -0
  253. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/primitives.py +22 -0
  254. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/printing.py +41 -0
  255. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/py.typed +0 -0
  256. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/recipients.py +78 -0
  257. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/sanitization.py +90 -0
  258. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/save_format.py +25 -0
  259. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/save_options.py +25 -0
  260. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/security/__init__.py +160 -0
  261. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/security/impl/__init__.py +1 -0
  262. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/security/impl/digests/__init__.py +1 -0
  263. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/signature.py +385 -0
  264. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/signature_appearance.py +263 -0
  265. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/stamps.py +563 -0
  266. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/svg.py +41 -0
  267. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/table_extraction.py +156 -0
  268. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/tables.py +272 -0
  269. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/tagged.py +254 -0
  270. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/text.py +762 -0
  271. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/text_block.py +286 -0
  272. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/text_comparison.py +31 -0
  273. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/text_layout.py +100 -0
  274. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/utils.py +74 -0
  275. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/validation.py +286 -0
  276. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/viewer_preferences.py +375 -0
  277. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/visual_comparison.py +53 -0
  278. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/visualization.py +118 -0
  279. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/xmp.py +75 -0
  280. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf_foss_for_python.egg-info/PKG-INFO +2233 -0
  281. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf_foss_for_python.egg-info/SOURCES.txt +677 -0
  282. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf_foss_for_python.egg-info/dependency_links.txt +1 -0
  283. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf_foss_for_python.egg-info/requires.txt +30 -0
  284. aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf_foss_for_python.egg-info/top_level.txt +1 -0
  285. aspose_pdf_foss_for_python-0.1.0/tests/test_acc01_font_embedding.py +142 -0
  286. aspose_pdf_foss_for_python-0.1.0/tests/test_add_image_png_variants.py +266 -0
  287. aspose_pdf_foss_for_python-0.1.0/tests/test_aes_key_derivation.py +291 -0
  288. aspose_pdf_foss_for_python-0.1.0/tests/test_agl.py +103 -0
  289. aspose_pdf_foss_for_python-0.1.0/tests/test_algorithm_2b_v5r6.py +140 -0
  290. aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_appearance_decorations.py +148 -0
  291. aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_appearance_generation.py +389 -0
  292. aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_cleanup.py +655 -0
  293. aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_icon_appearances.py +157 -0
  294. aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_import.py +808 -0
  295. aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_json.py +572 -0
  296. aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_property_roundtrip.py +183 -0
  297. aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_references.py +101 -0
  298. aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_xfdf.py +609 -0
  299. aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_xfdf_import.py +566 -0
  300. aspose_pdf_foss_for_python-0.1.0/tests/test_annotations.py +182 -0
  301. aspose_pdf_foss_for_python-0.1.0/tests/test_api_surface.py +24 -0
  302. aspose_pdf_foss_for_python-0.1.0/tests/test_appearance_subtypes_documented.py +140 -0
  303. aspose_pdf_foss_for_python-0.1.0/tests/test_appearance_text_encoding.py +127 -0
  304. aspose_pdf_foss_for_python-0.1.0/tests/test_artifacts.py +649 -0
  305. aspose_pdf_foss_for_python-0.1.0/tests/test_associated_files.py +267 -0
  306. aspose_pdf_foss_for_python-0.1.0/tests/test_attachment_metadata.py +500 -0
  307. aspose_pdf_foss_for_python-0.1.0/tests/test_attachment_name_tree.py +169 -0
  308. aspose_pdf_foss_for_python-0.1.0/tests/test_attachment_zip.py +626 -0
  309. aspose_pdf_foss_for_python-0.1.0/tests/test_attachment_zip_import.py +810 -0
  310. aspose_pdf_foss_for_python-0.1.0/tests/test_attachments.py +181 -0
  311. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_10_trailer_nested_dict.py +60 -0
  312. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_11_tokenizer_null.py +35 -0
  313. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_12_lazy_object_parse_errors.py +36 -0
  314. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_13_object_stream.py +124 -0
  315. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_16_lazy_encryption.py +61 -0
  316. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_17_exceptions.py +109 -0
  317. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_18_encrypted_exception_types.py +77 -0
  318. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_19_content_stream_text.py +104 -0
  319. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_20_content_stream_token_memory.py +107 -0
  320. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_21_fonts_cmap_tounicode.py +128 -0
  321. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_22_jbig2.py +56 -0
  322. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_23_jpx_ccitt.py +77 -0
  323. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_24_incremental_update.py +94 -0
  324. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_25_encryption_v_and_password.py +110 -0
  325. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_26_decrypt_lazy.py +70 -0
  326. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_27_incremental_byte_range.py +34 -0
  327. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_28_signatures_compromise_detector.py +86 -0
  328. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_29_pdfa_heuristic.py +61 -0
  329. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_30_pdfua_heuristic.py +142 -0
  330. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_33_mmap_dispose.py +68 -0
  331. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_34_dispose_close_consistency.py +171 -0
  332. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_35_float_precision.py +87 -0
  333. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_36_stream_boundaries.py +111 -0
  334. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_37_stream_decoder.py +67 -0
  335. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_38_attachments.py +159 -0
  336. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_39_outlines.py +223 -0
  337. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_40_annotations_appearance.py +107 -0
  338. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_41_generated_document.py +106 -0
  339. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_42_logging.py +143 -0
  340. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_5_lazy_cos.py +87 -0
  341. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_6_from_bytes.py +52 -0
  342. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_7_parse_exception_handling.py +86 -0
  343. aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_9_xref_stream.py +126 -0
  344. aspose_pdf_foss_for_python-0.1.0/tests/test_authored_color_spaces.py +293 -0
  345. aspose_pdf_foss_for_python-0.1.0/tests/test_authoring_resource_limits.py +154 -0
  346. aspose_pdf_foss_for_python-0.1.0/tests/test_auto_crop.py +423 -0
  347. aspose_pdf_foss_for_python-0.1.0/tests/test_auto_links.py +585 -0
  348. aspose_pdf_foss_for_python-0.1.0/tests/test_auto_tag.py +732 -0
  349. aspose_pdf_foss_for_python-0.1.0/tests/test_auto_tag_limits.py +81 -0
  350. aspose_pdf_foss_for_python-0.1.0/tests/test_auto_tag_structure.py +500 -0
  351. aspose_pdf_foss_for_python-0.1.0/tests/test_blank_page_removal.py +384 -0
  352. aspose_pdf_foss_for_python-0.1.0/tests/test_blank_pages.py +433 -0
  353. aspose_pdf_foss_for_python-0.1.0/tests/test_blank_split.py +670 -0
  354. aspose_pdf_foss_for_python-0.1.0/tests/test_block_extraction.py +435 -0
  355. aspose_pdf_foss_for_python-0.1.0/tests/test_bookmark_json.py +945 -0
  356. aspose_pdf_foss_for_python-0.1.0/tests/test_bookmark_split.py +464 -0
  357. aspose_pdf_foss_for_python-0.1.0/tests/test_ccitt_decoder.py +125 -0
  358. aspose_pdf_foss_for_python-0.1.0/tests/test_ccitt_fidelity.py +404 -0
  359. aspose_pdf_foss_for_python-0.1.0/tests/test_cff2_variable.py +172 -0
  360. aspose_pdf_foss_for_python-0.1.0/tests/test_cff_predefined_encoding.py +99 -0
  361. aspose_pdf_foss_for_python-0.1.0/tests/test_cff_rasterization.py +512 -0
  362. aspose_pdf_foss_for_python-0.1.0/tests/test_cjk_cmap_rendering.py +356 -0
  363. aspose_pdf_foss_for_python-0.1.0/tests/test_cmap_build_data.py +65 -0
  364. aspose_pdf_foss_for_python-0.1.0/tests/test_cmap_resource_limits.py +161 -0
  365. aspose_pdf_foss_for_python-0.1.0/tests/test_cmap_syntax.py +249 -0
  366. aspose_pdf_foss_for_python-0.1.0/tests/test_color_space_conversion.py +359 -0
  367. aspose_pdf_foss_for_python-0.1.0/tests/test_comparison_html.py +455 -0
  368. aspose_pdf_foss_for_python-0.1.0/tests/test_compatibility_surfaces.py +333 -0
  369. aspose_pdf_foss_for_python-0.1.0/tests/test_complex_text_layout.py +443 -0
  370. aspose_pdf_foss_for_python-0.1.0/tests/test_contact_sheet.py +564 -0
  371. aspose_pdf_foss_for_python-0.1.0/tests/test_content_append_isolation.py +390 -0
  372. aspose_pdf_foss_for_python-0.1.0/tests/test_content_authoring.py +175 -0
  373. aspose_pdf_foss_for_python-0.1.0/tests/test_content_bounds.py +523 -0
  374. aspose_pdf_foss_for_python-0.1.0/tests/test_conversion_leaves_nothing_behind.py +217 -0
  375. aspose_pdf_foss_for_python-0.1.0/tests/test_cos.py +77 -0
  376. aspose_pdf_foss_for_python-0.1.0/tests/test_cos_comments.py +110 -0
  377. aspose_pdf_foss_for_python-0.1.0/tests/test_cos_preservation.py +73 -0
  378. aspose_pdf_foss_for_python-0.1.0/tests/test_cos_tree_resource_limits.py +316 -0
  379. aspose_pdf_foss_for_python-0.1.0/tests/test_crop_box_intersection.py +122 -0
  380. aspose_pdf_foss_for_python-0.1.0/tests/test_cross_validation.py +446 -0
  381. aspose_pdf_foss_for_python-0.1.0/tests/test_curve_fidelity.py +315 -0
  382. aspose_pdf_foss_for_python-0.1.0/tests/test_cyclic_resources.py +187 -0
  383. aspose_pdf_foss_for_python-0.1.0/tests/test_damaged_structures.py +277 -0
  384. aspose_pdf_foss_for_python-0.1.0/tests/test_dct.py +378 -0
  385. aspose_pdf_foss_for_python-0.1.0/tests/test_document_fonts.py +562 -0
  386. aspose_pdf_foss_for_python-0.1.0/tests/test_document_lifecycle.py +266 -0
  387. aspose_pdf_foss_for_python-0.1.0/tests/test_document_metadata.py +304 -0
  388. aspose_pdf_foss_for_python-0.1.0/tests/test_document_signing.py +635 -0
  389. aspose_pdf_foss_for_python-0.1.0/tests/test_document_with_no_pages.py +158 -0
  390. aspose_pdf_foss_for_python-0.1.0/tests/test_duplicate_pages.py +538 -0
  391. aspose_pdf_foss_for_python-0.1.0/tests/test_embed_fonts.py +615 -0
  392. aspose_pdf_foss_for_python-0.1.0/tests/test_embedded_file_mime.py +90 -0
  393. aspose_pdf_foss_for_python-0.1.0/tests/test_encrypted_cos_writer.py +636 -0
  394. aspose_pdf_foss_for_python-0.1.0/tests/test_encrypted_pdf_validation.py +97 -0
  395. aspose_pdf_foss_for_python-0.1.0/tests/test_encryption_aes.py +53 -0
  396. aspose_pdf_foss_for_python-0.1.0/tests/test_encryption_task7.py +19 -0
  397. aspose_pdf_foss_for_python-0.1.0/tests/test_engine_v0.py +43 -0
  398. aspose_pdf_foss_for_python-0.1.0/tests/test_engine_v1.py +88 -0
  399. aspose_pdf_foss_for_python-0.1.0/tests/test_error_handling.py +126 -0
  400. aspose_pdf_foss_for_python-0.1.0/tests/test_export_save_options.py +345 -0
  401. aspose_pdf_foss_for_python-0.1.0/tests/test_extractor_facade.py +167 -0
  402. aspose_pdf_foss_for_python-0.1.0/tests/test_facades.py +601 -0
  403. aspose_pdf_foss_for_python-0.1.0/tests/test_field_alignment.py +398 -0
  404. aspose_pdf_foss_for_python-0.1.0/tests/test_field_appearance_and_actions.py +483 -0
  405. aspose_pdf_foss_for_python-0.1.0/tests/test_field_appearance_generation.py +1084 -0
  406. aspose_pdf_foss_for_python-0.1.0/tests/test_field_font_glyphs.py +340 -0
  407. aspose_pdf_foss_for_python-0.1.0/tests/test_field_type0_font.py +148 -0
  408. aspose_pdf_foss_for_python-0.1.0/tests/test_field_value_appearance.py +236 -0
  409. aspose_pdf_foss_for_python-0.1.0/tests/test_file_editor_insert.py +219 -0
  410. aspose_pdf_foss_for_python-0.1.0/tests/test_file_editor_io.py +292 -0
  411. aspose_pdf_foss_for_python-0.1.0/tests/test_file_header.py +242 -0
  412. aspose_pdf_foss_for_python-0.1.0/tests/test_file_output.py +234 -0
  413. aspose_pdf_foss_for_python-0.1.0/tests/test_fill_rules.py +290 -0
  414. aspose_pdf_foss_for_python-0.1.0/tests/test_filters.py +339 -0
  415. aspose_pdf_foss_for_python-0.1.0/tests/test_filters_decoding.py +112 -0
  416. aspose_pdf_foss_for_python-0.1.0/tests/test_filters_encoding.py +163 -0
  417. aspose_pdf_foss_for_python-0.1.0/tests/test_flattening_task5.py +18 -0
  418. aspose_pdf_foss_for_python-0.1.0/tests/test_font_authoring_formats.py +288 -0
  419. aspose_pdf_foss_for_python-0.1.0/tests/test_font_source.py +374 -0
  420. aspose_pdf_foss_for_python-0.1.0/tests/test_font_subset.py +744 -0
  421. aspose_pdf_foss_for_python-0.1.0/tests/test_font_subset_cff.py +700 -0
  422. aspose_pdf_foss_for_python-0.1.0/tests/test_font_subset_cff2.py +591 -0
  423. aspose_pdf_foss_for_python-0.1.0/tests/test_font_subset_type1.py +243 -0
  424. aspose_pdf_foss_for_python-0.1.0/tests/test_font_substitution.py +599 -0
  425. aspose_pdf_foss_for_python-0.1.0/tests/test_form_batch.py +474 -0
  426. aspose_pdf_foss_for_python-0.1.0/tests/test_form_creation.py +353 -0
  427. aspose_pdf_foss_for_python-0.1.0/tests/test_form_csv.py +381 -0
  428. aspose_pdf_foss_for_python-0.1.0/tests/test_form_csv_export.py +347 -0
  429. aspose_pdf_foss_for_python-0.1.0/tests/test_form_data.py +467 -0
  430. aspose_pdf_foss_for_python-0.1.0/tests/test_form_field_editing.py +564 -0
  431. aspose_pdf_foss_for_python-0.1.0/tests/test_form_field_lifecycle.py +141 -0
  432. aspose_pdf_foss_for_python-0.1.0/tests/test_form_field_properties.py +380 -0
  433. aspose_pdf_foss_for_python-0.1.0/tests/test_form_field_types.py +143 -0
  434. aspose_pdf_foss_for_python-0.1.0/tests/test_form_font_resources.py +88 -0
  435. aspose_pdf_foss_for_python-0.1.0/tests/test_form_json.py +371 -0
  436. aspose_pdf_foss_for_python-0.1.0/tests/test_form_reset.py +349 -0
  437. aspose_pdf_foss_for_python-0.1.0/tests/test_form_schema.py +412 -0
  438. aspose_pdf_foss_for_python-0.1.0/tests/test_form_validation.py +741 -0
  439. aspose_pdf_foss_for_python-0.1.0/tests/test_form_xobject_text.py +439 -0
  440. aspose_pdf_foss_for_python-0.1.0/tests/test_forms.py +164 -0
  441. aspose_pdf_foss_for_python-0.1.0/tests/test_forms_icons_and_submit.py +223 -0
  442. aspose_pdf_foss_for_python-0.1.0/tests/test_fuzz_corpus.py +26 -0
  443. aspose_pdf_foss_for_python-0.1.0/tests/test_generate_outlines.py +441 -0
  444. aspose_pdf_foss_for_python-0.1.0/tests/test_generated_appearance_rendering.py +260 -0
  445. aspose_pdf_foss_for_python-0.1.0/tests/test_generated_reexports.py +75 -0
  446. aspose_pdf_foss_for_python-0.1.0/tests/test_glyph_rasterization.py +382 -0
  447. aspose_pdf_foss_for_python-0.1.0/tests/test_graphics_absorber.py +534 -0
  448. aspose_pdf_foss_for_python-0.1.0/tests/test_grid_split.py +498 -0
  449. aspose_pdf_foss_for_python-0.1.0/tests/test_gvar_variable_truetype.py +544 -0
  450. aspose_pdf_foss_for_python-0.1.0/tests/test_hybrid_reference_files.py +161 -0
  451. aspose_pdf_foss_for_python-0.1.0/tests/test_image_codec_limits.py +133 -0
  452. aspose_pdf_foss_for_python-0.1.0/tests/test_image_dimensions.py +59 -0
  453. aspose_pdf_foss_for_python-0.1.0/tests/test_image_export.py +463 -0
  454. aspose_pdf_foss_for_python-0.1.0/tests/test_image_extraction.py +139 -0
  455. aspose_pdf_foss_for_python-0.1.0/tests/test_image_masking.py +360 -0
  456. aspose_pdf_foss_for_python-0.1.0/tests/test_image_optimize_coverage.py +543 -0
  457. aspose_pdf_foss_for_python-0.1.0/tests/test_image_placement_absorber.py +208 -0
  458. aspose_pdf_foss_for_python-0.1.0/tests/test_image_placement_editing.py +409 -0
  459. aspose_pdf_foss_for_python-0.1.0/tests/test_image_recompress.py +455 -0
  460. aspose_pdf_foss_for_python-0.1.0/tests/test_image_resample.py +60 -0
  461. aspose_pdf_foss_for_python-0.1.0/tests/test_image_resolution.py +568 -0
  462. aspose_pdf_foss_for_python-0.1.0/tests/test_image_zip.py +699 -0
  463. aspose_pdf_foss_for_python-0.1.0/tests/test_incremental_object_numbers.py +162 -0
  464. aspose_pdf_foss_for_python-0.1.0/tests/test_incremental_save.py +289 -0
  465. aspose_pdf_foss_for_python-0.1.0/tests/test_incremental_update.py +162 -0
  466. aspose_pdf_foss_for_python-0.1.0/tests/test_inert_surfaces.py +232 -0
  467. aspose_pdf_foss_for_python-0.1.0/tests/test_info_values.py +401 -0
  468. aspose_pdf_foss_for_python-0.1.0/tests/test_inline_images.py +416 -0
  469. aspose_pdf_foss_for_python-0.1.0/tests/test_integration_final.py +43 -0
  470. aspose_pdf_foss_for_python-0.1.0/tests/test_interactive_actions.py +148 -0
  471. aspose_pdf_foss_for_python-0.1.0/tests/test_interleave.py +806 -0
  472. aspose_pdf_foss_for_python-0.1.0/tests/test_jbig2_decoder.py +117 -0
  473. aspose_pdf_foss_for_python-0.1.0/tests/test_jpeg2000_decoder.py +624 -0
  474. aspose_pdf_foss_for_python-0.1.0/tests/test_jpeg2000_packed_headers.py +287 -0
  475. aspose_pdf_foss_for_python-0.1.0/tests/test_jpeg_encoder.py +249 -0
  476. aspose_pdf_foss_for_python-0.1.0/tests/test_jpx_task8.py +21 -0
  477. aspose_pdf_foss_for_python-0.1.0/tests/test_layer_authoring.py +546 -0
  478. aspose_pdf_foss_for_python-0.1.0/tests/test_layer_configurations.py +599 -0
  479. aspose_pdf_foss_for_python-0.1.0/tests/test_layer_radio_groups.py +189 -0
  480. aspose_pdf_foss_for_python-0.1.0/tests/test_lazy_images.py +115 -0
  481. aspose_pdf_foss_for_python-0.1.0/tests/test_legacy_writer_surface.py +123 -0
  482. aspose_pdf_foss_for_python-0.1.0/tests/test_link_destinations.py +446 -0
  483. aspose_pdf_foss_for_python-0.1.0/tests/test_link_extraction.py +660 -0
  484. aspose_pdf_foss_for_python-0.1.0/tests/test_link_urls.py +589 -0
  485. aspose_pdf_foss_for_python-0.1.0/tests/test_link_validation.py +811 -0
  486. aspose_pdf_foss_for_python-0.1.0/tests/test_lists.py +711 -0
  487. aspose_pdf_foss_for_python-0.1.0/tests/test_literal_strings.py +177 -0
  488. aspose_pdf_foss_for_python-0.1.0/tests/test_load_from_stream.py +294 -0
  489. aspose_pdf_foss_for_python-0.1.0/tests/test_load_limits.py +543 -0
  490. aspose_pdf_foss_for_python-0.1.0/tests/test_logging_task10.py +18 -0
  491. aspose_pdf_foss_for_python-0.1.0/tests/test_lowcode.py +444 -0
  492. aspose_pdf_foss_for_python-0.1.0/tests/test_lowcode_catalogue.py +541 -0
  493. aspose_pdf_foss_for_python-0.1.0/tests/test_lowcode_resource_limits.py +167 -0
  494. aspose_pdf_foss_for_python-0.1.0/tests/test_lowcode_static_image_import.py +371 -0
  495. aspose_pdf_foss_for_python-0.1.0/tests/test_lowcode_tiff_import.py +421 -0
  496. aspose_pdf_foss_for_python-0.1.0/tests/test_macroman_encoding.py +422 -0
  497. aspose_pdf_foss_for_python-0.1.0/tests/test_matrix_composition.py +221 -0
  498. aspose_pdf_foss_for_python-0.1.0/tests/test_media_box_fallback.py +148 -0
  499. aspose_pdf_foss_for_python-0.1.0/tests/test_merge.py +466 -0
  500. aspose_pdf_foss_for_python-0.1.0/tests/test_merge_bookmarks.py +401 -0
  501. aspose_pdf_foss_for_python-0.1.0/tests/test_merged_structure_keys.py +246 -0
  502. aspose_pdf_foss_for_python-0.1.0/tests/test_merged_structure_tree.py +398 -0
  503. aspose_pdf_foss_for_python-0.1.0/tests/test_merging_task4.py +104 -0
  504. aspose_pdf_foss_for_python-0.1.0/tests/test_metadata.py +73 -0
  505. aspose_pdf_foss_for_python-0.1.0/tests/test_named_destinations.py +509 -0
  506. aspose_pdf_foss_for_python-0.1.0/tests/test_names.py +183 -0
  507. aspose_pdf_foss_for_python-0.1.0/tests/test_numbers.py +307 -0
  508. aspose_pdf_foss_for_python-0.1.0/tests/test_object_generations.py +319 -0
  509. aspose_pdf_foss_for_python-0.1.0/tests/test_opt_01_gc.py +81 -0
  510. aspose_pdf_foss_for_python-0.1.0/tests/test_optimization_options.py +551 -0
  511. aspose_pdf_foss_for_python-0.1.0/tests/test_optimization_task6.py +21 -0
  512. aspose_pdf_foss_for_python-0.1.0/tests/test_optional_content.py +322 -0
  513. aspose_pdf_foss_for_python-0.1.0/tests/test_outline_named_destinations.py +187 -0
  514. aspose_pdf_foss_for_python-0.1.0/tests/test_outline_targets.py +438 -0
  515. aspose_pdf_foss_for_python-0.1.0/tests/test_owner_password_long_user.py +157 -0
  516. aspose_pdf_foss_for_python-0.1.0/tests/test_p0_bugfixes.py +146 -0
  517. aspose_pdf_foss_for_python-0.1.0/tests/test_package_metadata.py +74 -0
  518. aspose_pdf_foss_for_python-0.1.0/tests/test_page_count_split.py +654 -0
  519. aspose_pdf_foss_for_python-0.1.0/tests/test_page_extraction.py +337 -0
  520. aspose_pdf_foss_for_python-0.1.0/tests/test_page_import.py +361 -0
  521. aspose_pdf_foss_for_python-0.1.0/tests/test_page_import_references.py +169 -0
  522. aspose_pdf_foss_for_python-0.1.0/tests/test_page_labels.py +409 -0
  523. aspose_pdf_foss_for_python-0.1.0/tests/test_page_number_labels.py +267 -0
  524. aspose_pdf_foss_for_python-0.1.0/tests/test_page_ranges.py +191 -0
  525. aspose_pdf_foss_for_python-0.1.0/tests/test_page_rendering.py +251 -0
  526. aspose_pdf_foss_for_python-0.1.0/tests/test_page_resize.py +513 -0
  527. aspose_pdf_foss_for_python-0.1.0/tests/test_page_rotation_cropbox.py +112 -0
  528. aspose_pdf_foss_for_python-0.1.0/tests/test_page_size_and_boxes.py +524 -0
  529. aspose_pdf_foss_for_python-0.1.0/tests/test_page_stamp_and_imposition.py +461 -0
  530. aspose_pdf_foss_for_python-0.1.0/tests/test_page_subset_adoption.py +213 -0
  531. aspose_pdf_foss_for_python-0.1.0/tests/test_page_transparency.py +198 -0
  532. aspose_pdf_foss_for_python-0.1.0/tests/test_page_zip.py +561 -0
  533. aspose_pdf_foss_for_python-0.1.0/tests/test_pages.py +592 -0
  534. aspose_pdf_foss_for_python-0.1.0/tests/test_password.py +67 -0
  535. aspose_pdf_foss_for_python-0.1.0/tests/test_path_authoring.py +632 -0
  536. aspose_pdf_foss_for_python-0.1.0/tests/test_pattern_paint.py +303 -0
  537. aspose_pdf_foss_for_python-0.1.0/tests/test_pattern_space.py +293 -0
  538. aspose_pdf_foss_for_python-0.1.0/tests/test_pdf_cos_parser_stream_sources.py +89 -0
  539. aspose_pdf_foss_for_python-0.1.0/tests/test_pdf_extractor_bind_password.py +59 -0
  540. aspose_pdf_foss_for_python-0.1.0/tests/test_pdf_file_editor_last_exception.py +54 -0
  541. aspose_pdf_foss_for_python-0.1.0/tests/test_pdf_header_position.py +133 -0
  542. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa.py +55 -0
  543. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa4_info.py +195 -0
  544. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa4_pdfua2.py +631 -0
  545. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_cmyk_images.py +206 -0
  546. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_cmyk_normalize.py +145 -0
  547. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_conformance_extended.py +579 -0
  548. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_conversion.py +495 -0
  549. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_embed_fonts.py +355 -0
  550. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_output_intent_match.py +201 -0
  551. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_separation_devicen.py +340 -0
  552. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_task9.py +21 -0
  553. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_transparency_groups.py +210 -0
  554. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_validation_result.py +386 -0
  555. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfua2_document_root.py +153 -0
  556. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfua_conformance.py +434 -0
  557. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfua_mcid_coverage.py +152 -0
  558. aspose_pdf_foss_for_python-0.1.0/tests/test_pdfx_conformance.py +1428 -0
  559. aspose_pdf_foss_for_python-0.1.0/tests/test_permissions.py +498 -0
  560. aspose_pdf_foss_for_python-0.1.0/tests/test_png_transparency.py +165 -0
  561. aspose_pdf_foss_for_python-0.1.0/tests/test_predefined_cjk_cmaps.py +769 -0
  562. aspose_pdf_foss_for_python-0.1.0/tests/test_public_key_encryption.py +866 -0
  563. aspose_pdf_foss_for_python-0.1.0/tests/test_pushbutton_appearance.py +70 -0
  564. aspose_pdf_foss_for_python-0.1.0/tests/test_raster_output_formats.py +351 -0
  565. aspose_pdf_foss_for_python-0.1.0/tests/test_rasterized_copy.py +642 -0
  566. aspose_pdf_foss_for_python-0.1.0/tests/test_rasterized_copy_modes.py +360 -0
  567. aspose_pdf_foss_for_python-0.1.0/tests/test_rc4_external.py +57 -0
  568. aspose_pdf_foss_for_python-0.1.0/tests/test_redaction_overlay.py +136 -0
  569. aspose_pdf_foss_for_python-0.1.0/tests/test_redaction_saving.py +403 -0
  570. aspose_pdf_foss_for_python-0.1.0/tests/test_refactored_simple_pdf.py +64 -0
  571. aspose_pdf_foss_for_python-0.1.0/tests/test_render_annotations.py +251 -0
  572. aspose_pdf_foss_for_python-0.1.0/tests/test_render_clip.py +359 -0
  573. aspose_pdf_foss_for_python-0.1.0/tests/test_repair_task2.py +30 -0
  574. aspose_pdf_foss_for_python-0.1.0/tests/test_resource_limits_ccitt_jbig2.py +89 -0
  575. aspose_pdf_foss_for_python-0.1.0/tests/test_revision_chain.py +337 -0
  576. aspose_pdf_foss_for_python-0.1.0/tests/test_rich_text.py +235 -0
  577. aspose_pdf_foss_for_python-0.1.0/tests/test_rounded_rect.py +203 -0
  578. aspose_pdf_foss_for_python-0.1.0/tests/test_sanitize.py +572 -0
  579. aspose_pdf_foss_for_python-0.1.0/tests/test_save_stability.py +232 -0
  580. aspose_pdf_foss_for_python-0.1.0/tests/test_save_to_stream.py +203 -0
  581. aspose_pdf_foss_for_python-0.1.0/tests/test_scan_conversion.py +200 -0
  582. aspose_pdf_foss_for_python-0.1.0/tests/test_security.py +36 -0
  583. aspose_pdf_foss_for_python-0.1.0/tests/test_selective_flatten.py +784 -0
  584. aspose_pdf_foss_for_python-0.1.0/tests/test_shading.py +894 -0
  585. aspose_pdf_foss_for_python-0.1.0/tests/test_shading_resource_limits.py +255 -0
  586. aspose_pdf_foss_for_python-0.1.0/tests/test_sign_authored_field.py +499 -0
  587. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_appearance.py +788 -0
  588. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_certification.py +71 -0
  589. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_chain_validation.py +106 -0
  590. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_field_authoring.py +149 -0
  591. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_pades.py +551 -0
  592. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_revision_policy.py +350 -0
  593. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_revocation.py +139 -0
  594. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_seed_value.py +601 -0
  595. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_timestamp.py +65 -0
  596. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_trust_boundaries.py +715 -0
  597. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_validation.py +459 -0
  598. aspose_pdf_foss_for_python-0.1.0/tests/test_signature_verification.py +138 -0
  599. aspose_pdf_foss_for_python-0.1.0/tests/test_signatures_through_document.py +244 -0
  600. aspose_pdf_foss_for_python-0.1.0/tests/test_signing.py +35 -0
  601. aspose_pdf_foss_for_python-0.1.0/tests/test_signing_e2e.py +36 -0
  602. aspose_pdf_foss_for_python-0.1.0/tests/test_smoke_imports.py +22 -0
  603. aspose_pdf_foss_for_python-0.1.0/tests/test_soft_mask_rendering.py +364 -0
  604. aspose_pdf_foss_for_python-0.1.0/tests/test_spread_split.py +554 -0
  605. aspose_pdf_foss_for_python-0.1.0/tests/test_stamps.py +643 -0
  606. aspose_pdf_foss_for_python-0.1.0/tests/test_standard14_rendering.py +341 -0
  607. aspose_pdf_foss_for_python-0.1.0/tests/test_standard_font_text.py +185 -0
  608. aspose_pdf_foss_for_python-0.1.0/tests/test_standard_security_handler.py +363 -0
  609. aspose_pdf_foss_for_python-0.1.0/tests/test_std_fonts.py +40 -0
  610. aspose_pdf_foss_for_python-0.1.0/tests/test_stencil_masks.py +474 -0
  611. aspose_pdf_foss_for_python-0.1.0/tests/test_stream_cmap_render.py +218 -0
  612. aspose_pdf_foss_for_python-0.1.0/tests/test_stream_lengths.py +127 -0
  613. aspose_pdf_foss_for_python-0.1.0/tests/test_streaming.py +561 -0
  614. aspose_pdf_foss_for_python-0.1.0/tests/test_streaming_parity.py +255 -0
  615. aspose_pdf_foss_for_python-0.1.0/tests/test_stroke_rendering.py +259 -0
  616. aspose_pdf_foss_for_python-0.1.0/tests/test_structure_validation.py +236 -0
  617. aspose_pdf_foss_for_python-0.1.0/tests/test_subset_drops_unused_tables.py +119 -0
  618. aspose_pdf_foss_for_python-0.1.0/tests/test_svg_compositing.py +560 -0
  619. aspose_pdf_foss_for_python-0.1.0/tests/test_svg_export.py +408 -0
  620. aspose_pdf_foss_for_python-0.1.0/tests/test_svg_selectable_text.py +344 -0
  621. aspose_pdf_foss_for_python-0.1.0/tests/test_table_extraction.py +458 -0
  622. aspose_pdf_foss_for_python-0.1.0/tests/test_table_of_contents.py +434 -0
  623. aspose_pdf_foss_for_python-0.1.0/tests/test_tables.py +594 -0
  624. aspose_pdf_foss_for_python-0.1.0/tests/test_tagged_remediation.py +180 -0
  625. aspose_pdf_foss_for_python-0.1.0/tests/test_text_artifacts.py +513 -0
  626. aspose_pdf_foss_for_python-0.1.0/tests/test_text_block_pagination.py +358 -0
  627. aspose_pdf_foss_for_python-0.1.0/tests/test_text_blocks.py +770 -0
  628. aspose_pdf_foss_for_python-0.1.0/tests/test_text_comparison.py +535 -0
  629. aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_cross_operator.py +192 -0
  630. aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_cross_position.py +181 -0
  631. aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_limits.py +125 -0
  632. aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_multi_tj.py +126 -0
  633. aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_regex.py +494 -0
  634. aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_type0.py +246 -0
  635. aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_type0_encodings.py +150 -0
  636. aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_type0_reconstruct.py +356 -0
  637. aspose_pdf_foss_for_python-0.1.0/tests/test_text_editing.py +119 -0
  638. aspose_pdf_foss_for_python-0.1.0/tests/test_text_exclude_regions.py +533 -0
  639. aspose_pdf_foss_for_python-0.1.0/tests/test_text_export.py +400 -0
  640. aspose_pdf_foss_for_python-0.1.0/tests/test_text_export_resources.py +345 -0
  641. aspose_pdf_foss_for_python-0.1.0/tests/test_text_extraction.py +82 -0
  642. aspose_pdf_foss_for_python-0.1.0/tests/test_text_extraction_task3.py +18 -0
  643. aspose_pdf_foss_for_python-0.1.0/tests/test_text_fragment_absorber.py +493 -0
  644. aspose_pdf_foss_for_python-0.1.0/tests/test_text_fragment_geometry.py +249 -0
  645. aspose_pdf_foss_for_python-0.1.0/tests/test_text_highlight.py +515 -0
  646. aspose_pdf_foss_for_python-0.1.0/tests/test_text_lines.py +245 -0
  647. aspose_pdf_foss_for_python-0.1.0/tests/test_text_outlines.py +566 -0
  648. aspose_pdf_foss_for_python-0.1.0/tests/test_text_page_selection.py +466 -0
  649. aspose_pdf_foss_for_python-0.1.0/tests/test_text_region.py +476 -0
  650. aspose_pdf_foss_for_python-0.1.0/tests/test_text_render_modes.py +313 -0
  651. aspose_pdf_foss_for_python-0.1.0/tests/test_text_reshape_edit.py +360 -0
  652. aspose_pdf_foss_for_python-0.1.0/tests/test_text_run_separation.py +200 -0
  653. aspose_pdf_foss_for_python-0.1.0/tests/test_text_split.py +565 -0
  654. aspose_pdf_foss_for_python-0.1.0/tests/test_text_strings.py +212 -0
  655. aspose_pdf_foss_for_python-0.1.0/tests/test_text_word_gaps.py +146 -0
  656. aspose_pdf_foss_for_python-0.1.0/tests/test_tiling_pattern.py +121 -0
  657. aspose_pdf_foss_for_python-0.1.0/tests/test_truncated_stream_recovery.py +124 -0
  658. aspose_pdf_foss_for_python-0.1.0/tests/test_txt01_fallback.py +43 -0
  659. aspose_pdf_foss_for_python-0.1.0/tests/test_type1_rasterization.py +351 -0
  660. aspose_pdf_foss_for_python-0.1.0/tests/test_type3_fonts.py +214 -0
  661. aspose_pdf_foss_for_python-0.1.0/tests/test_typed_annotations.py +513 -0
  662. aspose_pdf_foss_for_python-0.1.0/tests/test_unicode_font_authoring.py +428 -0
  663. aspose_pdf_foss_for_python-0.1.0/tests/test_validation_task1.py +32 -0
  664. aspose_pdf_foss_for_python-0.1.0/tests/test_validator_bytearray_limits.py +220 -0
  665. aspose_pdf_foss_for_python-0.1.0/tests/test_verify_aes_generation.py +47 -0
  666. aspose_pdf_foss_for_python-0.1.0/tests/test_viewer_preferences.py +443 -0
  667. aspose_pdf_foss_for_python-0.1.0/tests/test_visual_comparison.py +658 -0
  668. aspose_pdf_foss_for_python-0.1.0/tests/test_visual_ignore_regions.py +585 -0
  669. aspose_pdf_foss_for_python-0.1.0/tests/test_webp_output.py +329 -0
  670. aspose_pdf_foss_for_python-0.1.0/tests/test_whitespace_extraction.py +229 -0
  671. aspose_pdf_foss_for_python-0.1.0/tests/test_woff.py +303 -0
  672. aspose_pdf_foss_for_python-0.1.0/tests/test_woff2.py +233 -0
  673. aspose_pdf_foss_for_python-0.1.0/tests/test_woff2_collections.py +150 -0
  674. aspose_pdf_foss_for_python-0.1.0/tests/test_word_extraction.py +615 -0
  675. aspose_pdf_foss_for_python-0.1.0/tests/test_wrapper_attributes.py +134 -0
  676. aspose_pdf_foss_for_python-0.1.0/tests/test_xmp_info_sync.py +418 -0
  677. aspose_pdf_foss_for_python-0.1.0/tests/test_xmp_namespace.py +174 -0
  678. aspose_pdf_foss_for_python-0.1.0/tests/test_xmp_packet.py +924 -0
  679. aspose_pdf_foss_for_python-0.1.0/tests/test_xref_streams.py +385 -0
@@ -0,0 +1,3433 @@
1
+ # Changelog
2
+
3
+ All notable changes to Aspose.PDF FOSS for Python will be documented in this
4
+ file.
5
+
6
+ The project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - **Turn scanned spreads into individual PDF pages.**
13
+ `Document.split_spreads()` supports selected spreads, both reading axes,
14
+ reversed reading order, an adjustable split and a central gutter. Teams can
15
+ prepare scans for reading and distribution while keeping covers, text and
16
+ vector/image content in an independent document.
17
+
18
+ - **Share PDF review comments through XFDF.**
19
+ Document and page exports carry supported annotations, markup geometry,
20
+ authors, dates and reply threads with page/type filters. Teams can hand off
21
+ review data without copying the full PDF or manually re-entering comments.
22
+
23
+ - **Download rendered PDF pages as one image archive.**
24
+ `Document.export_pages_zip()` packages selected views as PNG, JPEG or optional
25
+ WebP, with configurable resolution/color and an optional page manifest.
26
+ Teams can share previews or feed image workflows without managing individual
27
+ export files; processing completes before the destination is written.
28
+
29
+ - **Generate filled PDFs from a reusable template and a batch of records.**
30
+ `FormFiller` and `FormFillerOptions` create independent results with optional
31
+ field flattening, preserving template values omitted by a record. Teams can
32
+ prepare repeatable document batches without carrying answers between rows;
33
+ processing finishes before configured outputs are written.
34
+
35
+ - **Update link URLs across a document.**
36
+ `Document.replace_link_urls()` and `Page.replace_link_urls()` support
37
+ literal/regex migrations, page selection and dry-run counts while retaining
38
+ link appearance and other action data. Plans are checked before editing,
39
+ helping maintain published PDF links without recreating the document.
40
+
41
+ - **Create navigation bookmarks from text patterns.**
42
+ `Document.generate_outlines_from_text()` turns literal or regex matches into
43
+ page bookmarks, with capture groups for titles and reusable text filters.
44
+ Reports and document batches can gain navigation without manual bookmark
45
+ entry or heading styles; validation or extraction failures preserve the
46
+ existing tree.
47
+
48
+ - **Exclude headers, footers and sidebars from extracted text.**
49
+ `exclude_regions` on page/document extraction, the facade and low-code text
50
+ workflows removes overlapping glyphs without editing the PDF. Rectangles
51
+ combine with page selection, inclusion clips and marked-artifact filtering,
52
+ helping indexing and document pipelines avoid repeated page decoration.
53
+
54
+ - **Create grayscale and black-and-white PDF copies.**
55
+ `Document.rasterized_copy(mode="gray" | "bilevel")` prepares static documents
56
+ for scan processing, sharing and monochrome printing, with adjustable
57
+ threshold and optional dithering. Converted samples are stored directly as
58
+ 8-bit grayscale or packed 1-bit images, with resource-aware output planning.
59
+
60
+ - **Describe PDF form templates for reusable filling interfaces.**
61
+ `Form.to_schema()` and `Form.export_schema()` expose field types, labels,
62
+ effective constraints, choice options and typed defaults without exporting
63
+ current answers. Applications can inspect templates and build form workflows
64
+ with less custom PDF metadata handling, including streaming/encrypted inputs.
65
+
66
+ - **Extract PDF structure for indexing and data pipelines.**
67
+ `Document.extract_blocks()` and `Page.extract_blocks()` return immutable
68
+ headings, paragraphs, lists and tables with original page indices and
69
+ position anchors. `export_blocks_json()` provides versioned UTF-8 JSON to
70
+ bytes, files or binary streams, with opt-in figure PNGs. Applications can
71
+ consume document structure directly without parsing generated markup.
72
+
73
+ - **Clean up annotations by author and date.**
74
+ `Document.remove_annotations()` and `page.annotations.remove_matching()`
75
+ filter review markup by author, date range, subtype and page, with a dry-run
76
+ count before editing. Teams can clear superseded review rounds before
77
+ handoff while retaining unrelated feedback, links and forms by default.
78
+ Removal plans include owned popups and repairs to surviving reply links,
79
+ are validated before mutation, and do not decode page or appearance streams.
80
+
81
+ - **Measure visible content without editing the PDF.**
82
+ `Page.get_content_bounds()` and `Document.get_content_bounds(pages=None)`
83
+ return visible ink bounds in page coordinates, with `None` for blank-looking
84
+ pages. Processing teams can inspect white margins and prepare crop or layout
85
+ decisions before changing documents. Measurement follows the current page
86
+ view, supports scans and optional annotation appearances, and reads selected
87
+ streaming pages one at a time without changing page boxes or content.
88
+
89
+ - **Inventory PDF links for audits and content migrations.**
90
+ `Document.extract_links(pages=None)` and `Page.extract_links()` return
91
+ detached source coordinates, URI/local/remote targets and action-chain
92
+ diagnostics. Relative URIs use the catalog base; named destinations resolve
93
+ to current pages. Review teams can locate references needing updates without
94
+ opening each link. No target is executed, fetched or opened, and no page
95
+ content or image decoding is required.
96
+
97
+ - **Find low-resolution image placements.**
98
+ `Document.find_low_resolution_images(min_dpi=150, pages=None)` reports image
99
+ locations, pixel dimensions and effective DPI for each weak placement,
100
+ including inline images and nested forms. Rotation, shear, reuse and page
101
+ UserUnit are accounted for without decoding image samples. Teams can locate
102
+ assets that need replacement before print or publication and automate
103
+ resolution checks. This does not assess sharpness or certify print quality.
104
+
105
+ - **A contents page, built from the document's own headings.**
106
+ `generate_outlines` already turned them into bookmarks; `Document
107
+ .add_table_of_contents()` writes the same answer onto paper — the headings
108
+ down a page, indented by level, with dot leaders, the page each is on, and a
109
+ link that lands on the heading itself rather than on the top of its sheet.
110
+ - **The page numbers account for the contents.** Inserting it pushes every
111
+ page it points at further back, so the numbers cannot be read off the
112
+ document as it stands. They are not circular — how many lines there are
113
+ does not depend on what the numbers say — so the page count is settled
114
+ first, the pages are inserted, and only then is each heading's new place
115
+ worked out. `at=1` puts the contents after a cover, and a heading before
116
+ that keeps the number it had.
117
+ - **Page labels win over positions**: a contents saying `12` for a page the
118
+ reader sees as `xii` is wrong in the only way that matters.
119
+ - A heading too long for its line is shortened with an ellipsis rather than
120
+ run under its own page number; the leaders are right-aligned so they line
121
+ up down the page; a document with no headings gets **no page at all**.
122
+ - `title`, `max_level`, `prefer_structure`, `at`, `size`, `margin`,
123
+ `font_name`, `title_size`, `entry_size`, `indent`, `leader`, `links`,
124
+ `page_numbers` and `tag` are all adjustable; the sheet defaults to the size
125
+ of the page the contents goes in front of.
126
+ - `tag=True` marks the entries as `/TOCI` **inside a `/TOC`** (table 333) —
127
+ a row of `TOCI` at the root is not a table of contents, it is a row of
128
+ orphans.
129
+ - Checked with qpdf: every link resolves to exactly the page the line prints,
130
+ as an `/XYZ` at the heading's own coordinates, `--check` clean.
131
+
132
+ - **SVG export can write words instead of curves.** Every glyph went out as the
133
+ outline its font gives it — exact, and about as selectable as a photograph of
134
+ a page: nothing could search it, copy from it or read it aloud.
135
+ - `Page.to_svg(text=…)`, `Page.save_as_svg(text=…)` and
136
+ `Document.save_as_svg(text=…)` take `"outlines"` (the default, unchanged),
137
+ `"text"` (`<text>` elements instead of curves) or `"both"` (the curves,
138
+ with unfilled `<text>` over them).
139
+ - **Every glyph carries its own `x`.** A viewer draws with whatever font it
140
+ resolves the name to, so its advances are not the PDF's; positioning each
141
+ glyph means the layout does not move even when the face does. Rotation,
142
+ skew and scale ride in the element's `transform`, whose y column is negated
143
+ so the words are not mirrored by the page's own flip.
144
+ - The face is *described* for a viewer to match — `Times, serif` with
145
+ `font-weight`/`font-style` off the PostScript name, and a generic family
146
+ from the descriptor flags — since an embedded program lives in the PDF, not
147
+ in the SVG.
148
+ - **Invisible text (rendering mode 3) is collected**, which is what makes a
149
+ scanned page's OCR layer searchable once exported.
150
+ - A glyph whose meaning nothing in the file states gets no `<text>` and keeps
151
+ its outlines: a guess would put the wrong word in someone's copy buffer.
152
+ - One element per line, per font, per colour — gluing two lines together
153
+ would hand whoever copies the text one word made of two.
154
+ - Checked against cairosvg: `"both"` renders **pixel-identically** to
155
+ `"outlines"` while `ElementTree` reads the words straight out, and
156
+ `"text"`'s ink lands in the same bounding box to within a pixel.
157
+ - The rasterizer gained one no-op sink (`_note_glyph`) for this; rendering is
158
+ unchanged by construction.
159
+
160
+ - **`Document.sanitize()` takes out everything that is not what the document
161
+ shows.** Each piece was removable on its own — `info`, `javascript`,
162
+ `attachments`, `flatten_layers`, the annotation collections — but there was
163
+ no pass that did all of them, and no way to say what had been taken.
164
+ - Removed by default: `/Info`, the XMP packet and the producer's private
165
+ `/PieceInfo` store (catalog **and** pages); the `/Names /JavaScript` tree;
166
+ the open action when it *is* an action; the catalog's and every page's
167
+ `/AA`; any action on an annotation or field that runs script, launches a
168
+ file or sends data away, including one chained behind a harmless action
169
+ through `/Next`; embedded files, from the name tree and the `/AF` entries
170
+ that reach them; optional content, resolved as `flatten_layers` does so a
171
+ hidden layer's content is **deleted**; and markup annotations, which carry
172
+ an author and a date.
173
+ - Left alone by default: links (`links=True` to remove them) and the
174
+ interactive form (`forms=True`). A link that opens a URL keeps working —
175
+ that is what a link is for — while one that launches a program loses its
176
+ action and stays as a rectangle.
177
+ - Returns a `SanitizationReport` counting what went, so a caller can state
178
+ what was done rather than hope. `bool(report)` is `False` when the document
179
+ carried none of it.
180
+ - **The next save is a full rewrite.** Dropping a reference does not remove
181
+ the object, and an appended revision keeps the unsanitized one in front of
182
+ it in the same file — either way the attachment's bytes and the reviewer's
183
+ note would still be in there, findable with any COS tool. So the live graph
184
+ is garbage-collected on the way out and an incremental save is refused. A
185
+ qpdf-written document with ten findable secrets comes back with none of
186
+ them, `qpdf --check` clean and rendering unchanged.
187
+
188
+ - **What fonts does this document use?** `FontRepository` answered what fonts
189
+ the *machine* has; nothing answered what a *document* draws with, though the
190
+ document knows exactly.
191
+ - `Document.fonts` lists them, one
192
+ [`DocumentFont`](src/aspose_pdf/document_fonts.py) each: the name with and
193
+ without its subset tag, the kind (and a composite font's descendant kind),
194
+ the encoding and whether it remaps codes, whether the glyphs are in the
195
+ file and under which key, what the program *is* and what it *costs*,
196
+ whether the text can be extracted, and which pages use it.
197
+ - The walk is the one the PDF/A, PDF/UA and PDF/X checks use, so the
198
+ inventory and the conformance reports cannot disagree about which fonts a
199
+ document has — including a font reached only through a form's default
200
+ resources, which is listed with no page and a *location* saying so.
201
+ - **Fonts are grouped, dictionaries are not.** A document that writes its own
202
+ `/Helv` on every page uses one font, not twenty. Two *subsets* of one
203
+ family stay apart, and so do two dictionaries that name one font but carry
204
+ different programs — folding either would hide a program.
205
+ - `program_bytes` is how big the program is with its filters undone;
206
+ `stored_bytes` is what it costs this file. They are different numbers and
207
+ answer different questions, so both are reported.
208
+ - Checked field by field against an independent pikepdf/qpdf walk of the same
209
+ files, ours and one qpdf wrote.
210
+
211
+ - **A page can be taken from one document and put in another.**
212
+ `pages.insert(i, other.pages[j])` was refused outright — moving a page
213
+ between documents meant `extract_pages` into a scratch document, `merge` it,
214
+ then `move_page` it into position.
215
+ - `PageCollection.insert` and `PageCollection.add` now accept a `Page` of
216
+ another document and **import** it: the page and the graph it names —
217
+ resources, annotations, the fields its widgets belong to, the optional
218
+ content it mentions, its part of the structure tree, its label, and any
219
+ bookmark that pointed at it. It is the same import `Document.merge` does,
220
+ for one page and at a position; both now go through one
221
+ `Document._import_pages_from`.
222
+ - Checked with qpdf (`--check` clean, each page carrying its own
223
+ `/Resources`), pdfium and pdfminer.six, in both directions: a page out of
224
+ a qpdf-written file lands in ours intact, and a document we assemble this
225
+ way reads back correctly.
226
+ - Taking *every* page is still `merge`'s job, and a long run of pages is
227
+ cheaper through `extract_pages` plus a merge than one call each, since
228
+ each import walks the other document's forms, outlines and structure tree.
229
+
230
+ - **A signature can now be seen.** A signature field is invisible until
231
+ something is drawn in it, and nothing could draw anything: `Document.sign`
232
+ took no appearance, and `Form.add_signature_field` authored an empty box its
233
+ own docstring described as such. A signed document showed no sign of being
234
+ signed outside a viewer's signature panel.
235
+ - `Document.sign(..., appearance=SignatureAppearance(...))` draws who signed,
236
+ when, why and where into the field's widget. The name is `signer_name`, or
237
+ the common name in the signing certificate when that is not given; the date
238
+ is the moment of the save. Any line can be turned off, labels dropped, or
239
+ the lot replaced with `text=` of your own.
240
+ - **An image** — a scanned signature, a seal, a logo — goes beside the text
241
+ (`image_position="left"`, `"right"`, `"top"`), behind it
242
+ (`"background"`), or alone (`"only"`), scaled to fit without being
243
+ squashed. Colours: `background`, `border_color`, `border_width`,
244
+ `text_color`.
245
+ - The size is chosen to fit the box, preferring one at which no entry wraps:
246
+ `Date: 2026-10-09 12:03:32 +0200` broken after the time, with the zone
247
+ offset alone underneath, reads worse than the same line a point smaller.
248
+ - **Non-Latin names work** through `font=`, which embeds a font the way
249
+ `Page.add_text` does. Without one, text the standard fonts cannot encode is
250
+ refused by name rather than drawn as the wrong letters.
251
+ - With no `field`, `SignatureAppearance(page=…, rect=…)` adds a visible field
252
+ there; an appearance with neither a field nor a rect is refused, since the
253
+ invisible field that would otherwise be added has nowhere to draw.
254
+ - **The signature covers the appearance.** It is written into the document
255
+ before the save serializes it, not into a later revision, so it cannot be
256
+ edited or lifted onto another document without breaking the signature.
257
+ pyHanko reports a file signed this way as `INTACT:UNTOUCHED` with
258
+ `coverage=ENTIRE_FILE`, and one byte changed inside the appearance as
259
+ `INVALID`. A signature field authored by qpdf/pikepdf and signed here comes
260
+ back the same way.
261
+
262
+ - **A font the document only names can now be put into it.** A PDF may refer to
263
+ `Arial` and not carry it, and then its text draws differently wherever Arial
264
+ is different or absent — which PDF/A, PDF/UA and PDF/X all forbid, and which
265
+ no rewriting fixes, because the program is not in the file. The renderer
266
+ could already *draw* such a font from faces the caller made available
267
+ (`Document.font_substitution`); nothing could write one in.
268
+ - `Document.embed_fonts()` supplies it. With no arguments it covers the
269
+ Standard 14, Symbol and ZapfDingbats from the bundled metric-compatible
270
+ faces — no font source needed. `directory=` or
271
+ `sources=FontSubstitutionOptions(...)` (programs handed over, named
272
+ folders, the machine's own fonts) covers everything else, and with neither,
273
+ the document's own `font_substitution` is used.
274
+ - It returns what it could **not** supply, one message per font saying why:
275
+ no source answers to the name, the face's outlines are a kind that font
276
+ cannot carry, the font is a `CIDFontType0`, or it is a composite font whose
277
+ codes are not its CIDs (anything but an Identity CMap), so `/ToUnicode`
278
+ cannot say what to map. An empty list means every font in the document now
279
+ has a program.
280
+ - **Composite fonts too.** A `CIDFontType2` selects glyphs by index
281
+ (9.7.4.2), so another program's glyphs are not the ones its CIDs name: the
282
+ `/CIDToGIDMap` is rebuilt through `/ToUnicode` and the face's own `cmap`.
283
+ A document whose program was stripped and re-supplied renders
284
+ **pixel-identically** to the original under pdfium.
285
+ - The program goes under the key its kind uses and `/Subtype` is corrected to
286
+ match (table 122), since a TrueType program under a `/Type1` font is not an
287
+ embedded font at all. The document's `/Widths` and `/W` are kept, so
288
+ glyphs change and positions do not.
289
+ - `convert_to_pdfa`, `convert_to_pdfua` and `convert_to_pdfx` all run this
290
+ pass. PDF/UA previously embedded only the bundled Standard-14 faces and
291
+ ignored the document's font sources, so a named font it could have supplied
292
+ stayed missing.
293
+ - `convert_to_pdfa(font_lookup_directory=...)` searches that directory by
294
+ what each face **is** — the family and style in its own `name` table — not
295
+ by file name. It used to look for a file *called* `Arial.ttf` and attach
296
+ whatever was under that name: `arial.ttf` was missed, `Arial.otf` produced
297
+ a Type 1 key over an OpenType program, and 14 bytes of anything embedded as
298
+ a font program.
299
+ - Verified against qpdf (structure), pdfminer.six (text) and pdfium
300
+ (rendering), in both directions: a qpdf-written file with a named,
301
+ unembedded font comes back with a 50 KB `/FontFile2` that all three read.
302
+
303
+ - **Resizing a page now takes its drawing with it.** `Page.size` and
304
+ `PdfPageEditor.resize` changed the **sheet** and left the drawing where it
305
+ was, so a smaller sheet cropped it and a larger one padded it. Nothing scaled
306
+ page content at all.
307
+ - `Page.resize(size, mode=…)`, `Document.resize_pages(size, mode=…, pages=…)`
308
+ and `PdfPageEditor.resize(..., scale_content=True)` scale the drawing onto
309
+ the new sheet; `Page.scale_content(factor)` scales it inside the sheet it is
310
+ already on, centred by default or `about="origin"`.
311
+ - `mode` is `"fit"`, `"fill"` or `"stretch"`; the drawing is centred and the
312
+ sheet keeps its own origin. The facade's default is unchanged, so nothing
313
+ that called it moved.
314
+ - **The page's operators are not rewritten**: the stream goes inside one
315
+ `q` ... `Q` with the matrix at the top (new
316
+ `content_isolation.wrap_in_force`). A stray `Q` — one with nothing to
317
+ restore, which a viewer ignores — would pop that very save once a `q` stands
318
+ in front of it, so one extra `q` is pushed per stray to consume them, and a
319
+ text object left open is ended first. A page that restores a state it never
320
+ saved cannot be wrapped without changing how it draws and is refused by
321
+ name.
322
+ - Annotations travel with the drawing: `/Rect`, `/QuadPoints`, `/Vertices`,
323
+ `/L`, `/CL`, each run of an `/InkList`, and `/RD`'s four distances, which
324
+ scale but are never shifted. A `resize` takes the crop and production boxes
325
+ too.
326
+ - Checked with pdfium: an A4 page fitted onto Letter comes out 612x792 with
327
+ every glyph box, its height and the link rectangle scaled by the same 0.9407
328
+ and shifted by the same 26 points, and our raster of the result differs from
329
+ pdfium's on 0.07% of samples.
330
+
331
+ - **A bookmark tree from the document's headings.** `Document.outlines` could
332
+ be read and edited, but every bookmark had to be written by hand — and the
333
+ heading model needed to generate them had been computed for the HTML and
334
+ Markdown exports all along, just without a position to point at.
335
+ - `Document.generate_outlines()` makes an `OutlineItem` per heading, nested by
336
+ level. A **tagged** document is read from its structure tree (`/H1`..`/H6`,
337
+ or `/H` with the section nesting giving the level, `/RoleMap` followed) —
338
+ the author's own answer; an untagged one from the same size tiers `auto_tag`
339
+ infers, which is a heuristic and labelled one. `prefer_structure=False`
340
+ reads the pages either way.
341
+ - **Each bookmark lands on the heading, not on the top of its page.** The
342
+ place comes from the marked-content sequence the element owns — new
343
+ `auto_tag.mcid_spans` bounds it by byte range and the text objects inside
344
+ carry the coordinates — or from the inferred block's own anchor, which
345
+ `Block` now records. Written as `/XYZ` with the reader's magnification kept.
346
+ - Levels nest with skips: an `H3` after an `H1` becomes its child, not its
347
+ sibling. `max_level`, `open_to_level`, `replace=False` and `zoom=` adjust
348
+ the rest. It returns how many it made, and zero is the honest answer for a
349
+ document of uniform body text.
350
+ - Verified against qpdf's own outline reader — nesting, titles, each `/XYZ`
351
+ resolving to the right page and coordinate, the open state in the sign of
352
+ `/Count` — and the tree reads back identically after a qpdf rewrite.
353
+ - `OutlineItem` and `OutlineCollection` are now exported from `aspose_pdf`:
354
+ `Document.outlines` handed them out while a caller could not construct one
355
+ without reaching into the submodule.
356
+
357
+ - **Permissions have names.** `/P` is a signed 32-bit integer whose unused bits
358
+ are all 1, so "everything allowed" is `-4` and "nothing allowed" is `-3904`,
359
+ and `Permissions.PRINT | Permissions.COPY` as plain integers is `20` — a `/P`
360
+ with every reserved bit cleared, which ISO 32000-1 Table 22 does not allow.
361
+ Every permission argument in the package took one of those numbers.
362
+ - `Permissions` (exported from `aspose_pdf`) is that integer with the names
363
+ attached. It **is** an `int` subclass, so `Document.permissions` still
364
+ compares, masks, hashes and passes anywhere the raw value did — the whole
365
+ suite passed unchanged — while `can_copy`, `allowed`, `denied`, `to_dict()`
366
+ and `repr` say what it means.
367
+ - `Permissions.all()`, `none()`, `allowing(...)`, `denying(...)`, `with_(...)`
368
+ and `without(...)` keep the reserved bits right. Flags are names or the
369
+ eight class constants; a number carrying any bit outside Table 22's eight is
370
+ refused rather than ORed in.
371
+ - Taken by `Document.encrypt`, `change_passwords`, `encrypt_for_recipients`,
372
+ `Recipient`, `PdfFileSecurity.encrypt_file`/`change_password` and the
373
+ low-code `EncryptOptions`; a raw integer still works everywhere.
374
+ - **Bit 12 qualifies bit 3**, which cross-checking against qpdf is what
375
+ showed: `print_high_resolution` granted while `print` is clear is a document
376
+ nobody may print, and qpdf reads it that way. The value is written as asked
377
+ rather than silently corrected, and `can_print_faithfully` asks for both.
378
+ - Verified flag for flag: a file granting exactly one permission is read by
379
+ qpdf's own `Pdf.allow` as granting exactly that one, for all eight; and a
380
+ `/P` qpdf wrote reads back here by name with no bit forced or cleared
381
+ (`tests/fixtures_permissions_qpdf_no_copy.pdf`).
382
+ - The public-key handler's word is **not** `/P`, which is why
383
+ `encrypt_for_recipients` defaults to `-1` where `encrypt` defaults to `-4`.
384
+ The two differ because the two words do, not because either is wrong; the
385
+ eight permissions sit at the same bit positions in both.
386
+
387
+ - **Lists, authored.** The nested structure a list needs — `/L` of `/LI` of
388
+ `/Lbl` and `/LBody` — had been in the engine since `auto_tag` learned to
389
+ *recognise* one, but only for re-tagging content that was already on the page.
390
+ Writing a list meant placing every bullet and every line by hand.
391
+ - `Page.add_list(text_list, x, y)` takes a `TextList` of `ListItem`s: a marker
392
+ in a gutter, a body wrapped to the measure, sub-items a step further in.
393
+ `markers` cycles `•`, `–`, `·` by depth; an ordered list's `numbering`
394
+ cycles `decimal`, `lower-alpha`, `lower-roman`, each sub-list restarting its
395
+ own count, with Roman and alphabetic numbering going past the first round
396
+ (`z` then `aa`). `number_format`, `start` and a per-item `label` say
397
+ otherwise, `label=""` for an item with no marker at all.
398
+ - Markers are **right-aligned against the body**, so `9.` and `10.` line up on
399
+ their dots. pdfium reads every marker of a ten-item list as ending on one
400
+ edge and every body as starting on another.
401
+ - A list taller than the room left continues onto the next page, and an item's
402
+ marker always lands on the page its first line does — it rides that line.
403
+ - The `/L` carries the `/ListNumbering` of ISO 32000-1 Table 345 where the
404
+ markers have a standard name, so a reader that cannot see the glyphs knows
405
+ an ordered list from an unordered one; a marker no value describes writes no
406
+ attribute rather than the wrong one. A sub-list goes inside its parent
407
+ item's `/LBody`, where the standard puts it. A list continued onto the next
408
+ page **stays one list**, its far ids named with marked-content references.
409
+ A tagged list passes `convert_to_pdfua()` + `validate_pdfua()` with no
410
+ warnings and `convert_to_pdfa("2a")`.
411
+ - `engine/lists.py` reuses `text_faces` for the measuring and `text_blocks`
412
+ for the line objects and the line emitter, so a list, a text block and a
413
+ table place their baselines the same way and line up when set in sequence.
414
+
415
+ - **Decoration now says it is decoration (`/Artifact`).** ISO 14289-1 7.1 has
416
+ every mark on a page of a tagged document be either tagged as real content or
417
+ marked as an artifact, and nothing this package authored said the second: a
418
+ perfectly tagged document stopped being conformant the moment it was stamped,
419
+ and `validate_pdfua()` reported it as valid with no warnings.
420
+ - `TextStamp`, `ImageStamp` and `PageStamp` are `/Pagination` artifacts with
421
+ `/Subtype /Watermark` by default; a `PageNumberStamp` is a `/Header` or a
422
+ `/Footer` when its alignment puts it at the top or the bottom of the sheet,
423
+ with the matching `/Attached` — read off what the caller already said rather
424
+ than guessed. Each carries the `/BBox` of where it landed on that page, all
425
+ four corners of a rotated stamp included. `artifact=False` puts a stamp on as
426
+ bare content; `artifact_type`/`artifact_subtype` override the defaults.
427
+ - Redaction bars became a `/Layout` artifact whose `/BBox` is what the bars
428
+ cover.
429
+ - **Imposition was deliberately left alone** even though `n_up` and `booklet`
430
+ place their pages through the same code as a stamp: an imposed page is the
431
+ sheet's real content and has to stay taggable. The decision moved to the
432
+ caller rather than living in the placement.
433
+ - `Page.add_text`, `add_image`, `draw_rectangle`, `draw_line` and `draw_path`
434
+ take `artifact=True`, or `artifact="Pagination"`/`"Layout"`/`"Page"`, so a
435
+ rule or a running head drawn by hand can say what it is. Asking for an
436
+ artifact *and* a tag is refused.
437
+ - `validate_pdfua()` now scans each page of a tagged document for marks that no
438
+ marked-content sequence encloses and names the page and the operators. A `Do`
439
+ invoking a **form** is followed rather than flagged, because a form may cover
440
+ its own content. Reported as warnings: the scan reads operators, and
441
+ `convert_to_pdfua()` offers a shell-only conversion whose content is untagged
442
+ by design.
443
+ - qpdf's own content parser resolves every property list written this way, and
444
+ pdfium renders a marked page and an unmarked one pixel for pixel the same.
445
+
446
+ - **Regular expressions in `replace_text` and `redact_text`.** Searching
447
+ already understood patterns — `TextFragmentAbsorber` with
448
+ `TextSearchOptions(is_regular_expression=True)` — while editing took a literal
449
+ phrase only, so the one thing redaction is most wanted for, removing a *shape*
450
+ rather than a phrase, could not be asked for: `page.redact_text(r"\d{3}-\d{2}-\d{4}")`
451
+ found nothing and returned 0.
452
+ - `regex=True`, or a compiled `re.Pattern` as the search (which needs no
453
+ flag), on `Document.replace_text`, `Document.redact_text`,
454
+ `Page.replace_text` and `Page.redact_text`.
455
+ - With a pattern the replacement is a **template**: `\1` and `\g<name>`
456
+ expand to what *each* match captured, as `re.sub` defines them. A literal
457
+ search keeps its replacement verbatim, backslashes and all.
458
+ - A pattern reaches across `TJ` elements and consecutive show operators,
459
+ because it is matched against the same logical run a literal phrase is; `^`
460
+ and `$` anchor to that run. A pattern matching the empty string is ignored.
461
+ A pattern that will not compile, and a template naming a group that is not
462
+ there, are reported as `PdfValidationException`.
463
+ - The redaction **overlay bars** came along for free: the locator shares the
464
+ editor's span alignment, so a pattern's bars land exactly where the
465
+ equivalent literal's would. Verified three ways — pikepdf/qpdf finds the
466
+ redacted numbers in neither the decoded streams nor the raw file bytes,
467
+ pdfminer.six and pdfium both read the text with them gone, and pdfium's
468
+ raster shows a solid bar over where the digits were.
469
+
470
+ - **Flowing text.** Authored text could only be placed one string at a time at
471
+ one point. Wrapping it meant measuring the font yourself, or passing
472
+ `layout=TextLayoutOptions(...)` to `Page.add_text` — which shapes with
473
+ HarfBuzz and so refuses the Standard-14 fonts and needs the `text-layout`
474
+ extra. The plain case had no API at all, although the measuring it needs was
475
+ already written: the table generator has used the Standard-14 advances and an
476
+ embedded subset's CID widths since it landed.
477
+ - `Page.add_text_block(block, x, y)` takes a `TextBlock` of `Paragraph`s and
478
+ wraps each to the block's `width`, or to the room between *x* and the right
479
+ edge of the page. A plain string is one paragraph and a blank line starts a
480
+ new one; a single newline stays a hard break. Per paragraph: alignment,
481
+ font, size, colour, line height, space before and after, first-line indent
482
+ (negative for a hanging one), left and right indent, structure tag, and
483
+ `keep_together`.
484
+ - **Justification**, which is the part that needed thought. A justified line
485
+ is spread to the measure exactly — pdfium reads every one as ending on it to
486
+ the hundredth of a point — and the last line of a paragraph, the line before
487
+ every hard break, and a line of one word are all left alone. A simple font
488
+ is spread with `Tw`, a composite one with `TJ` adjustments, because
489
+ ISO 32000-1 9.3.3 says word spacing "shall not apply" to a two-byte code: an
490
+ Identity-encoded Type0 font ignores `Tw` entirely and the line would come
491
+ out unjustified.
492
+ - A block taller than the room left continues onto the next page, adding a
493
+ page like the one it is leaving when there is none. A paragraph continued
494
+ across pages stays **one** structure element, with the ids on the other
495
+ pages named as marked-content references (14.7.4.3) rather than the
496
+ paragraph being split into one `/P` per page — which would tell a reader the
497
+ sentence ended at the foot of the page. A tagged block passes
498
+ `convert_to_pdfua()` + `validate_pdfua()` and `convert_to_pdfa("2a")`.
499
+ - The faces the measuring runs on moved out of the table engine into
500
+ `engine.text_faces`, so a table and a block measure with the same code and
501
+ place their baselines the same way; the two line up when set one after the
502
+ other.
503
+
504
+ - **PDF/X.** The print-exchange standards had no support at all: a document
505
+ could be checked and converted for PDF/A and PDF/UA, but the one standard
506
+ family that is about a file *printing* the same way everywhere was missing, so
507
+ preparing artwork for a press meant assembling the output intent, the
508
+ identification keys and the trim geometry by hand.
509
+ - `Document.validate_pdfx(standard)`, `Document.is_pdfx_compliant(standard)`
510
+ and `Document.convert_to_pdfx(standard, ...)`, with `PdfXStandard`,
511
+ `PdfXValidationResult`, `PdfXValidateOptions` and `PdfXValidator` in
512
+ `aspose_pdf.pdfx` mirroring the PDF/A and PDF/UA shapes. Five levels:
513
+ `PDF/X-1a:2001`, `PDF/X-1a:2003`, `PDF/X-3:2002`, `PDF/X-3:2003`, `PDF/X-4`.
514
+ - Checked: the identification keys, one `/GTS_PDFX` output intent with an
515
+ embedded ICC profile whose colour space suits the part, font embedding,
516
+ `/TrimBox`-or-`/ArtBox` with bleed and media containment, `/Trapped` saying
517
+ True or False, encryption, prohibited actions and annotations, transfer
518
+ functions, and — per part — transparency, optional content, JPEG 2000,
519
+ JBIG2, LZW, alternate images and device-independent colour.
520
+ - **The identification is a different shape in each part**, and the conversion
521
+ writes each part's own. Parts 1 to 3 use the information dictionary plus the
522
+ `pdfx` XMP schema; ISO 15930-1 omits the conformance letter from its version
523
+ string (`PDF/X-1:2001` with `PDF/X-1a:2001` as the conformance). PDF/X-4
524
+ identifies itself in XMP alone under the separate PDF/X ID schema
525
+ (`pdfxid`), and converting to it removes the legacy Info keys. `exiftool`
526
+ and `qpdf` read all five back as written.
527
+ - **The conversion does not invent a printing condition.** The output intent
528
+ is the promise a PDF/X file makes about the press, so without an
529
+ `icc_profile=` and without a usable one already in the document the
530
+ requirement is *returned* rather than a profile nobody chose being embedded
531
+ — the position already taken on a font whose program is not in the file.
532
+ Parts 3 and 4 fall back to the bundled sRGB profile, a real registered
533
+ characterization; PDF/X-1a admits no RGB intent and needs a CMYK profile.
534
+ Colour is never converted either.
535
+ - The header version is deliberately **not** a conformance rule here:
536
+ ISO 15930-4 clause 5 says the header shall not be used to decide it. The
537
+ conversion still raises the header to the part's own version, never lowers
538
+ it.
539
+ - No free validator covers PDF/X — veraPDF does not — so unlike every PDF/A
540
+ level this library produces, these rules are not corroborated by a reference
541
+ implementation. They were cross-checked with `qpdf`/`pikepdf` and `exiftool`
542
+ in both directions, and `supported-features.md` says so in those words.
543
+
544
+ - **Tables.** There was no table API at all — no `Table`, no `Row`, no `Cell` —
545
+ so a report meant placing every string and every line by hand, measuring the
546
+ text first to know where the next one goes. The two pieces that makes hard were
547
+ already written and need no optional dependency: the Standard-14 advances in
548
+ `engine.std_metrics` and an embedded font's CID widths.
549
+ - `Page.add_table(table, x, y)` draws one: column widths or equal columns,
550
+ column spans, text wrapped to each column (newlines hard-break, long words
551
+ break rather than overflow), rows that grow to hold their lines, and style
552
+ falling back cell → row → table — fonts, sizes, colours, padding, both
553
+ alignments, borders, backgrounds, and a separate set for header rows.
554
+ - A table taller than the room left **continues onto the next page**, repeating
555
+ the header rows and adding a page like the one it is leaving when there is
556
+ none to continue onto. It returns where it ended, so the next thing on the
557
+ page knows where to go.
558
+ - The structure is tagged as it is drawn: a nested `/Table` of `/TR` of `/TH`
559
+ and `/TD` (14.8.4.3), each cell owning its marked-content sequence. A tagged
560
+ table passes `convert_to_pdfua()` + `validate_pdfua()` and
561
+ `convert_to_pdfa("2a")`, both of which require tagging; `tag=False` turns it
562
+ off.
563
+ - `font=` sets a whole table in an embedded Unicode font. A subset font grows as
564
+ it is *encoded*, so measuring from a width table taken before the text was
565
+ encoded answered zero for every character — the text is encoded first now,
566
+ and the table re-read when a CID is missing from it.
567
+
568
+ - **The facade layer, from two classes to ten.** `PdfExtractor` and
569
+ `PdfFileEditor` were the whole of it, so ported code that reached for
570
+ `PdfFileInfo`, `PdfFileSecurity`, `PdfBookmarkEditor`, `PdfAnnotationEditor`,
571
+ `PdfFileStamp`, `PdfPageEditor`, `PdfXmpMetadata` or `PdfConverter` had to be
572
+ rewritten against `Document` by hand — which is the one thing a compatibility
573
+ surface exists to avoid.
574
+ - They add no capability: each is a composition of operations documented
575
+ elsewhere. What they add is the shape, and three habits of the layer: page
576
+ numbers are **1-based** (as in the Aspose facades, and as
577
+ `PdfFileEditor.extract` already was), a writing operation answers `True` or
578
+ `False` and records why in `last_exception`, and `bind_pdf` takes a path,
579
+ bytes, a stream or an **open `Document`** — which is borrowed, not taken over,
580
+ so the facade does not close a document the caller is still holding.
581
+ - `PdfFileStamp.add_header`/`add_footer`/`add_page_number` are running heads
582
+ built on the stamp placement, and `PdfPageEditor.move_page` reorders pages,
583
+ which needed the page sizes and boxes of an earlier change to be settable at
584
+ all.
585
+ - `PdfXmpMetadata.get_value` reads a **language alternative** — which is what
586
+ `dc:title` and `dc:description` are — as its default text rather than as the
587
+ array it is written as, with the field itself one line away through `packet`.
588
+
589
+ - **A page can be placed on a page, and a document can be imposed.** Stamps drew
590
+ a line of text, an image or a page number; the one thing nobody could stamp was
591
+ a *page* — a letterhead, a background form, a watermark drawn in a PDF — even
592
+ though both pieces it takes were already there: the deep object copy a merge
593
+ makes, and the form-XObject placement every stamp goes through. Without it there
594
+ was no imposition either, so four pages could not be put on a sheet and a
595
+ document could not be laid out as a folded booklet.
596
+ - `PageStamp(page)` places a page as a reader shows it — its crop box, with its
597
+ `/Rotate` applied, which for a quarter turn swaps the form's width and height
598
+ — and imports everything it draws with, so the source document may be closed
599
+ straight afterwards. Every placement option applies, `background=True`
600
+ included, which is what a letterhead wants, and `opacity` goes into an
601
+ `/ExtGState` inside the form.
602
+ - `Document.n_up(rows, columns, …)` and `Document.booklet(…)` return a new
603
+ imposed document; `PdfFileEditor.make_n_up()` and `make_booklet()` are the
604
+ same two over files. Pages are scaled to fit their cell proportionally and
605
+ centred, the import memo is shared so a common font is copied once, and
606
+ nothing is rotated behind the caller's back — a landscape sheet is asked for
607
+ by passing one.
608
+ - `TextStamp(font=…)` sets a stamp in an **embedded Unicode font**, through the
609
+ same subset Type0/CID path page text uses, so a stamp can say what the
610
+ standard fonts' encodings have no codes for. `font_name` is then not checked
611
+ against the fourteen, since the face comes from the font.
612
+ - Checked with qpdf 12.4.2: a 4-up sheet carries four form XObjects whose BBox
613
+ is the source page's size, each invoked under its own scale-and-translate; a
614
+ booklet of six pages is four sheets of two A5 pages side by side, two of them
615
+ half empty; and a stamped page's `/Contents` becomes an array with the
616
+ background stamp first. poppler renders all of it, and the imported
617
+ letterhead's colours come out pixel-identical to ours.
618
+
619
+ - **How a form field looks, and what a field, a page or the document does.** Two
620
+ holes, both on the authoring side. A field could be created but not styled:
621
+ `/MK` was written with a hardcoded black border and a white background, `/BS`
622
+ with a width of 1, the `/DA` colour was always black, and `border_color` and
623
+ `background` reached the widget of a **push button** only — while the appearance
624
+ generator had read `/MK /BC` and `/MK /BG` all along, so the drawing side was
625
+ waiting for entries nothing could set. And there were no actions anywhere: not a
626
+ field's `/AA` (the keystroke, format, validate and calculate scripts that make a
627
+ form compute), not a page's, not the catalog's, and not the document-level
628
+ scripts of `/Names /JavaScript` where the functions those call are defined.
629
+ `JavaScriptAction` existed with nowhere to put one but a link.
630
+ - Every factory now takes `border_color`, `background_color`, `border_width`,
631
+ `border_style`, `rotation` and `text_color`, and a `Field` has each as a
632
+ property plus `font_size` and `set_appearance(**options)`. Setting one entry
633
+ leaves the others alone; `None` removes one. A `font_size` of 0 is auto-size
634
+ (12.7.3.3), which the appearance generator already resolved.
635
+ - `Field.actions`, `Page.actions` and `Document.actions` are mappings of trigger
636
+ to action. `keystroke`, `format`, `validate` and `calculate` are written on the
637
+ **field** and the pointer and focus triggers on the **widget** — which is
638
+ where a viewer looks for each — and a trigger the owner does not have is
639
+ refused, since a viewer never fires one and the mistake would otherwise be
640
+ invisible.
641
+ - `Document.javascript` is the document-level script tree. It is written sorted,
642
+ because a viewer runs the scripts in key order, and each script goes **into**
643
+ the tree as a direct dictionary: an indirect action would stay in the file as
644
+ an orphan when the script was removed, with its source still readable.
645
+ - Checked with qpdf 12.4.2: `/DA` carries the size and the colour, `/AA /V` is on
646
+ the field and `/AA /U` on the widget, `/MK` holds `/BC`, `/BG` and `/R`, `/BS`
647
+ holds `/W` and `/S`, the page and catalog `/AA` are where the tables put them,
648
+ and the script tree is in key order.
649
+
650
+ - **An annotation class per subtype, and the comment threads the property channel
651
+ could not carry.** Reading and writing annotations worked — every entry
652
+ travelled through `Annotation.properties`, which is what preserves subtypes this
653
+ API has no class for — but a caller had to know the standard's spellings
654
+ (`/IC`, `/QuadPoints`, `/Vertices`, `/InkList`, `/L`, `/LE`, `/CA`, `/BS`) and
655
+ get the corner order of a quadrilateral right by hand.
656
+ - `TextAnnotation`, `FreeTextAnnotation`, `SquareAnnotation`,
657
+ `CircleAnnotation`, `LineAnnotation`, `PolygonAnnotation`,
658
+ `PolyLineAnnotation`, `InkAnnotation`, the four text markups,
659
+ `StampAnnotation`, `FileAttachmentAnnotation`, `RedactAnnotation` and
660
+ `PopupAnnotation` name those entries. The typed properties *are* the property
661
+ channel — `square.interior_color` and `square.get_property("IC")` are the same
662
+ entry — and a subtype with no class of its own is still read as it was.
663
+ - Shared markup entries: `opacity`, `subject`, `creation_date`, `modified`, and
664
+ `border_width`/`border_style`/`border_dash`, which edit one `/BS` dictionary
665
+ entry at a time instead of replacing it.
666
+ - A factory per subtype, each computing a `/Rect` that holds what the annotation
667
+ draws (12.5.2) with room for the stroke and the line endings, and
668
+ `quad_from_rect` writing the corner order viewers expect (12.5.6.10) — the
669
+ order whose classic mistake draws a highlight as a bow tie. A single rectangle
670
+ handed to a text-markup factory is taken as the one quad it describes.
671
+ - **Replies and popups**: `annotation.reply(...)` writes `/IRT` and
672
+ `/RT /Reply`, `replies` and `in_reply_to` read the thread, and `add_popup()`
673
+ links a window **both ways** (`/Parent` and `/Popup`), since one without the
674
+ other leaves a window a viewer cannot associate with anything. These are
675
+ references between annotations, which the property channel drops on purpose,
676
+ so there had been no way to thread a comment at all.
677
+ - `Annotation.color` reads the same forms every other colour in the package
678
+ does: 1/3/4 components, a `"#rrggbb"` string, or an `aspose_pdf.Color`.
679
+ - Checked with qpdf 12.4.2: a reply's `/IRT` is the same object as the
680
+ annotation it answers, a popup and its annotation point at each other, and
681
+ `/BS`, `/CA`, `/IC`, `/QuadPoints` and `/LE` are written as the tables type
682
+ them.
683
+
684
+ - **Paths, the stroke state, transparency and clipping, on the authoring side.**
685
+ A page could be given exactly two things: an axis-aligned rectangle and a
686
+ straight line. No curve, no circle, no polygon, no dash, no line cap or join,
687
+ no transparency, and no way to turn, scale or clip anything — while the
688
+ *renderer* had always drawn all of it, because that is what reading a PDF
689
+ means. `IPath` sat in the "Known Unsupported" table for exactly this reason:
690
+ there was nothing for a path to be drawn with.
691
+ - `GraphicsPath` collects the subpaths of ISO 32000-1 8.5.2 — `move_to`,
692
+ `line_to`, `curve_to`, `quadratic_to`, `close`, `rect`, `ellipse`, `circle`,
693
+ `polygon`, `polyline` — and the builders chain. A segment that continues a
694
+ subpath is refused on a path that has not been started, rather than writing an
695
+ operator with nothing to draw from.
696
+ - `Page.draw_path()` paints one: either fill rule, `line_width`, `line_cap`,
697
+ `line_join`, `miter_limit`, `dash` (a pattern or `(pattern, phase)`),
698
+ `opacity`/`fill_opacity`/`stroke_opacity`, `blend_mode`, and a `transform`.
699
+ The alpha and blend go into an `/ExtGState` that is **reused** when the page
700
+ already carries the same one, instead of one dictionary per drawing.
701
+ - `with page.graphics(transform=..., clip=..., opacity=...)` holds a state over
702
+ everything appended inside it — **text and images included**, which is how
703
+ either of those is turned, scaled or faded — and clips to a path with `W n`.
704
+ Sections nest, and the state is undone at the end.
705
+ - `draw_rectangle` and `draw_line` take all of the same options now, each being
706
+ one path; a plain rectangle or line still writes byte for byte what it wrote
707
+ before.
708
+ - `IPath` is no longer a placeholder and leaves the unsupported table: it
709
+ collects segments and `draw_path` draws it, reading its own `fill_mode` and
710
+ `transform` for whichever the call does not give.
711
+ - Checked against poppler 26.09 at 288 dpi — curves, dashes, caps, joins,
712
+ even-odd fills, transforms, clips, alpha and blend modes all render
713
+ pixel-identical (0–4 levels of 255). At 72 dpi a one-point hairline's
714
+ antialiasing differs between any two rasterizers, the *undashed* reference
715
+ case included, so the comparison is made where the geometry is what is being
716
+ measured.
717
+
718
+ - **Named destinations, and the two bookmark entries that go with them.** The
719
+ engine resolved these names on the way *in* all along — a bookmark written by
720
+ name already reported the right page — but nothing could list, add, repoint or
721
+ remove one, so the indirection that keeps a link valid when its target moves
722
+ was unreachable.
723
+ - `Document.destinations` is a mapping of name to destination. It reads both
724
+ places a PDF keeps them (the `/Names /Dests` tree and the older `/Dests`
725
+ dictionary), types each value where this API has a class for it, and lists an
726
+ entry it cannot type rather than hiding it. Assigning takes a `Destination` or
727
+ a page index; a page the document does not have is refused rather than
728
+ clamped.
729
+ - A write edits **one** name. The tree is rewritten as a single ordered node,
730
+ because a name tree must stay sorted by key, and every other value is carried
731
+ over as the same object it already was — so an entry this API cannot express
732
+ comes through untouched. A name already defined in the older dictionary is
733
+ updated there instead of being duplicated into the tree; a removal clears
734
+ both places.
735
+ - A **name is now a target** wherever a typed destination is:
736
+ `page.add_link(rect, "chapter-2")`, `OutlineItem(destination="chapter-2")` and
737
+ `Document.open_action = "chapter-2"`. `OutlineItem.destination_name` reads it
738
+ back. Repointing the name moves every reference to it at once.
739
+ - `OutlineItem.color` is the bookmark's `/C` title colour (three DeviceRGB
740
+ numbers, as Table 153 types it) and `OutlineItem.open` its unfolded state,
741
+ written as the **sign of `/Count`** with the magnitude Table 153 asks for:
742
+ every row opening the item would show, counting what an already-open child
743
+ unfolds in turn. The outline root's `/Count` is now that same total rather
744
+ than the number of top-level items, which was only right while everything was
745
+ closed — which, before this, it always was.
746
+ - Checked against qpdf 12.4.2: the tree it reads back is sorted and every value
747
+ resolves to a page, its own outline API resolves a bookmark's name, and a file
748
+ qpdf has rewritten still reads here with its names, colours and open states
749
+ intact.
750
+
751
+ - **The low-code plugin catalogue, from four workflows to fourteen.**
752
+ `aspose_pdf.lowcode` shipped `Merger`, `Splitter`, `Optimizer` and
753
+ `TextExtractor` — while its own `Plugin` enum already named `EXTRACTOR`,
754
+ `CONVERTER`, `GENERATOR` and `EDITOR`, with no class behind any of the four. A
755
+ batch job that rotates a scan, drops a page, stamps a draft mark or turns pages
756
+ into PNGs had to be written against `Document` by hand, data sources and all.
757
+ - New plugins, each a composition of operations that already existed:
758
+ `ImageExtractor` (one result per embedded image, as a real image file),
759
+ `ImagesToPdf` (JPEG/PNG in, one page per image, each page cut to its picture
760
+ at a given dpi or fitted inside a named page size), `Converter` (HTML,
761
+ Markdown, SVG, TIFF, PNG, JPEG — one result per input for the first and the
762
+ fourth, one per page for the others), `PdfAConverter`, `Rotator`,
763
+ `PageRemover`, `Stamper`, `FormFlattener`, `Encryptor` and `Decryptor`.
764
+ - `Splitter` grew **selections**: `SplitOptions(selections=[range(0, 3), [3, 4],
765
+ slice(5, None)])` emits one document per selection instead of one per page.
766
+ Each entry is what `Document.extract_pages` takes, a slice is resolved against
767
+ the input's own length, and a selection naming a page the document does not
768
+ have is refused rather than trimmed.
769
+ - `Document.to_tiff()` returns the bytes of a multi-page TIFF, which only
770
+ `save_as_tiff()` could produce before — every other export had both forms, and
771
+ the plugin layer works in bytes.
772
+ - `PageSize` equality now compares the pair as a file holds it (six decimal
773
+ places, ISO 32000-1 7.3.3), so `page.size == PageSize.A4` is true for a
774
+ document that has been saved and reloaded. It was false by half a nanometre.
775
+ - Checked against qpdf 12.4.2 and poppler 26.09: an images-to-PDF file is two A4
776
+ pages carrying one image XObject each, the PNG kept as Flate and the JPEG
777
+ passed through as `/DCTDecode` without re-encoding; an encrypted result needs
778
+ its password in qpdf's eyes too; and Pillow reads the multi-page TIFF as three
779
+ Deflate frames.
780
+
781
+ - **Authored colour is grey, RGB or CMYK, by one rule everywhere a colour is
782
+ taken.** `Page.add_text`, `draw_rectangle` and `draw_line` went through
783
+ `normalize_rgb`, which refused anything that was not exactly three components:
784
+ `fill_color=(0, 0.2, 1, 0.05)` raised "RGB color must contain exactly three
785
+ channels", so a page could not be painted in ink or in grey. Every other part
786
+ of the package already read 1/3/4 components — a stamp's colour, an
787
+ annotation's `/C` and `/IC`, a field's `/MK` background — which left the page
788
+ authoring API as the one place a print workflow could not use.
789
+ - One component now writes `g`/`G`, three `rg`/`RG`, four `k`/`K` (ISO 32000-1
790
+ 8.6.8: the three device spaces a content stream can set without naming a
791
+ colour space first). qpdf 12.4.2 parses the operators and operands back, and
792
+ poppler 26.09 renders grey and RGB to the same pixel we do.
793
+ - Every colour argument — `color`, `stroke_color`, `fill_color`, a stamp's
794
+ `color`, `redact_text(overlay_color=)` — reads a component sequence, a single
795
+ number for grey, a `"#rgb"`/`"#rrggbb"` string, or an `aspose_pdf.Color`.
796
+ - `Color` became a usable value object instead of a shell with an `r` property
797
+ and no `g` or `b`: `Color.gray`, `Color.rgb`, `Color.cmyk`, `Color.from_hex`,
798
+ `components`, `color_space`, equality and a repr, exported from `aspose_pdf`
799
+ and from `aspose_pdf.drawing`. The ported `Color(pattern, r, g, b)`
800
+ constructor still builds an RGB colour. A `Color` carrying a
801
+ `GradientAxialShading` raises `UnsupportedFeatureException` when asked for
802
+ components — there is no shading-pattern writer for it to feed.
803
+ - Grey and RGB channels may be 0..1 or 0..255, which is what `Page.add_text`
804
+ already accepted; **a stamp's colour now takes that scale too** rather than
805
+ refusing anything above 1. CMYK is 0..1 only: a percentage and an 8-bit value
806
+ cannot be told apart from a fraction, so `(0, 20, 100, 10)` is refused with a
807
+ message saying why instead of being divided by 255.
808
+ - A **redaction bar** takes an ink as well, and a colour it cannot read is now
809
+ refused instead of quietly drawn black — the bar is what makes the removal
810
+ visible, so the wrong colour is worse than an error.
811
+
812
+ - **A page can be created at any size, and every one of its five boxes can be
813
+ read and set.** A new page was 612x792 and nothing else: `pages.add()` took no
814
+ size, `Page.media_box` was a read-only alias of `rect`, and of the five boxes
815
+ ISO 32000-1 Table 30 defines only `/CropBox` was reachable — a document of A4
816
+ pages, or a page carrying the trim and bleed a print shop asks for, could not
817
+ be authored at all.
818
+ - `PageSize` carries the ISO A sizes `A0`-`A6`, the ISO B sizes `B4`/`B5` and
819
+ the US `LETTER`, `LEGAL`, `TABLOID`, `LEDGER`, `EXECUTIVE` and `STATEMENT`,
820
+ with `landscape()`, `portrait()`, `rotated()`, `scaled()`, `as_rect(x, y)`,
821
+ `from_mm()`, `from_inches()` and `by_name()` for the ones not listed. The ISO
822
+ sizes are exact conversions from millimetres, so A4 is
823
+ 595.275591 x 841.889764 pt rather than a printer's rounding of it.
824
+ - `pages.add(PageSize.A4)`, `pages.add(size="legal")` and
825
+ `pages.insert(0, size=(300, 400))` create a page of that size; without one a
826
+ blank page is still US Letter. A size given twice, or alongside a page to
827
+ copy, is refused rather than one of the two being quietly dropped.
828
+ - `Page.media_box` and `Page.size` now set the sheet. `size` takes a
829
+ `PageSize`, a name or a `(width, height)` pair and resizes from the existing
830
+ origin, so content keeps the coordinates it was authored at. The value
831
+ reaches both places a size is held — the page list the renderer measures and
832
+ the `/MediaBox` a save writes — so a resized page renders and reopens at its
833
+ new size, and it is written on the page, overriding a `/MediaBox` inherited
834
+ from a page-tree node without touching the other pages under it.
835
+ - `Page.bleed_box`, `Page.trim_box` and `Page.art_box` read and set the
836
+ production boxes of 14.11.2. Each reports the box in effect: the stated entry
837
+ intersected with the media box, since none of the three "shall ordinarily
838
+ extend beyond" the sheet, or the crop box they default to where the page
839
+ states nothing usable. The entry written to the file is the one that was set,
840
+ unclipped, exactly as `/CropBox` is written, and assigning `None` removes it.
841
+ The three are **not inheritable**, so a `/TrimBox` on a page-tree node is not
842
+ the page's trim box.
843
+ - `Page.get_box(boundary)` / `Page.set_box(boundary, rect)` take the
844
+ `PageBoundary` that `/ViewArea` and `/PrintArea` are written with, so code
845
+ holding one can ask the page for the geometry it names.
846
+ - The media box is the one box that must have an area and cannot be removed: an
847
+ empty one is replaced with US Letter when read, so accepting one when written
848
+ would make the assignment look as though it had never happened. The other
849
+ four may be written as given, since a reader intersects them and falls back.
850
+
851
+ ### Fixed
852
+
853
+ - **Five annotation subtypes could be given an appearance and nothing said so.**
854
+ `Annotation.generate_appearance` builds one for fifteen subtypes;
855
+ its docstring named ten, omitting `FreeText`, `Stamp`, `Caret`, `Text` and
856
+ `FileAttachment`, and `supported-features.md` omitted the last two. A
857
+ docstring listing ten of fifteen is worse than one listing none — it reads as
858
+ a complete answer. All three lists now agree, and a test holds them to each
859
+ other and to what the code actually produces, since this is drift that
860
+ returns.
861
+
862
+ - **Appending a page from another document silently corrupted it.**
863
+ `pages.insert` refused a foreign page, but `pages.add` accepted one and
864
+ copied the page dictionary alone. The content kept naming `/F1` while the
865
+ page carried no `/Resources` of its own, so the name resolved to the
866
+ **target's** `/F1` — text written in Courier-Bold came out in whatever font
867
+ the target happened to have under that name, with no error anywhere. pdfium
868
+ rendered it; only the wrong letterforms showed it. `add` now takes the same
869
+ import as `insert`, so the page brings its own resources.
870
+
871
+ - **An embedded composite font failed our own PDF/A and PDF/X font checks.**
872
+ Both looked for `/FontDescriptor` on the top-level font dictionary, where a
873
+ `Type0` font legitimately has none — table 121 does not give it one, and
874
+ 9.7.4 puts it on the descendant along with the program. Every CID-font
875
+ document was therefore reported as `missing FontDescriptor`, embedded or not,
876
+ and could not be made to validate. Both now follow `/DescendantFonts`, and
877
+ judge the program by the key its kind uses (table 126: `/FontFile3` for a
878
+ `CIDFontType0`, `/FontFile2` or `/FontFile3` for a `CIDFontType2`). A
879
+ composite font that really carries no program now reads as *not embedded*
880
+ rather than as missing a descriptor. The PDF/UA check already did this
881
+ correctly and is unchanged.
882
+
883
+ - **A section holding a graphics state open could not survive the next append.**
884
+ Found while building the above: content isolation saves and restores what a
885
+ page's existing content leaves behind, so appending an unbalanced `q` on its own
886
+ had its `Q` inserted before the very next fragment — the text meant to be inside
887
+ a rotated section landed outside it, and the page's stream ended up unbalanced
888
+ (poppler: "Restoring state when no valid states to pop"). A section now collects
889
+ what is drawn inside it and appends the whole thing as one balanced fragment,
890
+ which leaves isolation doing exactly what it did before.
891
+
892
+ - **`Form.flatten` said it flattened "all fields in the form".** It is
893
+ `Document.flatten`: a page's annotations are baked in with the fields, because
894
+ flattening is one operation in the engine and not two. The docstring now says
895
+ so, rather than suggesting a fields-only form of it that nothing implements.
896
+
897
+ - **A pattern used inside a form XObject was anchored to the page.** ISO 32000-1
898
+ 8.7.3.1 maps pattern space to the default coordinate space of the content
899
+ stream the pattern is *used in*, so a form's matrix is part of it. A tiling
900
+ pattern filled inside a form drawn at `1 0 0 1 5 5 cm` laid its tiles from the
901
+ page's origin and then clipped them to the form's box: the first column of
902
+ every row came out five units narrow and the rest sat five units off. MuPDF and
903
+ pdfium place the tiles from the form's origin and agree to the pixel; so do we
904
+ now, and so does a **shading** pattern, which was anchored the same wrong way.
905
+ A pattern used inside a pattern cell is anchored to the cell, one level further
906
+ in, as the same clause says. The CTM in force at the moment of the fill still
907
+ does not move the tiles.
908
+
909
+ - **A form XObject's text ran into the page's own with no separator at all.**
910
+ Headers, footers, stamps and anything a layout tool reuses live in form
911
+ XObjects, and their text is placed by a matrix of their own — so after reading
912
+ one the extractor said "nothing has been shown yet", and the next string on the
913
+ page got no separator: `inside a form` and `on the page` came out as
914
+ `inside a formon the page`. A form's text is now a line of its own, whichever
915
+ side of the page's text it is drawn on.
916
+
917
+ - **A text-positioning move that did not advance along the line produced a
918
+ space.** Two strings at the same position are stacked, not side by side, so
919
+ `under` drawn over `OVER` read as `under OVER`; `T*` with the default zero
920
+ leading and a plain `0 0 Td` did the same. A move that goes back to where the
921
+ last string started, or behind it, starts a new line, which is what MuPDF and
922
+ poppler both answer. Horizontal position is tracked for this, and `BT` resets
923
+ it as ISO 32000-1 9.4.1 requires — a `Td` in a new text object used to count
924
+ from wherever the previous one had left the pen, which could put a string to
925
+ the right of one that is in fact to its left.
926
+
927
+ - **A damaged bookmark tree took the whole document with it.** Four shapes of
928
+ damage refused to open at all: an `/Outlines` object that would not tokenise,
929
+ an outline list whose `/First` pointed at an object that is not there (which
930
+ ISO 32000-1 7.3.10 defines as a reference to null, not an error), an item that
931
+ was its own `/First` and `/Next`, and nesting past the internal depth ceiling.
932
+ A bookmark list is not the page content, and MuPDF, pdfium and poppler open
933
+ every one of these files and read their pages. The walk now ends where the
934
+ damage is, with a warning naming it, and the bookmarks above it are kept — a
935
+ **resource limit** still raises, because that ceiling is the caller's and not
936
+ the file's fault.
937
+
938
+ - **A cross-reference table that parsed but named nothing was trusted.** A table
939
+ truncated to its free entry led straight to "no PDF objects found in the
940
+ input", although the objects were still in the file and the reconstruction scan
941
+ would have found them — it ran only when the table failed to *parse*. The scan
942
+ runs for an empty table too, so the refusal only ever follows a reconstruction
943
+ that found nothing; a trailer that parsed keeps naming the catalog, so a stale
944
+ `trailer` fragment appended after `%%EOF` cannot take it over.
945
+
946
+ - **A `/Resources` or a `/Font` entry that is not a dictionary raised
947
+ `AttributeError` out of text extraction.** A damaged file may hold a number
948
+ where a font belongs; without metrics the text still comes out, which is what
949
+ every reference reader does with such a page.
950
+
951
+ - **Merging left the structure tree behind, so merged-in pages lost their
952
+ tagging.** Assembling an accessible document out of parts is most of the
953
+ reason to merge, and the parts arrived untagged: the pages came with their
954
+ `/P <</MCID 0>> BDC` marked content intact while the elements describing it
955
+ stayed in the source. Merged into a tagged document the result was worse than
956
+ untagged — it went on saying `/MarkInfo /Marked true`, so it advertised a
957
+ structure tree that covered only half its pages; merged into an untagged one,
958
+ there was no `/StructTreeRoot` at all and the marked content was orphaned.
959
+ (An earlier fix had stopped the imported page *claiming* a key it could not
960
+ index, which prevented it inheriting someone else's headings but left the
961
+ tagging lost.) The elements travel now: each page's entry in the source parent
962
+ tree is imported under a fresh key, the top of each imported subtree is
963
+ re-parented onto this document's `/StructTreeRoot` — spliced into its
964
+ `Document` element when both have one, since ISO 14289-2 8.2.5.2 wants a
965
+ single one at the root — and `/RoleMap` and `/ClassMap` are unioned. Taking a
966
+ *subset* of pages brings only the elements that describe those pages: a kept
967
+ element's marked-content ids are dropped when they belong to a page that
968
+ stayed behind, so nothing arrives describing content that is not there.
969
+
970
+ - **Every filled rectangle and every clip was one pixel too wide.** The
971
+ rasteriser had four rules for turning a span's floating-point `x` range into
972
+ pixel columns: glyph outlines and strokes used the pixel-centre rule, while
973
+ path fill, the clip mask and shading-pattern fill covered `floor(x_from)` to
974
+ `ceil(x_to)` *inclusive* — one column too many for any integer-aligned edge.
975
+ Rows had always been chosen by their centres, so the error was asymmetric: a
976
+ 1-point vertical rule rendered two pixels across and a 1-point horizontal rule
977
+ one pixel down, and a clip leaked a column of content past its right edge (a
978
+ zero-width clipping rectangle, which encloses nothing, let a whole column
979
+ through). Fills now keep the pixels whose centres are inside, as the glyph path
980
+ already did; clips keep every pixel of which the region covers any area, which
981
+ is conservative and what MuPDF, pdfium and poppler all do. Measured on thirteen
982
+ rectangles × fill and clip: of the twenty-three cases where all three
983
+ references agree with each other, they agreed with us on none before and on all
984
+ twenty-three now.
985
+
986
+ - **CCITT-compressed images decoded to noise.** Group 4 is how a scanned
987
+ bilevel page is almost always stored, and the decoder could follow hardly any
988
+ of it: of ten bitmaps encoded by libtiff, one came back correct — the
989
+ all-black one, where a single run per row hid the errors. Two causes, both in
990
+ the tables rather than the loop. The code words were wrong: the black table
991
+ was largely a copy of the white one (black run 8 carried the white run 8 code,
992
+ the black 64 make-up code was the white one), and both tables held codes that
993
+ prefixed other codes, which cannot happen in a Huffman set and makes a
994
+ one-bit-at-a-time read stop early. And the two-dimensional decoder knew only
995
+ V0, VR1, VL1, horizontal and pass, treating everything that begins `0000` as
996
+ an unsupported extension — so VR2, VL2, VR3 and VL3 could never be read —
997
+ while `b1` was found as the first pixel of the opposite colour rather than the
998
+ first *changing element* of it. The tables are regenerated from ITU-T T.4,
999
+ including the 1792–2560 make-up codes that were missing altogether (every page
1000
+ scanned above 300 dpi needs them), and the row decoder is rewritten in terms of
1001
+ changing elements, the way the standard states it. **Group 3 is decoded now
1002
+ too**: `K >= 0` used to take a fast path that returned the encoded bytes as if
1003
+ they were the bitmap. Measured against MuPDF, pdfium and poppler, which agree
1004
+ with each other and with the source on all twenty documents of the matrix: we
1005
+ agreed on two, and now on twenty.
1006
+
1007
+ - **A rectangle of noise where an image failed to decode.** When a filter could
1008
+ not run, the still-compressed bytes were painted as samples — which looks like
1009
+ data, and so is worse than the picture being absent. An image whose stream
1010
+ cannot be decoded is now reported and skipped, with a warning. DCT and JPEG
1011
+ 2000 are unaffected: their bytes are passed through on purpose, and the
1012
+ renderer decodes them itself.
1013
+
1014
+ - **A text field authored with an embedded font rendered blank in every
1015
+ engine.** `add_text_field(font=…)` embeds a Type0 (CID) font so a non-Latin
1016
+ value renders, and the appearance it bakes shows that value's CIDs. The font
1017
+ graph, though, was written the moment the field was created — before the value
1018
+ had been encoded — and nothing rewrote it afterwards, so the embedded program
1019
+ kept no glyph at all, `/W` was an empty array and `/CIDToGIDMap` held the
1020
+ single entry CID 0. The appearance asked for CIDs the font could not draw:
1021
+ pdfium, MuPDF and this library all rendered nothing, while all three still read
1022
+ the field *value* back correctly. The program, widths and CID map are now
1023
+ rewritten once every widget has asked for its glyphs — the refresh the
1024
+ page-text path has always done — and the widget's `/MK` background and `/BS`
1025
+ border are painted under the value, so the box is visible whether or not the
1026
+ value draws anything.
1027
+
1028
+ - **Text hidden by optional content was extracted anyway, and whitespace-only
1029
+ text came back as raw CID bytes.** One cause: `extract_page_text` read an
1030
+ *empty* extraction as a parse failure and fell back to the best-effort pass,
1031
+ which reads every string in the stream as bytes and does not track the `/OC`
1032
+ state. So a page whose every word sat in an optional-content group that is off
1033
+ extracted that text regardless, and a page whose only text was a space came
1034
+ back as `'\x00\x01'` — binary where a caller expects characters — because
1035
+ extraction stripped the whitespace and so produced the empty string that was
1036
+ mistaken for failure. An empty result is an answer now: the parser reports
1037
+ through `showed_text` whether the stream asked for any text at all, which is
1038
+ what tells a page that draws none from a stream the parser could not follow,
1039
+ and the recovery pass still runs for the latter. Surrounding whitespace is kept
1040
+ as well, which is what MuPDF and pdfium return — extraction reports what the
1041
+ page draws.
1042
+
1043
+ - **A font subset kept every table it was not erasing glyphs from, roughly
1044
+ doubling the embedded program.** One line of text in Arial Unicode (50377
1045
+ glyphs) embedded 869768 bytes, of which the 12 glyphs actually used were 2636:
1046
+ `hdmx` was 302288 bytes, `LTSH` 50381, `GSUB` 66612 and `DSIG` 9648. None of
1047
+ them is read for an embedded font — layout tables because a content stream
1048
+ already carries positioned glyph codes, device metrics and hinting aids because
1049
+ the program is rasterised from `glyf` and `hmtx`, and `DSIG` because it signs
1050
+ the *unmodified* font and so signs nothing after erasure. They are dropped with
1051
+ the glyphs now, which is the set fontTools' own subsetter drops by default,
1052
+ less the colour and bitmap tables that a consumer may draw from. That font's
1053
+ program goes from 869768 to 428568 bytes (142651 to 65364 deflated) and the
1054
+ page renders byte-identically, with the same agreement with MuPDF and pdfium as
1055
+ before. `optimize_resources(subset_fonts=True)` benefits too: it was shrinking
1056
+ the file only because the stream deflates well.
1057
+
1058
+ - **Converting to PDF/A-1 or -2 could not remove an attachment added in the same
1059
+ session.** Those levels prohibit embedded files (ISO 19005-1 6.1.11), but the
1060
+ removal sat inside the catalog's `/Names` branch — and a document has no
1061
+ `/Names` dictionary until a save writes one, so an attachment added in this
1062
+ session survived the conversion, which only warned about it and produced a file
1063
+ veraPDF rejects. The attachments and the `/AF` array that points at them are
1064
+ dropped now whether the document has been saved or not, so the conversion
1065
+ produces a conformant file instead of a warning. veraPDF passes PDF/A-1B and
1066
+ -2B output that started with an attachment.
1067
+
1068
+ - **A base PDF/A-4 document whose attachment was not a PDF at all was reported
1069
+ valid.** ISO 19005-4 6.9 requires *every* embedded file to conform to PDF/A-1,
1070
+ -2 or -4 — veraPDF puts it as "All of the embedded files shall be compliant
1071
+ with ISO 19005-1, 19005-2 or 19005-4" and fails such a file. The check read it
1072
+ as a rule about PDF payloads only and skipped anything else, so a spreadsheet
1073
+ attached to a base part-4 file passed. The existence of PDF/A-4f settles the
1074
+ reading: that level is defined to carry embedded files of any type and
1075
+ *requires* an `/EmbeddedFiles` key, which would add nothing if the base level
1076
+ already permitted arbitrary attachments. A non-PDF payload and one that cannot
1077
+ be read are both reported now, naming `4f` as the level that allows them, and
1078
+ the check reads attachments added in the session as well as the object graph —
1079
+ the same blind spot the part-4 `/Info` check had. `4f` is unaffected, and a
1080
+ conforming PDF/A payload still passes.
1081
+
1082
+ - **Every PDF/UA-2 document the converter produced was rejected by veraPDF, and
1083
+ our own check passed it.** ISO 14289-2 8.2.5.2, by way of ISO 32000-2 Annex L
1084
+ and ISO/TS 32005, requires the structure tree root to contain a single
1085
+ `Document` structure element as its only child; veraPDF states it three ways —
1086
+ exactly one `Document`, no `Hn` under the root, no `P` under the root — and the
1087
+ authored headings and paragraphs were attached to `/StructTreeRoot` itself.
1088
+ `convert_to_pdfua(part=2)` now adds that element, moving whatever the root held
1089
+ into it in order and re-parenting each child, and leaves a root that already
1090
+ has one alone so converting twice changes nothing. `validate_pdfua(part=2)`
1091
+ reports a root holding none, more than one, or anything other than a
1092
+ `Document`. Part 1 has no such rule: its output never had the wrapper and still
1093
+ does not. veraPDF now passes both parts.
1094
+
1095
+ - **An attachment was never declared an associated file, so PDF/A-3 output was
1096
+ rejected.** ISO 19005-3 6.8 and ISO 19005-4 6.9 require every embedded file to
1097
+ be associated with the document or a part of it, which an `/AF` array
1098
+ expresses; veraPDF checks it as `isAssociatedFile`. Each file specification
1099
+ carried an `/AFRelationship` saying what the relationship was, but nothing put
1100
+ the specification in an `/AF` array to say there was one — so **veraPDF failed
1101
+ every PDF/A-3 document with an attachment**, relationship and all. The catalog
1102
+ now carries an `/AF` array naming each attachment's file specification, for
1103
+ every document as `/AFRelationship` and `/Subtype` already were, and the array
1104
+ is dropped with the last attachment so it never dangles. veraPDF now passes
1105
+ our PDF/A-3B and PDF/A-4f output with an attachment.
1106
+
1107
+ - **An attachment added without a MIME type made every PDF/A-4f file fail
1108
+ veraPDF.** ISO 19005-3 6.8 and ISO 19005-4 6.9 require a media type in an
1109
+ embedded file stream's `/Subtype` and name `application/octet-stream` as the
1110
+ value to use when the type is not known; `add_attachment` without `mime=`
1111
+ wrote no `/Subtype` at all, so the level that exists to carry attachments
1112
+ rejected every document that had one. The same file with an explicit
1113
+ `mime="text/plain"` passed, so only the default was missing. It is written for
1114
+ every attachment now, PDF/A or not, on the same footing as `/AFRelationship`.
1115
+ Reading it back follows the rule already used for `/AFRelationship`'s
1116
+ `Unspecified`: a declared `application/octet-stream` says nothing an absent
1117
+ `/Subtype` did not, so `FileSpecification.mime_type` reports `None` for it
1118
+ rather than a type the producer never chose. veraPDF now passes our PDF/A-4f
1119
+ output with and without an explicit type.
1120
+
1121
+ - **Every PDF/A-4 file the converter produced was rejected by veraPDF, and our
1122
+ own validator passed it.** ISO 19005-4 clause 6.1.3 forbids `/Info` in the
1123
+ trailer unless the catalog carries a `/PieceInfo`, and allows nothing but
1124
+ `/ModDate` in it where it is allowed — part 4 dropped the `/Title` requirement
1125
+ that parts 1 to 3 have and replaced it with this prohibition. The conversion
1126
+ did the opposite: its step 2 is "ensure /Info has a /Title", and it added one.
1127
+ `convert_to_pdfa` now drops the dictionary for part 4 (keeping a `/ModDate`
1128
+ where a `/PieceInfo` allows it), including entries `doc.info` cannot see — an
1129
+ `/Info` value with no text form is deliberately kept by the metadata view and
1130
+ would otherwise have survived. `validate_pdfa` reports both 6.1.3 rules and no
1131
+ longer demands a title for part 4, reading the pending `info` view as well as
1132
+ the object graph so a document about to be written non-conformant is not passed.
1133
+ veraPDF now passes our PDF/A-4, `4e` and `4f` output, alongside 1B, 2B, 3B
1134
+ and 2A.
1135
+
1136
+ - **A document could not be reopened with its own owner password once the user
1137
+ password reached 31 characters.** Every revision up to 4 was affected -- RC4
1138
+ 40-bit, RC4 128-bit and AES-128 -- and not only opening: `decrypt()` and
1139
+ `change_passwords()` failed the same way, so the owner could neither remove nor
1140
+ rotate the password of a file this library had written. Algorithm 7 recovers
1141
+ the *padded* user password from `/O`, always exactly 32 bytes, and the padded
1142
+ form is what the key derivation takes; the code turned it back into plaintext
1143
+ first by searching for the two bytes the padding begins with. A 31-byte
1144
+ password leaves exactly one of them and a 32-byte or longer one leaves none, so
1145
+ the search failed, the recovered password fell back to the empty string and the
1146
+ derived key was wrong. A user password merely containing those two bytes was
1147
+ cut in the wrong place. The recovered bytes are now used as they stand. qpdf,
1148
+ pdfium and MuPDF all open the affected files with the owner password at every
1149
+ length, so the `/O` written was correct throughout and only the reading was
1150
+ wrong; AES-256 was never affected, checking `/OE` directly instead.
1151
+
1152
+ - **`flatten()` raised `IndexError` on a streamed document.** It read
1153
+ `self.page_contents[i]` -- the content *cache*, which a document opened with
1154
+ `open_streaming` has not filled -- instead of asking for the page, so it failed
1155
+ for every page that had an annotation or a form field to flatten. A page with
1156
+ none reaches a `continue` before that line, which is why flattening a plain
1157
+ streamed document worked and flattening a useful one did not. The same defect
1158
+ `extract_text` once had. It now asks for the content it is about to edit, so
1159
+ only the pages that are actually flattened are decoded and a document with
1160
+ nothing to flatten stays lazy. With this the eager and lazy load paths agree on
1161
+ every operation measured: 28 operations over 18 documents, no divergence.
1162
+
1163
+ - **Saving a document opened with `open_streaming` deleted its attachments.**
1164
+ `from_file_lazy` never read them, and `_sync_attachments_to_cos` reads an empty
1165
+ mapping as *the caller removed them all* -- so the save dropped the
1166
+ `/Names /EmbeddedFiles` tree and left the `/Filespec` and `/EmbeddedFile`
1167
+ objects unreachable: 1023 bytes in, 975 out, `attachments == []` on reopening,
1168
+ with no warning and no exception, with or without an intervening edit. They
1169
+ were invisible before any save too (`attachments`, `embedded_files` and
1170
+ `get_embedded_file` all came back empty), along with their mime type,
1171
+ description and dates.
1172
+
1173
+ - **A signed document reported no signatures in streaming mode.** `signatures`
1174
+ gave 1 through `Document(path)` and 0 through `Document.open_streaming(path)`
1175
+ for the same file, so a caller asking whether a document is signed was told
1176
+ "no" about a signed one. The bytes were never at risk -- `can_append` and
1177
+ `existing_signatures_bind` read the COS graph rather than that list, so the
1178
+ save still appended a revision and left the signature intact.
1179
+
1180
+ Both are read at load now, beside the metadata and the outlines; only page
1181
+ content and the resources a page reaches are deferred. The payloads are
1182
+ bounded by the same `PdfLoadLimits` as any other stream.
1183
+
1184
+ - **Converting a document to PDF/A twice left the first conversion in the file.**
1185
+ `convert_to_pdfa` registered a new XMP stream, a new ICC profile, a new
1186
+ `/OutputIntent` and a new `/OutputIntents` array each time it ran and pointed
1187
+ the catalog at them, abandoning the previous four: converting twice left 4
1188
+ unreachable objects and 1055 bytes of dead streams, a third conversion 8 and
1189
+ 2110, without bound. `convert_to_pdfua` leaked its predecessor's XMP packet the
1190
+ same way, and so did an ordinary edit of `xmp_metadata`. The catalog entries are
1191
+ replaced where they stand now, so repeated conversion leaves nothing behind and
1192
+ the file size does not move; a level change still rewrites the packet. The ICC
1193
+ profile is the exception: a `/DestOutputProfile` stream can also be a page's
1194
+ `/ICCBased` colour space, so one holding the right bytes already is kept and one
1195
+ holding other bytes is left alone with a new profile written beside it. Nothing
1196
+ about the output was invalid before -- `optimize()` swept the dead objects up --
1197
+ it was waste in files written to be archived.
1198
+
1199
+ - **A page whose `/MediaBox` had no area could not be rendered.** It raised
1200
+ `PdfValidationException("page box must have positive area")`, and `Page.rect`
1201
+ reported the degenerate rectangle as though it were the page's size; the same
1202
+ went for coordinates that were not numbers, which silently became zeroes. Such
1203
+ a `/MediaBox` is now replaced with US Letter -- absent, not an array, too
1204
+ short, not numbers, not finite, or no area -- which is what pdfium and MuPDF
1205
+ both answer for every one of those shapes, MuPDF in the media box itself and
1206
+ not merely when drawing. Corners are ordered too, so `[612 792 0 0]` reads as
1207
+ the rectangle it describes, again matching MuPDF; a rectangle at negative
1208
+ coordinates is kept, as both references keep it. Our `Page.rect` now equals
1209
+ MuPDF's `mediabox` on all nine shapes tested. The `/MediaBox` written back is
1210
+ still the one the file held -- MuPDF and qpdf keep a degenerate one as well.
1211
+
1212
+ - **An attribute set on a name the object does not have was accepted and lost.**
1213
+ `d.pages[0].rotate = 270` -- the property is `rotation` -- raised nothing and
1214
+ did nothing: `Page` is rebuilt on every access, so the value went to a
1215
+ throwaway object and was gone before the next statement, and the document saved
1216
+ upright. `Page`, `PageCollection`, `Annotation` and its subclasses,
1217
+ `AnnotationCollection`, `Field`, `Form`, `UnsignedContent`,
1218
+ `UnsignedContentAbsorber`, `OutlineItem` and `OutlineCollection` now declare
1219
+ their attributes with `__slots__`, as `Layer`, `LayerConfiguration`,
1220
+ `LayerCollection` and `pages._LayerSection` already did, so Python refuses an
1221
+ undeclared name with `AttributeError`. The properties and the documented
1222
+ writable attributes are unaffected. `aspose_pdf.utils._object_to_dict`, which
1223
+ read `__dict__`, reads the declared names for such an object instead of
1224
+ falling back to a repr string -- which is what its docstring already promised.
1225
+
1226
+ - **A crop box reaching outside the media box was used as it stood.** ISO 32000-1
1227
+ Table 30 says it "shall be intersected with the media box" to determine what of
1228
+ the page is visible; without that, a page whose crop box was larger than its
1229
+ media box rendered at the crop box's size -- 2000x2000 for a Letter page --
1230
+ inventing sheet the document does not describe, and one overhanging an edge
1231
+ placed its content against a corner that is not on the page. The intersection
1232
+ is taken now, wherever the value is used: rendering, text export, stamp
1233
+ placement and `Page.crop_box`, which reports the box in effect. An empty crop
1234
+ box no longer makes a page unrenderable (it used to raise "page box must have
1235
+ positive area"), and one lying entirely off the page gives the media box too.
1236
+ The entry written to the file is still the one that was set, unclipped, as qpdf,
1237
+ pdfium and MuPDF all keep it. Measured across six shapes, the rendered size now
1238
+ matches both pdfium and MuPDF everywhere the two agree, and MuPDF where pdfium
1239
+ reports a page of no size.
1240
+
1241
+ - **One 16-bit sample got three different 8-bit values depending on which path
1242
+ reduced it.** Coming in from a PNG it was shifted down to its high byte, going
1243
+ out to one it was truncated (`value * 255 // 65535`), and inside a JPEG 2000
1244
+ codestream it was rounded -- the same question answered three ways, each a
1245
+ step from the others, so the same pixel embedded and then exported did not
1246
+ come back as itself. There is one rule now, in `engine/image_resample`: the
1247
+ rounded linear rescale, which ISO 15948 (PNG) 10.4 calls the most accurate way
1248
+ to reduce a sample's depth, against the shortcuts it calls "slightly less
1249
+ accurate" but faster -- a trade worth making for a screen, not for samples
1250
+ stored in a document or written to a file the caller keeps. Nothing changes at
1251
+ 1, 2, 4 or 8 bits, where the rescale is exact, and an 8-bit value widened to
1252
+ 16 the correct way (`value * 257`) still comes back unchanged. A 16-bit image
1253
+ can now read 1/255 away from pdfium (which truncates) or MuPDF (which shifts),
1254
+ which is less than either of them is from the true value; the shift also
1255
+ flattened every value from `0xFF00` up onto 255, losing the top of the range.
1256
+
1257
+ - **A file cut off inside a stream failed to open at all.** Truncation is the
1258
+ commonest damage a transfer does to a PDF, and a `/Length` that no longer
1259
+ reaches `endstream` was already treated as damage to recover from -- but a
1260
+ missing `endstream` raised `endstream not found for stream` and failed the
1261
+ whole document, so a three-page file cut in its first content stream gave
1262
+ nothing back although the page dictionary before the cut was intact. The
1263
+ stream is now the bytes that are there, logged as such. qpdf and MuPDF both
1264
+ read such a file; measured across cuts, the recovered page counts are qpdf's
1265
+ and the per-page text is MuPDF's, and for the one synthetic case where the
1266
+ stream is not at the end of the file our bytes are MuPDF's exactly. A cut
1267
+ landing inside an *object* rather than a stream is still refused, which is
1268
+ what qpdf and MuPDF do with it.
1269
+
1270
+ - **`unpack_samples` was documented as discarding half of every 16-bit sample.**
1271
+ Its docstring said 16-bit "keeps the high byte"; the code returns the whole
1272
+ big-endian value, which is what its callers need -- a `/Mask` colour-key range
1273
+ is expressed in the image's own component range, and the conversion to 8 bits
1274
+ is `to_8bpc_bytes`' job. The test that was meant to pin this asserted the high
1275
+ byte on `0x1234`, a value where both rules give `0x12`, so it passed either
1276
+ way; it now uses values that tell them apart. Measured over all 65536 values
1277
+ on a 16-bit greyscale image, our renderer and image export agree with pdfium
1278
+ exactly (bar 8 values where pdfium's float arithmetic lands a step away),
1279
+ while MuPDF answers `v >> 8` throughout -- one step higher over half the
1280
+ range. The rules differ by at most 1/255 and neither is the rounded value, so
1281
+ the divergence is documented rather than changed.
1282
+
1283
+ - **A document with no pages reported one page.** Six shapes of page-less input
1284
+ -- a bare header, a header and `%%EOF`, a header followed by garbage, a header
1285
+ with one object that is not a page, a catalog with no `/Pages`, and a
1286
+ well-formed but empty page tree -- were each answered with `len(doc.pages) == 1`
1287
+ and a blank Letter sheet invented on load, so a caller that checked the count
1288
+ was told the file had a page and then rendered a sheet the file never
1289
+ described. qpdf and pdfium refuse all six; MuPDF refuses the three with no PDF
1290
+ object in them and reports 0 pages for the three that have a structure to
1291
+ read. That split is the rule now: an input with no object in it raises
1292
+ `PdfParseException`, and a document whose page tree is empty or absent reports
1293
+ no pages -- it still opens, saves (qpdf and MuPDF read 0 pages from what we
1294
+ write), encrypts and can be authored into, `validate()` rejects it, and
1295
+ `repair()` is where a page gets added. `SimplePdf.from_bytes_safe`, the
1296
+ tolerant loader, repairs as before.
1297
+
1298
+ - **A JPEG 2000 codestream with packed packet headers did not decode.** A packet
1299
+ normally carries its header in front of its body; it may instead gather every
1300
+ header into a `PPT` segment in a tile header or a `PPM` segment in the main
1301
+ header (ISO 15444-1 A.7.4, A.7.5), and the bundled decoder refused such a file
1302
+ outright — so on a default install, without the `images` extra and its
1303
+ OpenJPEG, the image was simply missing. Both are read now, with the segments
1304
+ reassembled by their `Zppt`/`Zppm` indices and a `PPM` stream's `Nppm` runs
1305
+ handed to the tile-parts in codestream order. The test fixtures were made by
1306
+ moving a real codestream's packet headers, one packet at a time, and OpenJPEG
1307
+ decodes every one of them to the same pixels as the original.
1308
+
1309
+ - **A variable TrueType always drew its default master.** Variable CFF2 was
1310
+ instanced; `glyf` fonts, whose deltas live in `gvar`, were not read at all. A
1311
+ modern system font ships as one variable file rather than four static faces,
1312
+ so a substitute asked for Bold came back Regular. New `engine/gvar.py` applies
1313
+ the deltas -- scaled by each region's weight at the requested coordinates,
1314
+ with the points a tuple omits inferred from their neighbours (OpenType's IUP)
1315
+ and a composite's deltas moving its components -- and the axis machinery
1316
+ `fvar`/`avar` now lives in `engine/font_variations.py`, shared with the CFF2
1317
+ path. Checked against fontTools' instancer on eight combinations of real
1318
+ variable fonts and axes: over 2 400 glyphs and 100 000 points, no contour
1319
+ differed in structure and no coordinate by more than 1.5 font units, which is
1320
+ fontTools' rounding and not a difference in the deltas.
1321
+
1322
+ - **A Bézier stopped being a Bézier.** The interpreter turned every curve into
1323
+ twelve line segments and dropped the control points, which cost twice. The
1324
+ raster kept a fixed flat on each segment — a 250 pt circle was off by half a
1325
+ point at *any* resolution, where pdfium and MuPDF have no error — and SVG
1326
+ export, whose point is a resolution-independent facsimile, wrote those
1327
+ segments out as `L` commands. Flattening now takes its step count from the
1328
+ curve's length in device pixels, so the error stays under a fifth of a pixel
1329
+ at every scale, and each subpath records the cubics its points stand in for so
1330
+ the exporter writes them as `C`. On a page of 24 stroked circles the SVG went
1331
+ from 20 760 bytes with 1 152 `L` commands to 7 138 bytes with 96 `C`, and
1332
+ MuPDF — which reads SVG as well as PDF — renders the export and the original
1333
+ with no pixel differing beyond antialiasing, where the old export differed on
1334
+ 0.56% of the page.
1335
+
1336
+ - **Replacing or hiding an image did not change the document.**
1337
+ `ImagePlacement.replace` set the bytes on the object in hand and `hide` set a
1338
+ flag on it, so a save afterwards still wrote the picture that was there — the
1339
+ placement had no reference to the document at all. A placement collected by
1340
+ `ImagePlacementAbsorber` is now bound to the document it came from, and both
1341
+ methods redefine the image XObject while leaving the page's drawing operations
1342
+ alone, so the new picture appears exactly where the old one was. Hiding
1343
+ redefines the image as a single fully transparent pixel. Both follow MuPDF's
1344
+ `Page.replace_image` / `Page.delete_image`, are per-object like MuPDF's, and
1345
+ both now return whether the document was changed. pdfium and MuPDF render
1346
+ every case identically, and a replacement leaves no second copy of the image
1347
+ behind: the file comes out the size it went in.
1348
+
1349
+ - **An XMP property whose namespace is declared on its own element was read as
1350
+ absent.** Found while cross-checking PDF/X identification against pikepdf: the
1351
+ packet reader matched `name>`, so
1352
+ `<pdfxid:GTS_PDFXVersion xmlns:pdfxid="...">PDF/X-4</...>` — how pikepdf
1353
+ writes a property in a namespace nothing else in the packet uses — reported the
1354
+ property as missing, which rejected a conforming file written by another tool.
1355
+ The opening tag may now carry attributes. This is the reader the PDF/UA
1356
+ identification checks share.
1357
+
1358
+ - **A justified line's gaps were read back as two spaces each.** Found while
1359
+ building the above: a gap in a justified line holds a space *and* a
1360
+ displacement — the space is the text, the displacement is the slack that
1361
+ spreads the line — and the text extractor counted both, so `extract_text()`
1362
+ returned `"one two three"` for a line that says `one two three`.
1363
+ pdfminer.six reads such a line the same way; pdfium does not. A gap that
1364
+ follows a space now adds nothing, since the word boundary is already there.
1365
+
1366
+ - **`PdfFileSecurity.change_password(permissions=…)` could never work.** The
1367
+ facade has offered `permissions` and `algorithm` since it was written and
1368
+ forwarded them to `Document.change_passwords`, which has neither — so either
1369
+ one returned `False` with a `TypeError` in `last_exception`, swallowed by the
1370
+ facade's own report-rather-than-raise contract, which is why no test caught
1371
+ it. `Document.change_passwords` now takes both, `None` keeping what the
1372
+ document has, so a password change can deliberately re-permission or re-cipher
1373
+ a document instead of needing a decrypt and a re-encrypt.
1374
+
1375
+ ### Added
1376
+
1377
+ - **Stamps.** A document could not be stamped: `SimplePdf.set_watermark` set a
1378
+ field only the retired V0 writer read, so through `Document` it saved a file
1379
+ with no watermark in it. `TextStamp`, `ImageStamp` and `PageNumberStamp` now
1380
+ go on through `Page.add_stamp` and `Document.add_stamp(stamp, pages=...)`,
1381
+ with opacity, rotation, zoom, alignment and indents, and `background` to put
1382
+ the mark under the page's own content. A stamp is a form XObject in its own
1383
+ `q`/`Q`, placed inside the box a reader shows and with the page's `/Rotate`
1384
+ undone so it stands upright; one stamp on many pages is one object. The
1385
+ behaviour and the matrices are qpdf's `--overlay`/`--underlay`, and every
1386
+ placement and opacity was read back out of pdfium's and MuPDF's rasters.
1387
+
1388
+ - **The standard fonts' real metrics.** `StandardFonts.get_glyph_width`
1389
+ answered 600 for every glyph of every font, so text set in a standard font
1390
+ could not be measured -- a centred `"Page 1 of 10"` was out by a quarter of
1391
+ its length. New `engine/std_metrics.py` carries the advance widths and the
1392
+ face bounds of all fourteen, indexed by the code this library writes for
1393
+ each. Read from MuPDF's built-in faces and checked against pdfium: every
1394
+ glyph lands within 0.0002 pt of where pdfium puts it.
1395
+
1396
+ - **How a document asks to be opened and shown.** `/PageMode`, `/PageLayout`,
1397
+ `/OpenAction` and `/ViewerPreferences` survived a load and save but could
1398
+ not be read or written. `Document.page_mode`, `Document.page_layout`,
1399
+ `Document.open_action` (a `Destination` or an `Action`) and
1400
+ `Document.viewer_preferences` now cover them, with `PageMode`, `PageLayout`,
1401
+ `ReadingDirection`, `PrintScaling`, `DuplexMode` and `PageBoundary` for the
1402
+ values ISO 32000-1 tables 28 and 150 allow. An entry that is missing, of the
1403
+ wrong type, or outside those lists reads as its default, as pdf.js and
1404
+ PDFBox resolve it; `/PrintPageRange` follows pdf.js and is spoken in 0-based
1405
+ page indices. Both directions were checked against pdf.js, PDFBox and MuPDF.
1406
+
1407
+ - **Layers that exclude one another.** `/RBGroups` -- the radio buttons of a
1408
+ viewer's layers panel -- was read into a configuration but never enforced,
1409
+ so switching one layer on left the others of its group on and the saved file
1410
+ said so. `LayerCollection.make_exclusive` declares such a group,
1411
+ `exclusive_groups` lists them, and `Layer.visible = True` now switches off
1412
+ every other member of each group naming that layer, the way pdf.js and MuPDF
1413
+ do; switching one off leaves the rest alone. Removing a layer drops it from
1414
+ the arrays instead of leaving them naming a group that is gone.
1415
+
1416
+ - **Long-term validation for encrypted documents.** `Document.add_ltv` and
1417
+ `add_document_timestamp` refused an encrypted document; the store and the
1418
+ timestamp field are now written through the encrypting writer with the
1419
+ file's own key, and validation reads the store back with it, so a
1420
+ password- or certificate-protected file reaches PAdES-LTA. Checked with
1421
+ pyHanko on AES-256, AES-128, RC4 and certificate-recipient documents.
1422
+
1423
+ - **A found text fragment knows where it is.** `TextFragment` gains `rect`
1424
+ and `quads` in default user space, `font_name`, `font_size` and `color`,
1425
+ filled in when a page (or a document's pages) is visited. The boxes are the
1426
+ redactor's own locator's, checked against MuPDF and pdfium; text that cannot
1427
+ be tracked to a single run is left unplaced rather than boxed wrongly.
1428
+
1429
+ - **FDF and XFDF.** `Form.export_fdf`/`export_xfdf` write a form's data and
1430
+ `Form.import_fdf`/`import_xfdf` fill a form from it, redrawing the fields
1431
+ and applying FDF's field and widget flags. Checked against Apache PDFBox in
1432
+ both directions; a signature's value is never carried.
1433
+
1434
+ - **Form fields say more than their value.** `Field` gains `partial_name`,
1435
+ `alternate_name`, `mapping_name`, `flags`, `read_only` and `required`
1436
+ (settable), `no_export`, `default_value`, `max_length`, `multiline`,
1437
+ `password`, `comb`, `options`, `multi_select`, `editable`,
1438
+ `export_values`, and `widgets` with each widget's page and rectangle.
1439
+ Inheritable attributes come from the nearest ancestor; every property was
1440
+ compared with pdf.js, pdfium, qpdf and MuPDF.
1441
+
1442
+ - **Page labels.** `/PageLabels` was neither read nor written. `Page.label`
1443
+ returns the label a viewer shows (`"iii"`, `"A-1"`, or `None` without
1444
+ labels), and `Document.page_labels` maps the page each range starts at to a
1445
+ `PageLabel(style, prefix, start)` that can be set and deleted. A malformed
1446
+ tree reads as most of pdfium, qpdf, pdf.js and MuPDF read it, and letters
1447
+ repeat as the specification says. Inserting and deleting pages moves the
1448
+ ranges as MuPDF does; merged and extracted pages keep their labels as with
1449
+ qpdf's `--pages`.
1450
+
1451
+ - **Export settings that were silently ignored now work.** `HtmlSaveOptions`
1452
+ and `MarkdownSaveOptions` reached `Document.save` with everything but
1453
+ `split_into_pages` / `extract_images` dropped. Figures now go to files when a
1454
+ folder is named (`resources_directory`; `image_directory` or
1455
+ `resources_directory_name`), each image once and linked by a relative URL;
1456
+ `markdown_format` selects GFM or CommonMark (tables as HTML blocks);
1457
+ `max_distance_between_text_lines` sets where lines stop joining a paragraph
1458
+ (default now 1.6 font sizes, the value the export always used); and
1459
+ `use_area_clipping` leaves out content anchored outside the crop box, as
1460
+ MuPDF does. The XPS/APS intermediate-file settings raise, since no such file
1461
+ is produced. The same controls are keyword arguments of `to_html`,
1462
+ `to_markdown`, `save_as_html` and `save_as_markdown`.
1463
+
1464
+ - **Signing through `Document`.** `Document.sign(field, certificate=...,
1465
+ private_key=...)` fills a signature field -- or, with no field named, an
1466
+ invisible one it adds -- with an `adbe.pkcs7.detached` or, with
1467
+ `pades=True`, a CAdES (PAdES-B) signature, optionally timestamped from a
1468
+ network or local authority (PAdES-T), with an embedded chain, reason,
1469
+ location and contact, or as a certifying signature (`certify=`).
1470
+ `Document.add_ltv(certificates=..., crls=..., ocsp_responses=...)` adds the
1471
+ signatures' validation material and any given to a `/DSS` (PAdES-LT), and
1472
+ `Document.add_document_timestamp(...)` appends an RFC 3161 document
1473
+ timestamp (PAdES-LTA). All three are made by `save()`, in call order, each
1474
+ as a revision appended to the bytes the save writes -- incrementally when
1475
+ the document is signed already -- so every earlier signature stays valid;
1476
+ the document is then reloaded from the file it wrote, so `signatures` lists
1477
+ the new ones and a later save appends to them. Arguments are checked when
1478
+ the call is made: a field that does not exist, is not a signature field or
1479
+ is signed already, a key that is not the certificate's, a key type the
1480
+ chosen encoding cannot sign with, a certification on a signed document.
1481
+ Encrypted documents are signed, stored and timestamped with their own key;
1482
+ signing a document being encrypted for certificate recipients is refused,
1483
+ since it could not be reopened after the save. Every combination checked in pyHanko (intact, valid, trusted, whole
1484
+ file covered, benign modification levels) and opened in pdfium and MuPDF.
1485
+
1486
+ ### Fixed
1487
+
1488
+ - **An embedded Standard-14 font was not an embedded font.** PDF/A conversion
1489
+ attaches a bundled metric-compatible substitute, which is an sfnt, as
1490
+ `/FontFile2` -- but left the dictionary saying `/Subtype /Type1`, and a Type 1
1491
+ font's program is `/FontFile` or `/FontFile3`, never `/FontFile2` (ISO 32000-1
1492
+ table 122). veraPDF 1.30.2 read that as "the font program is not embedded",
1493
+ correctly, so *every* PDF/A file this library produced failed the one rule the
1494
+ embedding exists to satisfy -- while this library's own check looked for any
1495
+ `FontFile*` key and reported the document compliant. Such a font is now
1496
+ written as `/TrueType`, and the check requires the program under the key its
1497
+ kind uses. pdfium renders the converted page identically, pixel for pixel.
1498
+
1499
+ - **Fonts were looked for on pages only.** The embedding pass and the PDF/A
1500
+ check both walked page `/Resources`, so a font reachable only through the
1501
+ AcroForm's `/DR` -- the `/Helv` a form field's appearance is drawn with -- an
1502
+ annotation's appearance stream, or a form XObject's own resources was neither
1503
+ embedded nor reported. Both now walk every font used for rendering, and each
1504
+ complaint says where the font was found.
1505
+
1506
+ - **PDF/UA conversion left fonts unembedded**, though ISO 14289-1 7.21.4.1
1507
+ requires them embedded and this library's own PDF/UA check reported it. It
1508
+ embeds them now, as the PDF/A conversion does.
1509
+
1510
+ With these three, veraPDF's font rule passes on every sample
1511
+ `scripts/write_conformance_samples.py` writes, for all seven PDF/A levels and
1512
+ both PDF/UA parts.
1513
+
1514
+ ### Removed
1515
+
1516
+ - **The second writer.** `SimplePdf` had two: the COS writer for a document with
1517
+ an object graph, and an older one for a document assembled in memory. Nothing
1518
+ the public API does produces the second kind — every load builds a graph,
1519
+ malformed input included — which is how features only that writer knew about
1520
+ came to sit in the engine doing nothing (see the entry below). A document with
1521
+ no graph is now given one and serialised like any other, and the 511-line
1522
+ writer is gone. Six of the seven shapes it used to serialise — pages, text,
1523
+ metadata, an outline, an attachment, an empty page — come out of qpdf, pdfium
1524
+ and MuPDF with identical facts. The seventh differs on purpose: writing
1525
+ `encrypted = True` by hand no longer encrypts, because that field says how a
1526
+ document *arrived* and `encrypt()` is what asks for encryption. `encrypt()` on
1527
+ a document assembled in memory still produces a file qpdf and MuPDF need the
1528
+ password to open.
1529
+
1530
+ - **Engine methods that could not take effect.** The writer above was written to
1531
+ only when a `SimplePdf` has no COS graph, which no path through `Document`
1532
+ produces, so `SimplePdf.set_watermark`, `sign`, `add_signature`, `hide_image`
1533
+ and `replace_image` did nothing at all through the public API. Worse,
1534
+ `sign`/`add_signature` fabricated a signature: an orphan `/Sig` holding
1535
+ `/Contents <0000>`, over no byte range, referenced by no field, in a document
1536
+ with no `/AcroForm`. All five are gone, together with that writer's branches
1537
+ for them -- a watermark is what `Page.add_stamp` does on any document and
1538
+ signing is what `Document.sign` does for real. Verified in qpdf: the file the
1539
+ legacy writer produces is object-for-object what it was, minus the forged
1540
+ signature, and MuPDF and pdfium read it the same.
1541
+
1542
+ - **Code no caller could reach**: `SimplePdf.save_incremental` (a wrapper over
1543
+ `to_bytes_incremental`, which is the path `Document.save(incremental=True)`
1544
+ uses), `supports_incremental_update`, `extract_attachment` (the working one
1545
+ is `PdfExtractor.extract_attachment`), `_field_font_family`, `_get_array`,
1546
+ `incremental_update.write_incremental` and `_build_signature_placeholder`,
1547
+ `encryption._validate_key_length`, `pdf_parser_cos._match`,
1548
+ `image_export.looks_like_encoded_image`, `font_authoring.pdf_base_name` and
1549
+ `is_subset`, and the two compatibility wrappers `text_edit._group_show_runs`
1550
+ and `ccitt._pack_rows` -- whose tests now exercise the live helpers they
1551
+ wrapped, `_walk_show_runs` and `_pack_row`, so the rules they asserted are
1552
+ still asserted where they are used.
1553
+
1554
+ ### Fixed
1555
+
1556
+ - **`PdfExtractor` read documents beside the engine instead of through it.**
1557
+ It parsed page content itself, so a document whose content is loaded lazily
1558
+ extracted nothing at all and a page with a damaged content stream lost the
1559
+ text the engine recovers; each page now comes from
1560
+ `SimplePdf.extract_page_text`. Its images were the decoded samples rather
1561
+ than an image file -- writing one to a `.png` produced a file nothing opens
1562
+ -- and are now reconstructed as `save_image` writes them, through a new
1563
+ `SimplePdf.image_file()` that both paths share. `get_next_image()` takes an
1564
+ optional path or stream to write to, and `get_next_image_name()` names the
1565
+ image the cursor is on.
1566
+
1567
+ - **An extended sequential JPEG (`SOF1`) decoded to nothing without Pillow.**
1568
+ Such a frame is coded exactly like a baseline one for 8-bit Huffman scans
1569
+ (ITU-T T.81 F.1.2) and libjpeg, pdfium and MuPDF all read it that way; the
1570
+ pure-Python decoder refused it, so the image was missing wherever Pillow was
1571
+ not installed. It is now decoded as baseline, while a 12-bit frame still
1572
+ falls back.
1573
+
1574
+ - **A field under a parent without a name disappeared.** `/T` is optional,
1575
+ and a nameless node's kids take the name above it -- pdf.js, qpdf and MuPDF
1576
+ all read the kid of such a parent as `inner`, not `box.inner` -- but the
1577
+ form stopped at the nameless node, so the field was neither listed nor
1578
+ settable.
1579
+
1580
+ - **Setting a value on a field whose type is stated by its parent treated it
1581
+ as a text field.** `/FT` and `/Ff` were read from the field alone, so a list
1582
+ box kid of a parent carrying the type could not take several values, and a
1583
+ check box kid would get text instead of a state.
1584
+
1585
+ - **A field whose value is set on its parent read as `None`.** `/V` is
1586
+ inheritable (ISO 32000-1 table 220) and pdfium, pdf.js, qpdf and MuPDF all
1587
+ give the kid its parent's value; it was only looked for on the field
1588
+ itself.
1589
+
1590
+ - **Saving HTML, Markdown or SVG ignored `overwrite`, and a failed export
1591
+ destroyed the file it was replacing.** `save(path, DocFormat.HTML)` replaced
1592
+ an existing file even with `overwrite=False`; it now refuses, checking every
1593
+ file an export would write before writing any. The HTML, Markdown, SVG,
1594
+ TIFF, raster, extracted-image and attachment writers truncated their target
1595
+ before writing it, so a full disk or a killed process left it empty; they
1596
+ now write atomically, as a PDF save does. Content anchored outside the crop
1597
+ box, which no viewer shows, is no longer exported by default
1598
+ (`clip_to_page=False` restores it).
1599
+
1600
+ - **A document timestamp's coverage was unclear, and a second one looked like
1601
+ tampering.** `engine.dss.add_document_timestamp` left the `<` and `>` of its
1602
+ `/Contents` inside the signed ranges, so pyHanko could not match the gap to
1603
+ the token and reported the timestamp's coverage as unclear; and every
1604
+ timestamp was a field named `Timestamp`, so renewing an archive timestamp
1605
+ made two fields of one name, which pyHanko rejects as a suspicious
1606
+ modification of every signature before it. The gap now runs over the
1607
+ delimiters, as it does for a signature, and a later timestamp is
1608
+ `Timestamp2`, `Timestamp3`, ... It also refuses an encrypted document, whose
1609
+ field name it would have written in clear.
1610
+
1611
+ - **A second `/DSS` replaced the first.** `enable_ltv` and `build_dss` wrote a
1612
+ new store from the signatures' own material, dropping revocation data an
1613
+ earlier call had added and every `/VRI` entry but one. An existing store is
1614
+ now extended in place: its streams are referred to again, only new material
1615
+ is added, its `/VRI` entries are carried over unchanged (pyHanko treats a
1616
+ changed one as tampering), and nothing new means no revision at all.
1617
+
1618
+ - **A certifying signature could follow another signature.** `sign_field`
1619
+ with `certify_permissions` on an already signed document added `/Perms`
1620
+ after the earlier signatures, which pyHanko reports as a change they did not
1621
+ allow. A DocMDP signature "shall be the first signed field in the document"
1622
+ (ISO 32000-1 12.8.2.2.1), and is now refused otherwise.
1623
+
1624
+ - **An Ed25519 signature was digested with SHA-256.** RFC 8419 pairs Ed25519
1625
+ with SHA-512 when signed attributes are present, and pyHanko rejects any
1626
+ other pairing. Ed25519 now signs with SHA-512 (a seed value demanding
1627
+ another digest is refused), and Ed448, which needs SHAKE256, is refused
1628
+ rather than signed with a digest validators reject.
1629
+
1630
+ - **`aspose_pdf.generated.*` was an older, broken copy of the library.**
1631
+ `generated.document.Document` merged documents as blank pages, broke a signed
1632
+ file's signature on save, accepted a wrong password in `decrypt`, and always
1633
+ reported `info`, `version`, `form`, `outlines` and `permissions` as `None`;
1634
+ `generated.forms.UnsignedContentAbsorber.extract()` returned its own keyword
1635
+ arguments; `generated.pdfa` held inert look-alikes of the PDF/A classes. Each
1636
+ module now re-exports the canonical class, as `generated.security` already
1637
+ did, so both import paths behave identically. Code that relied on the old
1638
+ copies' own signatures -- an absorber built without a document,
1639
+ `PdfAValidateOptions.set_option`, `optimize(compress_images=...)` -- now gets
1640
+ the canonical ones.
1641
+
1642
+ - **A PNG with a transparent background was placed as an opaque rectangle.**
1643
+ `Page.add_image` (and push-button icons) stripped the alpha channel of
1644
+ grey+alpha and RGBA PNGs and ignored `tRNS`. Transparency is now written as a
1645
+ soft mask: the alpha channel, a palette's per-entry opacity, or the colour a
1646
+ grey or RGB `tRNS` names, compared at the image's own bit depth (a 16-bit
1647
+ pixel that shares only the key's high byte stays opaque). Placed over a red
1648
+ background, every kind renders in pdfium and MuPDF as Pillow composites it.
1649
+
1650
+ - **A signed document looked unsigned through the public `Document`.**
1651
+ `Document` had no `signatures`, so `SignaturesCompromiseDetector(Document(...))`
1652
+ reported "unsigned document" -- also for a file whose page was replaced after
1653
+ signing -- and `UnsignedContentAbsorber` read `is_signed` flags that pages,
1654
+ fields and annotations do not have, reporting everything in a fully signed
1655
+ document as unsigned, the signature field included. `Document.signatures` now
1656
+ lists the signatures, and the absorber reports what is new or different since
1657
+ the newest signature that verifies, compared object by object with the signed
1658
+ revision (decrypted first where the file is encrypted): replaced page content,
1659
+ an annotation added or edited later, a field filled in after signing with its
1660
+ widget. On files signed by pyHanko with RSA, RSA-PSS and ECDSA keys, and on
1661
+ every layout this library signs, it reports nothing unsigned.
1662
+
1663
+ - **Content added to an existing page could land off the page, be clipped away
1664
+ or be invisible.** `Page.add_text`, `add_image`, `draw_rectangle`,
1665
+ `draw_line`, `Page.layer(...)` and redaction bars appended their content
1666
+ after the page's own, which left its graphics state as it found it -- so a
1667
+ page ending with a `cm` it never undid drew text placed at (300, 400) at
1668
+ (603, 800), off a Letter-sized page; a clipping path left in force hid the
1669
+ addition entirely, `3 Tr` made added text invisible and a dash pattern turned
1670
+ an added solid line into a dashed one. pdfium and MuPDF showed all of these.
1671
+ The page's content is now saved before and restored after when, and only
1672
+ when, it leaves state behind; its streams are referenced rather than
1673
+ rewritten, so incremental saves still only append, and pages this library
1674
+ authors are unchanged. Adding to one of several pages that shared a
1675
+ `/Contents` array also no longer added to all of them.
1676
+
1677
+ - **Signing a document that had been updated before could destroy it.** The
1678
+ incremental-update builder behind signing, the `/DSS` and document
1679
+ timestamps took its next object number from the newest cross-reference
1680
+ section, which lists only what that revision changed. After any earlier
1681
+ update that rewrote a low-numbered object -- a catalog touched by a save, a
1682
+ field filled in by another tool -- it handed out numbers older revisions
1683
+ still owned: signing gave the signature dictionary the page's number, the new
1684
+ revision replaced the page, pdfium could not open the result and MuPDF found
1685
+ no text in it, while the signature itself validated. The appended revision's
1686
+ `/Size` shrank with it. New objects are now numbered above the trailer's
1687
+ `/Size` and every object header in the file. Signing files written by this
1688
+ library, qpdf (object streams, linearized, QDF), MuPDF, a hybrid-reference
1689
+ layout and a two-revision file now validates intact in pyHanko and opens in
1690
+ pdfium, MuPDF and qpdf.
1691
+
1692
+ - **A PDF with anything in front of its header did not open.** The reader
1693
+ required the data to start with `%PDF-`, where Acrobat accepts the header
1694
+ anywhere in the first 1024 bytes and pdfium, MuPDF and qpdf read such files.
1695
+ A prefix left by a mail client or an HTTP capture usually arrives after the
1696
+ file was written, so every offset in it is short by the prefix's length; the
1697
+ parser now notices when `startxref` lands on a cross-reference section only
1698
+ once moved by the header's position, and moves every offset -- table,
1699
+ `/Prev`, `/XRefStm` -- by as much. Saving such a file incrementally writes it
1700
+ whole instead, so its offsets are right again. The version is read from the
1701
+ header wherever it stands.
1702
+
1703
+ - **One stream could make a document unopenable.** An object was taken to end at
1704
+ the first `endobj` after its header, so a content stream that merely contains
1705
+ the word `endobj` -- in a comment, in a string -- had its correct `/Length`
1706
+ rejected as running past the object, and the document failed to open. A
1707
+ wrong `/Length` (too long, too short or negative, the usual damage from a
1708
+ transfer that rewrites line endings) failed it the same way, as did an
1709
+ object missing its `endobj` before the cross-reference table. pdfium, MuPDF
1710
+ and qpdf read all of these. `/Length` is now trusted when `endstream` follows
1711
+ it, a wrong one is replaced by the closing `endstream` (never one inside the
1712
+ data, never a later object's), and an indirect `/Length` is followed: streams
1713
+ measured by one -- pdfTeX and matplotlib write them -- now hold exactly the
1714
+ bytes qpdf reads instead of the end-of-line before `endstream`.
1715
+
1716
+ - **Hybrid-reference files lost everything kept in object streams, and saving
1717
+ one broke it.** A file written to stay readable by PDF 1.4 software (ISO
1718
+ 32000-1 7.5.8.4) indexes its objects in object streams in a cross-reference
1719
+ stream named by the trailer's `/XRefStm`, and lists only the rest in its
1720
+ classic table. `/XRefStm` was never read, so those objects -- typically the
1721
+ page tree, fonts and document information -- did not exist: the document
1722
+ opened with no text and no title, and a full save wrote a file without its
1723
+ page tree, which qpdf and MuPDF refuse to open. The stream is now merged
1724
+ into its revision, the table's in-use entries answering first; a broken
1725
+ `/XRefStm` is ignored. qpdf, MuPDF and pdfium agree on every layout tested
1726
+ but one, where the table lists the packed objects as free; there this reads
1727
+ as pdfium does.
1728
+
1729
+ - **ReportLab documents opened without their metadata, and encrypted ones did
1730
+ not open at all.** ReportLab writes a comment into the trailer dictionary of
1731
+ every file (`% ReportLab generated PDF document -- digest (opensource)`), and
1732
+ the object tokenizer did not treat `%` as the start of a comment, which ISO
1733
+ 32000-1 7.2.3 allows anywhere white-space may stand. The trailer failed, the
1734
+ reader rebuilt the cross-reference table by scanning the file, and the scan
1735
+ parsed the trailer with the same tokenizer and lost it again: title and
1736
+ author were gone, and an encrypted ReportLab file -- whose `/Encrypt` is in
1737
+ that trailer -- failed with a resource-limit error under either password.
1738
+ Comments are now white-space in objects and trailers alike (form feed too),
1739
+ and the trailer extractor steps over comments and strings holding `<<`, `>>`
1740
+ or `(`. pdfium, MuPDF and qpdf read every case in the new tests; text and
1741
+ metadata from the rest of a mixed corpus are unchanged.
1742
+
1743
+ - **Extracting a page copied every page its links pointed at.** Importing a
1744
+ page followed its link annotations' page references, so a link to a page
1745
+ that was not being copied brought that page -- content and resources -- into
1746
+ the result as an orphan no page tree lists. Extracting a table-of-contents
1747
+ page from a 60-page document wrote a 40 KB file from a 32 KB source, holding
1748
+ all 60 pages. Such a reference now becomes `null`, as qpdf writes it (16 KB,
1749
+ one page). Links by *name* were copied as names, which the source catalog
1750
+ alone defines: in an extract they went nowhere, and in a merge they resolved
1751
+ against the other document's names, sending a second copy's links into the
1752
+ first copy. They are now carried as the destinations they name, as
1753
+ bookmarks are.
1754
+
1755
+ - **Bookmarks by name, by action or with no target read as page 1, and saving
1756
+ made that true.** `OutlineItem.page_index` was read from a `/Dest` array
1757
+ only, so a bookmark naming its destination -- through the `/Names /Dests`
1758
+ tree most writers use, or the older `/Dests` dictionary, directly or in a
1759
+ `/GoTo` action -- read as page 0, and so did every bookmark that lands on no
1760
+ page: one with no target (ISO 32000-1 makes both `/Dest` and `/A` optional),
1761
+ a URI, an undefined name. Opening and saving a document gave a target-less
1762
+ bookmark a jump to page 1 -- a PyMuPDF page deletion leaves exactly such
1763
+ bookmarks. Named destinations now resolve as in pdfium and MuPDF,
1764
+ `page_index` is `None` where a bookmark lands on no page, and such bookmarks
1765
+ are written back as they were. Merging carried a named bookmark across as
1766
+ its name, which the merged document resolved against its own names -- a
1767
+ second copy of a file sent its bookmarks into the first copy's pages -- and
1768
+ dropped page-less bookmarks; names are now carried as the destinations they
1769
+ name.
1770
+
1771
+ - **A page with comments could not be read, flattened or edited.** A sticky
1772
+ note's `/Popup` is an annotation whose `/Parent` is the note, and a reply's
1773
+ `/IRT` is the note it answers. The annotation property channel inlined
1774
+ whatever an entry referenced, so reading a note walked note -> popup -> note
1775
+ and raised `Annotation property graph contains a cycle`: `page.annotations`,
1776
+ `Document.flatten()` and `annotations.delete()` failed on every page carrying
1777
+ a comment written by Acrobat or MuPDF. Another annotation, like a page, is
1778
+ now never a property value (typed `/Annot`, or untyped with `/Subtype` and
1779
+ `/Rect`); editing a note and saving keeps its popup and replies linked, as
1780
+ pikepdf and PyMuPDF read the result.
1781
+
1782
+ - **Text in a CMap written without line breaks came out as raw codes.** Every
1783
+ CMap reader -- ToUnicode, and an embedded Encoding CMap's codespaces,
1784
+ `cidrange`, `cidchar` and `WMode` -- went line by line and recognised an
1785
+ operator only at the end of a line. A one-line CMap, an entry on the
1786
+ `beginbfchar` line or an entry split over two lines gave no mappings, so
1787
+ extraction, search and substitute-glyph rendering saw codes like
1788
+ `\x01\x02`. They now share one PostScript tokenizer (comments and strings
1789
+ skipped, whitespace inside hex strings ignored, an odd final digit padded).
1790
+ `bfrange` destinations were also read as one integer, which dropped every
1791
+ ligature range (`<00660066>`) and surrogate-pair range (`<D835DC00>`); they
1792
+ now count up in the last UTF-16 unit. Thirteen layouts extract exactly as
1793
+ pdfium, MuPDF and pdfminer.six do; on malformed CMaps where they disagree the
1794
+ majority reading is used. Text from twenty PDFs written by ReportLab, PyMuPDF,
1795
+ cairo and matplotlib is unchanged.
1796
+ - **A form's `/F1` was drawn in the page's `/F1`.** The renderer cached outline
1797
+ fonts by resource name, so when a page and a form XObject it draws both name
1798
+ a font `/F1` -- most generators name their first font that -- whichever ran
1799
+ first drew both; GraphicsAbsorber measured text boxes through a second
1800
+ name-keyed cache. Fonts are now cached by font dictionary, as Type 3 fonts
1801
+ already were.
1802
+
1803
+ - **Lab colour was painted as RGB, and Lab, Separation and DeviceN images as
1804
+ their raw components.** The shared colour converter counted Lab's three
1805
+ components and read them as red, green and blue, so `60 40 -30 sc` filled
1806
+ yellow where pdfium and MuPDF paint mauve, and every Lab shading was wrong.
1807
+ The image decoder knew grey, RGB, CMYK and device palettes and nothing else:
1808
+ a Separation or DeviceN image's tints were drawn as grey or RGB, and a Lab
1809
+ image or a palette over Lab, read as bytes over 255 when L\* spans 0-100,
1810
+ came out near black. `optimize(image_compression_quality=...)` baked that
1811
+ into the file -- a Lab photo was rewritten as a near-black DeviceRGB JPEG --
1812
+ and `save_image` exported it. Lab now converts to sRGB as both references
1813
+ render it (relative to the space's own white, unclamped by `/Range` for
1814
+ fills; image a\* and b\* as the byte less 128; palette entries over the
1815
+ base `/Range`), and one sample conversion serves the renderer, image export
1816
+ (eager and streamed) and `optimize`, which agree pixel for pixel. A spot or
1817
+ DeviceN image now joins the overprint preview as a fill in its space does.
1818
+ - **A palette image's `/Decode` rounded to an index.** Both references
1819
+ truncate `Dmin + s * (Dmax - Dmin) / (2^bpc - 1)`: `[1 0]` on 8-bit samples
1820
+ selects entry 1 for sample 0 and entry 0 for the rest, where every cell came
1821
+ out as entry 1. Image export and `optimize` ignored a palette's `/Decode`
1822
+ entirely, and `optimize` inverted an image with `/Decode [1 0]` twice once
1823
+ its samples had been converted -- a Separation image came out as its
1824
+ negative. The array is now applied once, wherever the samples are mapped.
1825
+
1826
+ - **Type 3 fonts rendered as black boxes.** The renderer knew TrueType, Type 1
1827
+ and CID fonts, and drew a placeholder box for any glyph it could not outline
1828
+ -- including every Type 3 glyph, which is not an outline but a content
1829
+ stream (ISO 32000-1 9.6.5). matplotlib's PDF backend writes Type 3 fonts by
1830
+ default, so every title, axis label and tick number of a matplotlib chart
1831
+ came out as a row of boxes; SVG export, a separate path, drew them right.
1832
+ Glyph procedures now run under `FontMatrix * text space * Tm * CTM`, with the
1833
+ font's `/Resources` (or the page's), advancing by `/Widths` with character
1834
+ and word spacing, horizontal scaling and `TJ` kerning applied; a `d1` glyph
1835
+ ignores its own colour operators and paints in the text's colour; a glyph may
1836
+ show text in another Type 3 font. A real matplotlib chart and ten targeted
1837
+ cases (colour, flipped and non-uniform font matrices, rotation, kerning,
1838
+ widths, font resources, nesting, invisible text) agree with pdfium and MuPDF.
1839
+ - **Invisible text did not advance.** `Tr 3` returned before moving the text
1840
+ matrix, so `3 Tr (hidden) Tj 0 Tr (seen) Tj` drew the visible word on top of
1841
+ the invisible one -- in any font. It now takes up the room it always had.
1842
+
1843
+ - **Check boxes written by MuPDF rendered empty.** MuPDF draws a check mark as
1844
+ ZapfDingbats `(3)` and declares the font with `/Encoding /WinAnsiEncoding`.
1845
+ The renderer overlaid that named Latin encoding on the font's built-in one,
1846
+ so code 0x33 became the glyph `three`, which ZapfDingbats does not have, and
1847
+ nothing was drawn -- on the live form and in the flattened copy alike. The
1848
+ same happened to Symbol under any named base. pdfium and MuPDF keep the
1849
+ built-in encoding for these two fonts whatever base is named, and now so do
1850
+ we: WinAnsi, Standard and MacRoman render exactly like no `/Encoding`.
1851
+ `/Differences` still apply, and are now read in the font's own glyph names
1852
+ -- `a20`, the heavy check mark, is no Adobe Glyph List name and used to be
1853
+ ignored -- through new code-to-glyph-name tables for both fonts (from their
1854
+ Adobe AFM encoding vectors, checked to cover exactly the codes of the
1855
+ existing Unicode tables). A difference naming a glyph the font lacks draws
1856
+ nothing, as in pdfium.
1857
+
1858
+ - **The renderer composed transforms in the wrong order.** ISO 32000-1 writes
1859
+ `cm` as `CTM' = M * CTM`, `Td` as `Tlm = [1 0 0 1 tx ty] * Tlm`, a glyph
1860
+ advance as `Tm = [1 0 0 1 tx 0] * Tm` and a form as painted under
1861
+ `Matrix * CTM`: the newer matrix applies first. Ten sites applied it last.
1862
+ Nothing showed while the transforms commuted, which is why no test or sweep
1863
+ caught it; when they did not, a `cm` inside a scaled `cm` -- how Microsoft's
1864
+ print-to-PDF places every image -- drew off the page, a word set with
1865
+ `20 0 0 20 x y Tm /F1 1 Tf` piled its letters on top of each other, `Td` and
1866
+ `T*` under a rotated or scaled `Tm` moved lines the wrong way, and any form
1867
+ XObject with a scaling or rotating `/Matrix` vanished outright. A new helper
1868
+ states the product in the specification's order, and a sweep of thirteen
1869
+ non-commuting cases now agrees with pdfium and MuPDF to a pixel (it
1870
+ disagreed on all thirteen before). `ImagePlacementAbsorber` had the same
1871
+ inversion in its own helper, whose formula contradicted its docstring, and
1872
+ reported such an image at its unscaled position. The glyph-box fallback for
1873
+ an unresolvable font also drew nothing under a scaled `Tm`: its padding had
1874
+ a floor of half a *text-space* unit, the whole box at size 1.
1875
+ - **One real written as `.8` made a document impossible to open.** ISO 32000-1
1876
+ 7.3.3 lists `-.002` among its own examples of a real, and MuPDF and
1877
+ Ghostscript write them; the COS tokenizer recognised a number only by a
1878
+ digit or a sign, so `/MK << /BG [1 1 .8] >>` in a MuPDF-written form failed
1879
+ the whole file. And a number with nothing after it -- the last member of an
1880
+ object stream, typically a stream's `/Length` -- sent the reference
1881
+ lookahead past the end of the input, so the object was lost.
1882
+ - **Flattening replaced a form's own appearance with ours.** Every text and
1883
+ choice field was rebuilt from its value before flattening, so a form from
1884
+ another producer was flattened with a different drawing -- font sizes
1885
+ changed, `/MK` rotation lost, a selection bar added to list boxes. A widget
1886
+ that carries an appearance now keeps it, as 12.5.5 describes, and only
1887
+ `/NeedAppearances true` rebuilds them; MuPDF renders its own form, flattened
1888
+ here, pixel-identical to the live form.
1889
+
1890
+ - **Saving a signed document broke its signatures, even unchanged.** `save()`
1891
+ rewrote every document in full, and a rewrite moves every byte a signature
1892
+ covers: open a signed PDF, save it without touching it, and pyHanko reports
1893
+ the signature as no longer intact -- in a file whose `/SigFlags 3` still
1894
+ declared *AppendOnly*, the flag ISO 32000-1 defines for exactly this. The
1895
+ default is now to append: `incremental` defaults to `None`, which writes an
1896
+ incremental update whenever the file carries signatures (the AppendOnly bit,
1897
+ or a signed field), and a full rewrite otherwise. An untouched signed
1898
+ document now saves back byte for byte; an edited one gains a revision on top
1899
+ of the signed bytes and its signatures stay valid. Verified with pyHanko on
1900
+ documents signed by this library and by pyHanko. `incremental=False` still
1901
+ forces a full rewrite and now warns that it invalidates the signatures, as
1902
+ does a save that must rewrite because the protection is changing.
1903
+ **Behaviour change:** a signed document saved with default arguments is
1904
+ now appended to rather than rewritten, so an optimisation run on one
1905
+ (`optimize()` then `save()`) grows the file unless `incremental=False` is
1906
+ passed.
1907
+
1908
+ - **Changing a document's passwords lifted every permission restriction, and
1909
+ `decrypt` accepted any password.** `change_passwords` re-encrypted with
1910
+ `encrypt`'s defaults, so a document that forbade printing and copying came
1911
+ out of a routine password change allowing both -- `/P` -4, confirmed with
1912
+ qpdf on files from both qpdf and this library -- and moved to AES-256
1913
+ whatever cipher it had used. It now keeps the permissions and the cipher and
1914
+ changes only the passwords. `Document.decrypt(password)` checked its
1915
+ argument behind a flag that is off after a load, so on any loaded document
1916
+ `decrypt("anything")` took the protection off; it now accepts only the user
1917
+ or the owner password, checked against the file's own `/Encrypt`, and
1918
+ refuses the rest. Both methods now agree on which passwords count: either
1919
+ of the document's, where `change_passwords` had accepted only the one the
1920
+ document was opened with, and an owner password given to `encrypt` in this
1921
+ session could not `decrypt` at all.
1922
+
1923
+ - **A save that failed destroyed the file it was saving over.** Every PDF save
1924
+ to a path wrote with `Path.write_bytes`, which truncates its target before
1925
+ writing. Saving a document over the file it came from -- what
1926
+ `overwrite=True` is for -- therefore erased the original the moment the
1927
+ write began, and a write that then failed left nothing behind. Reproduced on
1928
+ a real, nearly full volume: the save raised `ENOSPC` and the only copy of the
1929
+ document was a **0-byte file** that no longer opened. Full saves, incremental
1930
+ saves and the low-code plugins' file output now go through one writer that
1931
+ stages the bytes beside the target, fsyncs them and renames them over it, so
1932
+ the same save now fails with the same `ENOSPC` and the original is untouched,
1933
+ with no staging file left behind. Symlinks, permission bits and the refusal
1934
+ to write a read-only file behave as before.
1935
+
1936
+ - **A streamed document converted to PDF/A came out non-conformant, and the
1937
+ validator, also streamed, called it valid.** Three more paths read streaming
1938
+ mode's deliberately empty content cache as though it were the document.
1939
+ `convert_to_pdfa` iterated that cache to rewrite DeviceCMYK operators, so on
1940
+ a document opened with `open_streaming` it rewrote nothing -- and left the
1941
+ CMYK content under the sRGB OutputIntent it had just installed.
1942
+ `validate_pdfa` skipped its device-colour scan of page content the same way,
1943
+ so that exact file passed when opened streamed and failed when opened
1944
+ eagerly: convert and check in streaming mode, and a non-conformant file is
1945
+ reported conformant. And `repair()` padded the empty cache with one blank
1946
+ entry per page, after which every page of the document read as empty. All
1947
+ three now read each page through `get_page_content`; a streamed conversion
1948
+ writes what an eager one writes (identical apart from IDs and timestamps,
1949
+ pixel-identical under pdfium).
1950
+
1951
+ - **HTML and Markdown export dropped every word inside a form XObject.**
1952
+ Headers, footers and stamps usually live in forms, and the export's layout
1953
+ analysis reads the page's own content stream, where a form is one `Do` -- so
1954
+ those words vanished from `to_html` and `to_markdown` while `extract_text`,
1955
+ which walks into forms, kept them: two of our outputs disagreeing on the same
1956
+ page. Each text object a form shows now joins the page's own, positioned
1957
+ where it lands (the form's `/Matrix`, then the CTM of its `Do`) so it sorts
1958
+ into reading order, and decoded with the form's own resources, or the page's
1959
+ where it declares none. Text is keyed by element rather than by byte offset,
1960
+ since a form's offsets are into its own stream and collided with the page's.
1961
+ Reading order matches MuPDF on every case tested: placed by `cm`, by
1962
+ `/Matrix`, by both under a scale, nested, a footer drawn first. `replace_text`
1963
+ still does not edit inside forms, and now says why: a form is often shared by
1964
+ every page that shows it.
1965
+
1966
+ - **Outlined text rendered solid.** Table 106 gives four rendering modes that
1967
+ stroke a glyph -- `Tr` 1 (stroke), 2 (fill then stroke), and their clipping
1968
+ twins 5 and 6 -- and the renderer had all four fall through to the fill. An
1969
+ outlined headline or a hollow watermark came out as solid filled text, in the
1970
+ *fill* colour, which mode 1 explicitly does not use. The glyph's outline is
1971
+ now stroked with the stroke colour and the graphics state's pen; mode 2 fills
1972
+ and then strokes; the clipping twins paint exactly as their counterparts.
1973
+ Measured against pdfium and MuPDF, which agree with each other to within a
1974
+ fraction of a percent on all eight modes, every mode now matches. The SVG
1975
+ export already had this right, which is where the two disagreed.
1976
+ Contours are closed before stroking: only the TrueType backend returns a
1977
+ closed loop, so an embedded CFF or Type 1 glyph would otherwise be drawn with
1978
+ one side of every loop missing.
1979
+
1980
+ - **A document opened with `open_streaming` extracted no text, and could not
1981
+ delete a page.** Streaming mode leaves `page_contents` empty on purpose --
1982
+ content is decoded per page on demand -- but three paths read that list as
1983
+ though it were the document. `extract_page_text` indexed it directly, so
1984
+ every page of every streamed document extracted as `""`, silently, and
1985
+ `Document.extract_text` counted it, so the whole document did too; the
1986
+ page-text cursor reported no pages at all. `pages.delete` deleted from it
1987
+ unconditionally and raised `IndexError: list assignment index out of range`
1988
+ for **every index** — while the two parallel lists right below it were
1989
+ already guarded for exactly this. Text now comes through `get_page_content`,
1990
+ which serves both modes, page counts come from the pages, and deletion
1991
+ shifts the page's identity without decoding anything: a streamed document
1992
+ stays lazy through both. Deleting from a streamed document now matches the
1993
+ eager path page for page across every deletion pattern tested.
1994
+
1995
+ - **Metadata could not be removed at all: `doc.info.clear()` cleared nothing.**
1996
+ The sync wrote every key it found in `Document.info` and never deleted one,
1997
+ so `del`, `pop`, `clear()` and assigning a smaller dictionary all left the
1998
+ file exactly as it was — a document stripped of its author and title before
1999
+ being shared went out with both, and nothing said so. (`doc.info = {}` did
2000
+ not even reach the sync, which returned early on an empty dictionary.) An
2001
+ entry that is gone from the dictionary is now removed from `/Info`, on a full
2002
+ save and on an incremental one; verified with pikepdf, exiftool and poppler,
2003
+ which no longer see the stripped entries. An entry `Document.info` never
2004
+ showed — an array, a dictionary, anything with no faithful text — is not
2005
+ removed by editing what you could see, and a warning names what stayed
2006
+ behind. `/Info` has one writer again: the title `convert_to_pdfa` adds goes
2007
+ through `metadata` like everything else, rather than being written straight
2008
+ into the COS dictionary where the next save would have taken it for a key the
2009
+ caller had deleted.
2010
+
2011
+ - **Opening a file and saving it turned its `/Trapped` flag into our own
2012
+ `repr`.** Every `/Info` value that was not a string was read with `str()` on
2013
+ the parser's object, so `/Trapped /True` arrived in `Document.info` as the
2014
+ text `"PdfName(/True)"` and was written back as the *string*
2015
+ `(PdfName\(/True\))`. No API call was needed: a plain load-and-save
2016
+ destroyed it, and the same happened to a producer's own entries — a boolean
2017
+ became `(PdfBoolean(True))`, an array became a string holding a list of
2018
+ reprs. A consumer asking whether the document is trapped got a string that
2019
+ cannot equal `/True`, which pikepdf confirms. One rendering rule now serves
2020
+ both directions: a name reads as its text, a boolean as `true`/`false`, a
2021
+ number as its digits, and a value with no faithful text (an array, a
2022
+ dictionary) is left out of `Document.info` instead of being flattened into
2023
+ it. On save an entry whose text has not changed is left exactly as the file
2024
+ had it, whatever its type, and `/Trapped` is written as a **name** — `"true"`,
2025
+ `"True"` and `"/True"` all reaching `/True`, an unrecognised value passed
2026
+ through as a name, an empty one as the entry's own default `/Unknown`.
2027
+ Verified with pikepdf and exiftool, which now read the flag they should.
2028
+
2029
+ - **A metadata sync invented dates and wrote non-dates into date fields.** The
2030
+ `/Info` ↔ XMP synchronisation read a PDF date with a regex that was anchored
2031
+ only at the start and defaulted every missing component: `D:2026` — a legal
2032
+ date meaning that year (ISO 32000-1 7.9.4 truncates from the right) — became
2033
+ `2026-01-01`, and `2026-09-10`, an ISO date that had found its way into
2034
+ `/Info`, became **`2026-01-01`**: a wrong date that looks right. A stray
2035
+ digit (`D:202609100`), an hour of 30, trailing prose — all were read as far
2036
+ as they parsed. And when conversion failed outright the raw text was written
2037
+ into `xmp:CreateDate` regardless, so `"10 September 2026"` ended up in a
2038
+ property that holds a date, in the packet PDF/A conformance is judged on.
2039
+ Both converters now match the whole string, check that every component is in
2040
+ range, and preserve precision in both directions (`D:2026` ↔ `2026`); the one
2041
+ thing they pad is a lone hour, because XMP's shortest time is `hh:mm`. What
2042
+ is not a date is refused, and the sync **skips** it with a logged warning
2043
+ instead of copying it — the neighbouring properties still sync. Verified
2044
+ against pikepdf, exiftool and poppler: all three agree with the new readings,
2045
+ and pikepdf itself mangles 10- and 12-digit dates (`D:2026091008` →
2046
+ `2026-09-01T00:00:08`) where the other two agree with us.
2047
+
2048
+ - **`info['CreationDate'] = datetime.now()` failed at save time, three frames
2049
+ down, naming nothing.** The document information dictionary maps names to
2050
+ **text strings** (ISO 32000-1 14.3.3), dates among them written in the
2051
+ `D:YYYYMMDDHHmmSSOHH'mm'` form of 7.9.4 — but nothing checked, so a
2052
+ `datetime`, an `int`, a `None` or a non-string key travelled down to the COS
2053
+ layer and died there with `PdfString value must be bytes or str` (or
2054
+ `PdfName must be a string`) on `save()`, long after the assignment that
2055
+ caused it and without naming the entry, the type it got or what it wanted.
2056
+ The contract is now checked where `/Info` is written, so it cannot be
2057
+ bypassed: the refusal is a `PdfValidationException` naming the entry and the
2058
+ offending type, and a value that looks like a date is additionally told how a
2059
+ PDF date is spelled — `value.strftime("D:%Y%m%d%H%M%S+00'00'")`, a suggestion
2060
+ a test takes out of the message and round-trips, so it stays true. `str`,
2061
+ `bytes` and `bytearray` are accepted as before.
2062
+
2063
+ - **One circular reference in a page's resources cost the whole page.** A form
2064
+ XObject whose own `/Resources` name that same form makes the resource graph
2065
+ point back at itself — malformed, and writers do produce it. The converter
2066
+ refused the whole graph, so the page lost its fonts, its images and its text;
2067
+ and because the renderer converts a form's resources as it draws, `render()`,
2068
+ `save_as_image()` and `to_svg()` **raised** rather than returning a page at
2069
+ all. The offending edge is dropped now and everything beside it survives,
2070
+ which is what pdfminer and MuPDF do; the drop is logged rather than silent. A
2071
+ cyclic **page tree** is still refused: there is no document behind that one.
2072
+
2073
+ - **Text drawn through a form XObject was not extracted at all.** A form is a
2074
+ piece of content a page draws with `Do` (ISO 32000-1 8.10) — headers,
2075
+ footers, stamps, and anything a layout tool reuses live in one. The renderer
2076
+ walks into a form and so does the graphics absorber, but the text reader
2077
+ consumed the `Do` operand and moved on, so a page whose words were all inside
2078
+ forms extracted as empty. It walks in now, with the form's own `/Resources`
2079
+ governing inside it and the page's standing in where a form declares none (or
2080
+ declares an empty set, which older writers use to mean the same). Nested
2081
+ forms are followed to a bounded depth, so a form that draws itself stops.
2082
+ An **image** XObject is not a content stream and is not read as one, nor are
2083
+ its samples inlined into the page's resources to look for words in them.
2084
+ `extract_text`, `Document.extract_text` and the text absorbers all see a
2085
+ form's text now; pdfminer and MuPDF agree.
2086
+
2087
+ - **A subset of pages adopted the whole document's fields and layers.** The
2088
+ rule for taking part of a document is written down — what belongs to the
2089
+ pages comes, the document's own belongings do not — but two things came
2090
+ regardless of whether the pages reached them. Every field of the source
2091
+ `/AcroForm` was adopted, so a page split off on its own carried the *other*
2092
+ pages' fields: controls listed in the form that no page in the document
2093
+ draws, and nothing can fill. Every optional content group was adopted too, so
2094
+ the same split carried layers nothing in it names — entries in a viewer's
2095
+ layers panel that switch nothing on or off. A subset now takes a field only
2096
+ when one of its widgets came, and a layer only when a page reaches it.
2097
+ Whole-document merges are unchanged: every page comes, so everything is
2098
+ reached, and a layer an author left empty or a field whose widgets were never
2099
+ placed is theirs to keep — the same way attachments were already handled.
2100
+
2101
+ - **An imported page kept its index into a structure tree it had left
2102
+ behind.** A page's `/StructParents` and an annotation's `/StructParent` (ISO
2103
+ 32000-1 14.7.4.4) are keys into *their own document's* `/ParentTree`.
2104
+ Merging does not carry the structure tree across — that is a reasoned
2105
+ boundary — but the key came anyway. Merged into a tagged document, the
2106
+ imported page arrived holding `/StructParents 0` and so claimed the target's
2107
+ first page's headings as its own content, with two pages answering to one
2108
+ entry; extracted or concatenated into a fresh document, it pointed at a
2109
+ `/StructTreeRoot` that was not there. The page import drops both keys now.
2110
+ Checked by reading the structure tree back with pdfium's own reader.
2111
+
2112
+ - **`PdfFileEditor.insert` brought a page's drawing and nothing it drew with.**
2113
+ It was the last of the model-only page copies, handing the engine a list of
2114
+ page rectangles and a list of content bytes: the inserted page arrived with
2115
+ no `/Resources`, so every font and image its content named resolved to no
2116
+ object, and its annotations, links and form fields were dropped. `Document.
2117
+ merge` and `PdfFileEditor.concatenate`/`extract` had already been moved onto
2118
+ the real page import; this was the one left behind. It goes through the same
2119
+ `SimplePdf.append` now, which grew an *at* so that inserting and appending
2120
+ stay one routine rather than two, and an insert at the end is byte-for-byte
2121
+ the document a merge produces. The model-only `insert_pages` had no callers
2122
+ left and is gone.
2123
+
2124
+ - **An incremental update to a file indexed by a cross-reference stream broke
2125
+ the chain.** The update always appended a *classic* `xref` table, and ISO
2126
+ 32000-1 7.5.8.4 lets a classic trailer's `/Prev` name only a classic table —
2127
+ so the chain pointed at something no reader can read as one. Everything
2128
+ before the update was lost, and only a reader that gave up and rescanned the
2129
+ whole file found the pages again. An update now writes the same kind of
2130
+ section it chains to, written plain and unencrypted as 7.5.8.2 requires.
2131
+
2132
+ - **An object stream answered for objects it no longer held.** Reading a
2133
+ revision chain, inflating an object stream cached *every* member, including
2134
+ numbers a later revision had re-issued as plain objects, so the stale copy
2135
+ outranked the newer one for the life of the document. The rule — the newest
2136
+ revision to mention an object owns it — was being applied to the plain xref
2137
+ entries and not to these, nor to an object a revision had *freed*.
2138
+
2139
+ - **A full save copied `/Prev` out of the source trailer.** A full rewrite has
2140
+ no previous revision, so the offset pointed past the end of the file it was
2141
+ written into, and reading it back fell into a full-file reconstruction scan.
2142
+ Only the keys that name something in the file being written are carried now
2143
+ — `/Root`, `/Info`, `/ID`, `/Encrypt` — which is the list its
2144
+ cross-reference-stream sibling already used four lines away.
2145
+
2146
+ - **Setting a form field's value left it drawing the old one.** An appearance
2147
+ stream *is* what a reader draws: ISO 32000-1 12.7.3.3 lets it trust the
2148
+ stream and never look at `/V` unless the AcroForm sets `/NeedAppearances`,
2149
+ and this library writes `/NeedAppearances false` — a promise that the streams
2150
+ are current. Assigning to `Field.value` wrote `/V` and stopped, so a filled
2151
+ form showed its **authored** value in every reader that follows the rule
2152
+ (a text field's stream still said `(Hello) Tj` after the value became
2153
+ `CHANGED VALUE HERE`), while readers that regenerate anyway showed the new
2154
+ one. The generator was already here — `generate_appearances()` runs it over
2155
+ the whole form — and the setter now runs it over the one field it changed,
2156
+ leaving every other field's appearance, including a hand-made one, alone.
2157
+ Text, multiline, combo, list box, check box and radio all follow their value
2158
+ now; verified with pikepdf and by rendering before and after in MuPDF.
2159
+
2160
+ - **The two paths that build a field's inherited attributes had drifted.** The
2161
+ bulk appearance generator never seeded `rv`, so a rich-text field reached
2162
+ that way could not inherit an ancestor's `/RV` while the same field reached
2163
+ while being authored could. There is one definition now.
2164
+
2165
+ - **An annotation with no `/AP` was drawn as nothing.** ISO 32000-1 12.5.2
2166
+ gives an annotation the properties its appearance is made of — a `Square`'s
2167
+ `/IC` and `/C`, a `Line`'s `/L`, a `Highlight`'s `/QuadPoints` — and a reader
2168
+ generates the appearance when the file does not carry one, which plenty of
2169
+ writers do not. The library could already build them (`generate_appearances()`
2170
+ writes them into the document); the *renderer* never asked, so every such
2171
+ annotation was missing from a rendered page and from the SVG export. It asks
2172
+ now, through a new `SimplePdf.build_annotation_appearance`, and throws the
2173
+ stream away rather than editing the document it is drawing. Where pdfium and
2174
+ MuPDF agree on what to draw — `Square`, `Circle`, `Ink`, `FreeText` — so do
2175
+ we now.
2176
+
2177
+ - **A note or attachment icon filled its whole rectangle.** 12.5.6.4 and
2178
+ 12.5.6.15: a `Text` or `FileAttachment` annotation's icon is a fixed size and
2179
+ the rectangle does not scale it, so a 120-point box got a 120-point sticky
2180
+ note where both readers draw a small one centred in the space. It is drawn at
2181
+ twenty points now, which is where readers settle.
2182
+
2183
+ - **A blend mode over bare page blended against paper that is not there.** ISO
2184
+ 32000-1 11.4.7: a page's contents are a transparency group, and that group is
2185
+ *isolated* — it begins with nothing behind it, and the paper is joined once,
2186
+ at the end. The canvas was the paper instead, opaque from the first pixel, so
2187
+ `Screen` over bare page came out white where it should leave the colour
2188
+ alone, and every other non-Normal mode was wrong in its own way wherever it
2189
+ painted over nothing. The compositing formula already had the term for it
2190
+ (11.3.8, `(1 - αb) · Cs`); it was simply never reached, because the page
2191
+ canvas carried no alpha to be zero. It does now, and the finished page meets
2192
+ the background in one pass at the end. All sixteen blend modes match pdfium
2193
+ and MuPDF, over bare page and over paint alike.
2194
+
2195
+ The pass costs almost nothing: a pixel nothing reached still holds the
2196
+ background it was filled with and a fully covered one needs no help, so only
2197
+ partial coverage — `ca`/`CA` below 1, or a soft mask — is mixed, and a page
2198
+ with none of it is finished by a single scan in C (0.6 ms on a 1.6-megapixel
2199
+ supersampled canvas). Coverage is stored as the floor of the alpha rather
2200
+ than rounded to nearest, so its complement is exactly the backdrop's share;
2201
+ measured over thirteen `ca` values that agrees with pdfium on all thirteen,
2202
+ where rounding agreed on nine.
2203
+
2204
+ - **A pattern only painted path fills.** ISO 32000-1 8.7.3: a pattern is
2205
+ selected with `scn`/`SCN` in a Pattern colour space, and from then on it *is*
2206
+ the fill or the stroke colour. `SCN` was ignored outright, so a stroke drawn
2207
+ with a pattern came out in whatever colour happened to be set — black on a
2208
+ fresh page — and glyphs were filled straight from `fill_color`, so text
2209
+ filled with a pattern (how a gradient headline is drawn) came out solid. Both
2210
+ now paint the pattern, tiling and shading alike: a glyph hands over its
2211
+ outlines the way a path hands over its subpaths, while a stroke — which has
2212
+ no such path, only the area the pen covered — has its coverage collected as a
2213
+ mask and the clip narrowed to it, after which the same tiler runs for all
2214
+ three. Setting a plain colour, or a colour space, puts the pattern down again
2215
+ (8.6.8), on the stroking side as well as the filling one. Verified against
2216
+ pdfium and MuPDF.
2217
+
2218
+ - **`BT` cleared the whole text state, which is graphics state.** ISO 32000-1
2219
+ 9.4.1 gives `BT` two jobs — the text matrix and the text *line* matrix — and
2220
+ 9.3.1 puts everything else a text object uses in the **graphics** state: the
2221
+ font and its size, `Tc`, `Tw`, `Tz`, `TL`, `Tr`, `Ts`. Those outlive a text
2222
+ object and are saved and restored by `q`/`Q`. Both readers reset all of it at
2223
+ `BT`, so a page that selects its font once and then opens a text object per
2224
+ line — which is how a great many generators write one — rendered every line
2225
+ after the first at the 12pt fallback in the wrong face, and its text was
2226
+ *extracted* with no font at all, silently dropping every character the
2227
+ encoding was needed for (`café` came back as `caf`). A `TL` set in an earlier
2228
+ object was lost the same way, so `T*` moved nowhere and ran two lines
2229
+ together.
2230
+
2231
+ - **Text rendering modes 4 to 7 did not clip.** Table 106: they add what they
2232
+ show to the clipping path, which is applied once, at `ET`. The renderer
2233
+ stored `Tr` and never looked at it past mode 3, so a text-shaped window — the
2234
+ usual way to put a picture inside letters — clipped nothing and the picture
2235
+ covered the page. Mode 7 now collects outlines without painting, the
2236
+ accumulated glyphs intersect the clip at `ET`, and the result is graphics
2237
+ state like any other clip, so `Q` restores it. A glyph that contributes no
2238
+ outline (a space) still opens the window, and so leaves it empty; a showing
2239
+ operator given an empty string builds no clipping path at all. Verified
2240
+ against both pdfium and MuPDF.
2241
+
2242
+ - **No filled path ever had a hole in it.** ISO 32000-1 8.5.3.3 settles which
2243
+ points a path encloses by one of two rules, and the operator picks one — `f`
2244
+ against `f*`, `B` against `B*`, `W` against `W*`. The rule belongs to the
2245
+ *path*, not to any one subpath, because that is the point of it: a shape with
2246
+ a hole is two subpaths and whether the hole is open depends on counting the
2247
+ crossings of both. The renderer filled each subpath on its own, which is
2248
+ neither rule, and dropped the star entirely — so a donut, a logo counter, a
2249
+ letter drawn as vectors and a region clipped out with `W*` all came out
2250
+ solid. Glyph outlines had their own correct nonzero filler, which is why type
2251
+ looked right while artwork did not; that filler, the path fill, the clip
2252
+ rasterizer and the shading fill are now one scanline pass that takes the rule
2253
+ as an argument. The SVG export names the rule it uses (`fill-rule`,
2254
+ `clip-rule`) instead of relying on the default. Verified against pdfium, and
2255
+ the SVG against a browser engine.
2256
+
2257
+ - **`/Mask` was ignored, so a masked image painted an opaque box.** ISO 32000-1
2258
+ 8.9.6 gives an image three ways to be transparent and it may use one:
2259
+ `/SMask`, which was implemented, and `/Mask` in either of its two forms,
2260
+ which was not. `/Mask` may name a **stencil**, whose set samples are the ones
2261
+ *not* to paint — sampled over the image's unit square at whatever resolution
2262
+ it has, with the sense stated by the stencil's own `/Decode` — or hold an
2263
+ **array**, which is colour-key masking: `2 × n` bounds on the *raw* sample
2264
+ values, dropping a pixel only when every one of its components falls inside
2265
+ its own range. Both now produce the same per-pixel alpha map that `/SMask`
2266
+ does, so the renderer and the SVG export needed nothing new to honour them,
2267
+ and `/SMask` wins where a file writes both. A `/Mask` stream that does not
2268
+ declare `/ImageMask true` is left alone, as table 89 requires it. Colour-key
2269
+ masking is not applied to a `DCTDecode` or `JPXDecode` image, whose samples
2270
+ are still a codestream at that point — 8.9.6.4 advises against keying a
2271
+ lossily coded image anyway. Verified against pdfium.
2272
+
2273
+ - **A stencil mask painted black instead of the colour that was set.** An image
2274
+ with `/ImageMask true` is not a picture (ISO 32000-1 8.9.6.2): its one-bit
2275
+ samples say where the *current fill colour* goes and where the page shows
2276
+ through. The renderer decoded it as a one-bit grey image, so every stencil
2277
+ came out black on white whatever colour was in force — and a stencil is how
2278
+ scanned text, logos and Type 3 glyph bitmaps are drawn. It now paints the
2279
+ fill colour, carrying that colour's own overprint behaviour with it, and
2280
+ leaves masked-out samples untouched. The SVG export emits the same thing as
2281
+ an RGBA image rather than a grey one.
2282
+
2283
+ - **`/Decode` was collected and never read.** The array that says which
2284
+ interval of its colour space a sample spans (8.9.5.2) reached the renderer's
2285
+ image metadata and stopped there, so an inverted image rendered
2286
+ un-inverted — including the `[1 0 1 0 1 0 1 0]` that Adobe CMYK JPEGs carry.
2287
+ The image exporter honoured it for a single grey component and ignored it for
2288
+ RGB, CMYK and Indexed. One implementation now serves the renderer, the SVG
2289
+ export and the exporter: per component, at any bit depth, applied before the
2290
+ colour conversion, and in *index* space for `/Indexed`, whose default range is
2291
+ `[0, 2**bpc - 1]` rather than `[0 1]` because its samples are palette entries.
2292
+ Verified against pdfium.
2293
+
2294
+ - **An inline image's samples were read as content-stream tokens.** Between
2295
+ `ID` and `EI` lie the image's bytes, and bytes spell whatever they happen to
2296
+ spell — an operator nobody wrote, a name, an opening `(` that closes nowhere.
2297
+ Lexed as tokens they were noise, and because a literal string runs to its
2298
+ closing paren the noise usually swallowed the rest of the page: text drawn
2299
+ after an inline image went missing from `extract_text()` and from the text
2300
+ absorbers, and its marks went missing from the renderer, the SVG export and
2301
+ the graphics absorber. The rule was implemented in exactly one place — the
2302
+ layout reader behind `to_html()`/`to_markdown()` — and the tokenizer the
2303
+ other four readers share did not know it. Both now go through one module,
2304
+ which also applies the rule ISO 32000-1 8.9.7 gives for finding the end of
2305
+ the data: unfiltered samples occupy exactly
2306
+ `ceil(Width × BitsPerComponent × components / 8) × Height` bytes, so the
2307
+ common case needs no searching for a free-standing `EI` — which samples can
2308
+ imitate, and do. A declared length is still checked against the `EI` that
2309
+ should follow it, and only a filtered image falls back to the search.
2310
+
2311
+ ### Added
2312
+
2313
+ - **Inline images are painted.** `BI … ID … EI` reached the renderer as
2314
+ nothing at all; the abbreviated dictionary is now expanded to the image
2315
+ XObject it is a short spelling of (`/W` → `/Width`, `/Fl` → `/FlateDecode`,
2316
+ `/RGB` → `/DeviceRGB` and the rest of table 93) and painted by the same code
2317
+ as `Do`, including a `/ColorSpace` that names an entry in the page's
2318
+ resources. Verified against pdfium across raw, Flate, ASCIIHex, ASCII85 and
2319
+ RunLength data, 1/4/8 bits per component, grey, RGB, CMYK and Indexed
2320
+ colour.
2321
+
2322
+ - **Words ran together in text extracted from kerned `TJ` arrays.** The
2323
+ displacements between a `TJ` array's strings are kerning inside a word or the
2324
+ space between words, and telling them apart needs to know how wide the glyphs
2325
+ are. A Standard 14 font is normally written *without* a `/Widths` array, its
2326
+ metrics being the reader's to know, and the extractor then assumed 1000 units
2327
+ for every glyph — about four times a typical lowercase letter — putting the
2328
+ word-gap threshold four times too high. Ordinary word spacing was read as
2329
+ kerning, so `Hello world` came out as `Helloworld`. The bundled
2330
+ metric-compatible substitutes already answer this for the appearance
2331
+ builders, which measure text with them to wrap and centre it; the extractor
2332
+ asks them too now, and matches pdfminer across Helvetica, Times, Courier and
2333
+ their bold and italic cuts.
2334
+
2335
+ - **Extracted text had no lines, and the absorbers found no text at all.** The
2336
+ content-stream reader answered every text-positioning operator with a space —
2337
+ `Td`, `TD`, `Tm` and `T*` alike — though those four both start a new line and
2338
+ shift along the one in progress. The line structure was discarded at exactly
2339
+ the point where it was known, so a page of prose came out as one very long
2340
+ line. The separator is now chosen where the text is *shown*, from the baseline
2341
+ it is shown at. Alongside it, `'` (show on the next line) was declared as
2342
+ taking three operands where ISO 32000-1 table 109 gives it one, so the
2343
+ operator was skipped for want of operands and every line it drew was lost.
2344
+
2345
+ - **`TextFragmentAbsorber` and `TextAbsorber` collected nothing from a real
2346
+ document.** They look for an `extract_text()` method, and neither `Page` nor
2347
+ `Document` had one — a hundred tests passed on them because every one fed a
2348
+ stub with a `.text` attribute, and not one used a `Document`. Both now have
2349
+ the accessor, sharing the single per-page routine that the whole-document
2350
+ extractor and the page-at-a-time cursor also use instead of each repeating it.
2351
+
2352
+ - **`add_text` drew the wrong glyphs for anything but ASCII.** The string in a
2353
+ text-showing operator is a sequence of *codes*, and a simple font gives each
2354
+ code a glyph through its encoding. `add_text` wrote the text's UTF-8 into one
2355
+ and declared no encoding at all, so the font fell back to its built-in
2356
+ StandardEncoding and drew whatever glyphs those bytes named: `café` came out
2357
+ with two wrong letters where the accent was, `100€` with three, Cyrillic as a
2358
+ row of Latin nonsense. Nothing failed and nothing warned, and reading the page
2359
+ back through this library hid it because the extractor made the same
2360
+ assumption — pdfminer and Acrobat did not. The font now declares
2361
+ `WinAnsiEncoding` and the text is encoded into it, by inverting the very table
2362
+ the reader resolves codes through, so what is written and what is read back
2363
+ agree by construction. A character that encoding has no code for is refused,
2364
+ naming it and pointing at `font=`, rather than drawn as a different one.
2365
+ `Symbol` and `ZapfDingbats` keep their built-in encoding, which declaring a
2366
+ base encoding would have replaced.
2367
+
2368
+ - **`validate()` called every conforming document invalid, and broken ones
2369
+ valid.** The structural check walked the object graph and rejected it if any
2370
+ node was reachable from itself. A PDF object graph is not a tree: a page's
2371
+ `/Parent`, an annotation's `/P`, an outline's `/Prev` all point back, and
2372
+ `/Parent` is *required* on every page (ISO 32000-1 7.7.3.2) — so `validate()`
2373
+ and `check()` returned `False` for every document this library wrote, and
2374
+ deleting the required entry made them return `True`. They returned `True` for
2375
+ genuinely broken files too, because what is actually wrong there is still a
2376
+ perfectly traversable graph: a page missing from its parent's `/Kids` is never
2377
+ reached, and a catalog whose `/Pages` names nothing simply ends the walk. The
2378
+ walk now only has to terminate, and what makes a page tree a page tree is
2379
+ checked directly — `/Count`, `/Parent` membership, no node reached twice, a
2380
+ `/MediaBox` on every page.
2381
+
2382
+ - **`repair()` left a salvaged document claiming a page it did not write.** The
2383
+ page sync refused to add anything when the object graph had no pages yet, on
2384
+ the grounds that there might be no page tree to add to. A document repaired
2385
+ from a truncated file has one — an empty one — and pages in the model that
2386
+ belong in it, so it saved with `/Count 0` while reporting one page. What
2387
+ decides is now whether there is a tree, and `repair()` runs the sync after
2388
+ making sure there is. Found by the fixed `validate()` on its first outing.
2389
+
2390
+ - **The file-based editor workflows went through a second merge that lost the
2391
+ same things `Document.merge` used to.** `PdfFileEditor.concatenate` and
2392
+ `.extract`, and the low-code merger and splitter behind them, called a
2393
+ different implementation: it built a fresh model from page rectangles and
2394
+ content bytes, pooled the sources' images into one renamed namespace, and
2395
+ rewrote the content streams to match. A concatenated page had no `/Resources`
2396
+ of its own, every page in the result carried every image in it, and
2397
+ annotations were dropped. Both are the same operation as appending a
2398
+ document's pages, and are now the same code. Extraction adds one rule to it: a
2399
+ subset of pages is a different document, so what belongs to the pages comes
2400
+ and the document's embedded files do not, and a bookmark comes only if the page
2401
+ it points at did — remapped to where that page landed, which also makes
2402
+ extracting pages in a new order work. The content-stream renaming module the
2403
+ old merge needed is deleted; nothing renames anything now.
2404
+
2405
+ - **Every saved file was missing the comment that marks it binary.** A PDF that
2406
+ holds binary data puts a comment of at least four bytes above 127 immediately
2407
+ after `%PDF-x.y` (ISO 32000-1 7.5.2), so that a transfer in text mode or an
2408
+ editor weighing line-ending translation copies the bytes as they stand.
2409
+ ISO 19005-1 6.1.2 requires it, and this library wrote none — so every file it
2410
+ produced failed PDF/A-1 on its second line, and its own conformance check
2411
+ never looked, reporting a freshly converted PDF/A-1b document as having no
2412
+ issues at all. All three writers emit it now, and the checker reports its
2413
+ absence as a warning: it is the one requirement here that is not a property of
2414
+ the object graph, since a full save supplies one and an incremental save,
2415
+ which keeps the original bytes as its prefix, does not.
2416
+
2417
+ ### Added
2418
+
2419
+ - **The PDF/A checker enforces the implementation limits.** ISO 32000-1 annex
2420
+ C.1, which ISO 19005-1 6.1.13 adopts: a name of more than 127 bytes, a string
2421
+ of more than 65535, an integer past ±2,147,483,647. A conforming reader is not
2422
+ obliged to handle any of them, so a file that needs it to is not archivable —
2423
+ and all three passed the checker in silence.
2424
+
2425
+ - **A number too small or too large was written in a notation PDF does not
2426
+ have.** A real is decimal digits with a period, and exponential notation is
2427
+ not permitted (ISO 32000-1 7.3.3); Python writes small and large floats as
2428
+ `1e-05` and `1.5e+20`, and the COS writer handed those straight to the file,
2429
+ where `e` starts a keyword rather than continuing a number. Setting a crop box
2430
+ a little too small was enough to produce a document this library could not
2431
+ reload. Two neighbouring faults went with it: an infinity or a NaN was written
2432
+ as the word Python names it by, and a whole number past +/-2,147,483,647 was
2433
+ written without a period — an integer token outside the range annex C.1
2434
+ guarantees, which qpdf resolves to null, silently deleting the annotation that
2435
+ held it. Content streams had the rule right in their own function all along;
2436
+ both paths share it now.
2437
+
2438
+ - **A name containing a space or a delimiter produced a file nothing could
2439
+ read.** A PDF name escapes any byte that is not a regular character as `#`
2440
+ followed by two hex digits (ISO 32000-1 7.3.5); this library wrote the bytes
2441
+ as they were. A space, a bracket or a parenthesis *ends* a name, so a document
2442
+ with one anywhere came out unparseable — by other tools and by this library
2443
+ itself, which raised `Unexpected token` on reloading what it had just written.
2444
+ Reading was the mirror: `#20` was taken for three characters, so a name
2445
+ another producer escaped came back wrong. Both are reachable from the public
2446
+ surface wherever a caller names something — an annotation property's key, or
2447
+ a value marked with `annotations.Name`. One place had noticed and fixed it for
2448
+ itself, with a private pair of functions that escaped an attachment's media
2449
+ type; those are gone, because a name now escapes whatever it holds.
2450
+
2451
+ - **Non-Latin text was written in an encoding no other reader could decode.** A
2452
+ PDF text string is PDFDocEncoded or UTF-16BE behind a `FEFF` byte order mark
2453
+ (ISO 32000-1 7.9.2.2). This library wrote raw UTF-8, which is neither, so a
2454
+ conforming reader took those bytes for PDFDocEncoding: every non-Latin title,
2455
+ bookmark, annotation, field value and attachment name it produced showed as
2456
+ mojibake in Acrobat and in every other tool. Reading its own files back hid it
2457
+ completely, because the reader made the same assumption. Field names were the
2458
+ exception — they went through the one encoder that was right — and that
2459
+ mismatch is what made it visible from inside: the field-value setter matched
2460
+ names with a hand-rolled UTF-8 decode, so `form["Ф"].value = "V"` found no
2461
+ field, wrote nothing, reported success, and the value was gone after a save.
2462
+ There is now one encoder and one decoder, used everywhere a text string is
2463
+ written or read; the decoder also accepts UTF-16LE and marked UTF-8, which
2464
+ other producers write. Checked against pikepdf in both directions.
2465
+
2466
+ - **Merging a document brought across a tracing of its pages, not the pages.**
2467
+ `Document.merge` copied each page's rectangle and its content bytes and
2468
+ nothing else. A page without its `/Resources` names fonts and images that
2469
+ resolve to no object, so it drew blank — or, worse, drew with whatever the
2470
+ *target* had registered under the same name, which for two documents built by
2471
+ this library is always `/F1`: merged text rendered in the wrong typeface with
2472
+ nothing to show that anything was wrong. Everything else was dropped in
2473
+ silence — annotations, form fields, bookmarks, attachments, layers. A page is
2474
+ now imported: its dictionary is copied into the target's graph with every
2475
+ object it reaches, once each, references remapped, and attributes inherited
2476
+ from an ancestor of the source's page tree resolved onto it. The structures
2477
+ those pages belong to come with them, since a widget whose field is not in
2478
+ `/AcroForm /Fields` is a control the form does not know about, and an
2479
+ optional content group missing from `/OCProperties` is not a layer any viewer
2480
+ offers to switch. A field or attachment name used by both documents is given
2481
+ to the newcomer under a numbered variant rather than replacing what is there.
2482
+ The source document is untouched and shares no object with the result, and
2483
+ merging no longer takes the other document's title.
2484
+
2485
+ - **Opening a document and saving it rewrote every bookmark.** The outline model
2486
+ read a bookmark back as one number — the index of the page its `/Dest` named —
2487
+ and wrote it out again as "fit that page". A `/XYZ` view lost its position and
2488
+ zoom; a bookmark carrying an *action* lost the action altogether and became a
2489
+ jump to page 1. Nothing warned, and no structure check could notice, because
2490
+ "fit page 1" is a perfectly well-formed destination. An index is also not an
2491
+ identity: deleting or inserting a single page silently repointed every
2492
+ bookmark past it, while link annotations — which keep the page *reference* —
2493
+ were never affected. A bookmark loaded from a file now carries the target the
2494
+ file held, verbatim, and that is what gets written back; naming a target
2495
+ through `page_index` or `destination` replaces it. `OutlineItem.destination`
2496
+ also reads the target back typed, so a bookmark's action or view can be
2497
+ inspected and not only written.
2498
+
2499
+ - **A literal string lost its parentheses and backslashes.** Inside `( ... )` a
2500
+ backslash introduces an escape (ISO 32000-1 7.3.4.2); the reader consumed the
2501
+ backslash *and* the character after it and kept neither. Since `\(`, `\)` and
2502
+ `\\` are exactly what the writer escapes, any title, bookmark label,
2503
+ annotation, field value or script containing a parenthesis or a backslash came
2504
+ back short of them — a document could be opened and saved with its own text
2505
+ quietly edited. The other escape forms had never been read at all: `\ddd` is
2506
+ an octal byte, a backslash before an end-of-line joins the lines, and an
2507
+ end-of-line without one is a single line feed however the file writes it. The
2508
+ value is now assembled as bytes, so a high byte is the file's byte rather than
2509
+ the UTF-8 of the character it resembles in Latin-1. Checked against pikepdf
2510
+ over every escape form.
2511
+
2512
+ - **An internal link can be read back.** `Page.annotations` raised
2513
+ `Annotation property graph contains a cycle` for any link pointing at a page
2514
+ of the same document — the commonest annotation there is — and because the
2515
+ failure happened while building the collection, no annotation on that page
2516
+ could be read at all. The property channel turns an annotation entry into
2517
+ plain Python and resolves indirect references as it goes, so a `/Dest` pulled
2518
+ in a copy of the page it named; that page lists the annotation holding the
2519
+ destination, and the walk came back to where it started. A `/Dest`, or the
2520
+ `/D` of a `/GoTo` action, now reads back as the `aspose_pdf.interactive`
2521
+ destination it was written from, carrying the page index, and writing that
2522
+ property back reproduces the page reference. A remote (`GoToR`) destination
2523
+ keeps naming its page by number, since it belongs to another file.
2524
+
2525
+ - **A link or bookmark written after inserting a page pointed at the wrong
2526
+ one.** Both places that turn a page index into a page reference read
2527
+ `_page_obj_ids`, a list kept by hand alongside the edits, which carries a `0`
2528
+ placeholder for an inserted page until the next save: every page after the
2529
+ insert was off by one, and a destination naming the inserted page itself was
2530
+ written as `0 0 R` — the head of the free list (ISO 32000-1 7.5.4), never a
2531
+ real object, so the link led nowhere. Both now resolve the page by walking
2532
+ the page tree, which is right at every moment, and share one helper.
2533
+
2534
+ ### Added
2535
+
2536
+ - **An encrypted save keeps the document it was given.** Encryption lived only
2537
+ in the legacy writer, which rebuilds a file from an in-memory model, so
2538
+ encrypting a loaded document silently discarded its form fields, attachments,
2539
+ optional content and marked content — everything the model does not carry.
2540
+ The COS writer applies the security handler itself now, enciphering each
2541
+ object's strings and stream payload as it serialises them, so encryption is a
2542
+ property of the bytes rather than a different way of writing the file. The
2543
+ entries that have to stay readable stay readable: the `/Encrypt` dictionary
2544
+ (a reader needs it *before* it has a key), the cross-reference stream, a
2545
+ signature's `/Contents`, and `/Metadata` under `/EncryptMetadata false`.
2546
+ Object streams work under encryption too — the `/ObjStm` is enciphered whole,
2547
+ under its own object number, with the strings inside it left alone.
2548
+ `optimize()` no longer switches off font subsetting, image recompression and
2549
+ content de-duplication on an encrypted document; those were disabled because
2550
+ the graph used to hold ciphertext, which it no longer does.
2551
+
2552
+ - **`Document.update_attachment` changes one embedded file without replacing
2553
+ it.** `add_attachment` supersedes an entry outright, so re-adding a name to
2554
+ change its description silently dropped the MIME type, dates and
2555
+ relationship the file already carried — and there was no other way to edit a
2556
+ single field. An argument left out is now left alone, `new_name=` renames
2557
+ (carrying the metadata across, and refusing to replace a different
2558
+ attachment), and the resulting `FileSpecification` comes back.
2559
+
2560
+ - **A form field's rich text is drawn in the font the field declares.** The
2561
+ markup was laid out in a Standard-14 face whatever the `/DA` named, so a form
2562
+ using an embedded brand font rendered visibly wrong type. Runs asking for
2563
+ exactly that face now use it by reference to the form's own `/DR` resource,
2564
+ measured with its `/Widths`, so wrapping follows the real advances too; a run
2565
+ wanting bold, italic or another family still falls back to the Standard 14 of
2566
+ the matching family, since an arbitrary embedded face has no bold sibling. A
2567
+ Type0/CID field font is deliberately left out: its appearance is written in
2568
+ CID codes, and drawing PDFDocEncoded literals with it would mis-encode every
2569
+ word.
2570
+
2571
+ - **Existing content can be put on a layer.** A layer could be created and
2572
+ content authored onto it inside a `Page.layer` block, but a watermark, a
2573
+ stamp or a reviewer's comments already in the document had no way onto one at
2574
+ all. `Layer.add(content)` tags an `Annotation` or an `ImagePlacement`,
2575
+ `Layer.remove` takes the tag off — only its own, so removing one layer never
2576
+ reveals content belonging to another — and `Layer.contains` reports
2577
+ membership, counting content that reaches the group through an `/OCMD`. The
2578
+ renderer already honoured `/OC` on annotations and XObjects, so switching the
2579
+ layer off stops drawing them immediately.
2580
+
2581
+ - **Rich text is no longer stuck in Helvetica.** `/RC` and `/RV` markup now
2582
+ honours `font-family`, choosing among the Standard 14 text faces — Helvetica,
2583
+ Times and Courier, each in regular, bold, italic and bold-italic — and the
2584
+ HTML monospace tags (`<tt>`, `<code>`, `<kbd>`, `<samp>`) select Courier. A
2585
+ font stack takes the first name it recognises, and a name that signals no
2586
+ family leaves the run with the one it inherited rather than resetting it to
2587
+ the default. Each family is measured with its own advances, so wrapping and
2588
+ alignment are right for Times and for fixed-pitch Courier instead of using
2589
+ Helvetica's widths for all three. A form field's `/DA` font seeds the family
2590
+ that markup naming none inherits, so a field declared in Times renders its
2591
+ styled spans in Times.
2592
+
2593
+ - **PDF/A-4 now checks that an attached PDF is PDF/A itself.** ISO 19005-4 6.9
2594
+ requires it — an attachment has to declare `pdfaid:part` 1, 2 or 4, with part
2595
+ 3 deliberately absent because carrying arbitrary files is what PDF/A-3 is
2596
+ *for* — and only the `"4f"` level lifts the rule; `"4e"` adds 3D and rich
2597
+ media and nothing else. Validation reads the attachment's declaration and
2598
+ then checks it against the same rules as any other document, so one that
2599
+ merely claims PDF/A does not pass, with each problem named for the file it
2600
+ came from. Whether an attachment is a PDF is decided by its bytes rather than
2601
+ by the MIME type its producer declared. The structural half descends two
2602
+ levels; below that the declaration is taken at its word and a warning says
2603
+ so.
2604
+
2605
+ - **An encrypted document can be saved incrementally.** `save(incremental=True)`
2606
+ refused any encrypted document; it now appends a revision enciphered with the
2607
+ file's own key, keeping `/Encrypt` in the new trailer, so the original bytes
2608
+ — and any signature over them — stay untouched. Changing the protection is
2609
+ still refused, and now says why: adding it, removing it or changing the
2610
+ password re-keys every object, and the ones in the preserved prefix cannot
2611
+ follow. Building a `/DSS` into an encrypted document is refused for the same
2612
+ kind of reason, rather than writing validation material nobody can read.
2613
+
2614
+ - **A signed document can be encrypted.** Signing an encrypted document used to
2615
+ fall back to the rebuilding writer, losing the same structure. It now takes
2616
+ the same path as any other signature — an appended revision over the saved
2617
+ bytes — enciphered with the file's own key, with `/Contents` written over it
2618
+ in the clear as ISO 32000-1 7.6.2 requires. pyHanko validates the result as
2619
+ covering the entire file, with the document's form fields still present.
2620
+
2621
+ - **`convert_to_pdfua(part=2)` moves the tag tree into the standard structure
2622
+ namespace.** It declared the namespace on the structure root and stopped
2623
+ there, so a tree carried over from part 1 kept the unqualified types
2624
+ ISO 14289-2 replaced: the declaration says the namespace exists, an element's
2625
+ `/NS` says its type comes from it. Every element now names it, an element
2626
+ that already names a different namespace keeps it, and `validate_pdfua(2)`
2627
+ reports the ones that do not — after the root declaration, so the thing to
2628
+ fix first is not buried.
2629
+
2630
+ - **A render can say where it spent its time.** Pass a `PerformanceLogger`
2631
+ (`aspose_pdf.visualization`) to `Page.render()` and it records seconds per
2632
+ phase into a dictionary you own — nothing is timed without one, so a plain
2633
+ render pays nothing, and two renders in different threads keep their own
2634
+ numbers. This is what that class was for: it was a working stopwatch that
2635
+ nothing in the package ever fed. `VirtualizationPerformance`, the
2636
+ process-global one beside it, stays a caller-only stopwatch — a library that
2637
+ timed itself into module-level state would interleave two documents rendered
2638
+ at once.
2639
+
2640
+ - **CI cross-validates the library's output against an independent
2641
+ implementation.** Every other test asks whether the library agrees with
2642
+ itself; a new `cross-validate` job asks whether qpdf agrees — it parses and
2643
+ rewrites documents covering layers, tagging, PDF/A-4, PDF/UA-2, optimization
2644
+ and signing, and reads their structure back out (including that a signature's
2645
+ byte range really covers the whole file). `tests/test_cross_validation.py`
2646
+ skips when pikepdf is absent, so a plain checkout stays green. The same job
2647
+ writes one conformance sample per level with
2648
+ `scripts/write_conformance_samples.py` and runs veraPDF over them, publishing
2649
+ its report as an artifact — advisory rather than gating, because the useful
2650
+ output is which rules a sample fails.
2651
+
2652
+ - **`auto_tag` sees four shapes it used to miss.** A table row may now leave a
2653
+ column **blank** (it gets an empty `/TD`, so the cells after it stay in their
2654
+ own columns instead of shifting left) and a cell reaching past the next
2655
+ column is a **merge** carrying `/ColSpan`, which the HTML export writes as
2656
+ `colspan`. A **wide-gutter table** is no longer cut into page columns first:
2657
+ a clear gap whose text fills less than a third of the band, with content on
2658
+ both sides of every line, is a table's column gap, and splitting it read the
2659
+ rows inside out. A list item indented past the one before it opens a
2660
+ **sub-list** inside that item's `/LBody` rather than continuing flat. And a
2661
+ list whose markers are **drawn rather than typed** — a glyph-sized image
2662
+ beside each line — becomes an `/L` whose items carry the image as their
2663
+ `/Lbl`, instead of loose paragraphs with pictures between them.
2664
+
2665
+ - **PDF/A-4 and PDF/UA-2 — the PDF 2.0 conformance parts — are validated and
2666
+ produced.** Both are defined *on* PDF 2.0 rather than capped by it, so the
2667
+ header is required to be exactly 2.0 and conversion raises an older one.
2668
+ ISO 19005-4 dropped the accessible/basic/unicode split: the levels are `"4"`
2669
+ (no conformance letter at all), `"4e"` (engineering — the level that exists
2670
+ to permit 3D and rich media) and `"4f"` (embedded files of any type), plus a
2671
+ required `pdfaid:rev` of 2020. ISO 14289-2 adds a `pdfuaid:rev` of 2024 and
2672
+ the PDF 2.0 standard structure namespace in the struct root's `/Namespaces`;
2673
+ reach it with `validate_pdfua(part=2)` and `convert_to_pdfua(part=2)`.
2674
+
2675
+ - **A signature field's `/SV` seed value is honoured, not just read.** It is
2676
+ the field author's instruction to whoever signs, and only `/SubFilter` and
2677
+ `/Reasons` were checked — a field demanding SHA-512, a particular handler, a
2678
+ timestamp or a certifying signature got one that quietly ignored the demand.
2679
+ Every entry now either binds or is refused, and several are *followed*:
2680
+ `/DigestMethod` picks the digest (SHA-1 and RIPEMD160 are refused rather than
2681
+ silently downgraded to), `/TimeStamp /URL` supplies the authority when the
2682
+ caller named none, and `/LockDocument /true` makes the signature certify.
2683
+ `/MDP` binds regardless of `/Ff`, being the one entry with no flag.
2684
+ `Form.add_signature_field(seed_value=…)` can author all of them.
2685
+
2686
+ - **Whole-document signing and field signing are one path.**
2687
+ `SimplePdf.signing_creds` used to synthesise its own field and patch its own
2688
+ byte range inside the legacy writer, which meant a signed save rebuilt the
2689
+ file from the in-memory model — silently dropping form fields and anything
2690
+ else only the COS writer preserves. It now authors a field (or reuses one the
2691
+ caller already authored, seed value and all), saves normally, and fills it
2692
+ with `sign_field`.
2693
+
2694
+ - **Layers resolve for printing and exporting, not only for the screen.** A
2695
+ group's `/Usage` dictionary says what it should do for an event and the
2696
+ configuration's `/AS` usage application dictionaries are what apply it —
2697
+ neither was read, so a watermark marked "do not print" printed anyway.
2698
+ `Document.layers.resolve("Print")` now reports the states an event calls for
2699
+ and `apply_usage("Print")` adopts them, which makes flattening a print copy
2700
+ actually drop the watermark. `Layer.set_usage(printing=False)` writes both
2701
+ halves, because a `/Usage` entry no `/AS` entry mentions changes nothing.
2702
+ Zoom ranges and BCP 47 language tags are evaluated when a magnification or
2703
+ locale is supplied, and left alone when one is not.
2704
+
2705
+ - **Alternate optional content configurations are listed and can be applied.**
2706
+ `Document.layers.configurations` reports `/D` and every `/Configs` entry with
2707
+ the layers it shows and locks, `apply_configuration(name)` adopts a preset as
2708
+ the document's state, and `save_configuration(name)` snapshots the current
2709
+ states as a new one. Removing a layer now also purges it from the alternates
2710
+ and from the usage applications, instead of only from `/D`.
2711
+
2712
+ - **The optimizer subsets the last two font kinds it left whole.** *CFF2*
2713
+ (`/FontFile3` with `/Subtype /OpenType`, PDF 2.0) is now erased glyph by
2714
+ glyph like every other embedded program, as a CIDFontType0 or as a simple
2715
+ font resolved through the sfnt's own `cmap`. CFF2 removed `endchar`, so an
2716
+ erased glyph there is a zero-length charstring, and its advance width — which
2717
+ CFF2 keeps in `hmtx` — is untouched. The `FDArray`/`FDSelect`, each Font
2718
+ DICT's `Private` and the `ItemVariationStore` move with it, so a subset
2719
+ variable font still instantiates; verified against fontTools at four weights.
2720
+ *MacExpertEncoding* is now bundled alongside Standard/WinAnsi/MacRoman, as
2721
+ are the predefined **Expert** and **ExpertSubset** CFF charsets, whose glyph
2722
+ ordering the specification fixes rather than the font storing it. A
2723
+ `/BaseEncoding` outside the four names PDF 32000-1 allows still keeps the
2724
+ font whole — that name is malformed rather than unsupported, and guessing
2725
+ which was meant could erase a used glyph.
2726
+
2727
+ - **An Expert-encoded CFF or Type 1 font draws real glyphs instead of boxes.**
2728
+ The same two tables the optimizer needed are what the renderer was missing:
2729
+ a font under `/MacExpertEncoding` now resolves its oldstyle figures and small
2730
+ caps through its charset, and extracts as text through the Adobe Glyph List.
2731
+
2732
+ - **A variable CFF2 font can be drawn at a chosen instance.** The
2733
+ ItemVariationStore's regions are read, `fvar` supplies the axes (with `avar`
2734
+ applied) and each `blend` resolves to `default + Σ scalar x delta`, verified
2735
+ against fontTools' own instancer. Substitute faces use it to reach a style:
2736
+ a modern system font ships as one variable file rather than four static
2737
+ ones, so asking for Bold now moves the `wght` axis instead of settling for
2738
+ the default master.
2739
+
2740
+ ### Fixed
2741
+
2742
+ - **Inserting an existing page produced a blank one.** `pages.insert(index,
2743
+ page)` is documented as taking an existing page, but carried over only the
2744
+ media box and the content bytes: the new page named fonts and images that
2745
+ were not in *its* resources, so every reference dangled, nothing was drawn,
2746
+ and no structural error showed for it — annotations were dropped too. A copy
2747
+ now keeps the page's resources, rotation and boxes, takes its own content
2748
+ stream so the two can be edited apart, and gets fresh copies of the
2749
+ annotations (widgets excepted: one is a form field's presence on a page, and
2750
+ duplicating it would put a single field in two places). A page from another
2751
+ document is refused rather than half-copied — `Document.merge` is what brings
2752
+ pages across.
2753
+
2754
+ - **A rich-text field whose widget was a separate object never showed its
2755
+ markup.** `/RV` is a field-level entry, like `/V`, so a field with its widget
2756
+ under `/Kids` — which is what this library's own `Form.add_text_field`
2757
+ produces — keeps it on the field. It was read off the widget alone, so every
2758
+ such field rendered its plain `/V` and the markup was never seen. It is now
2759
+ inherited down the field tree beside `/V`.
2760
+
2761
+ - **Opening a document and saving it made the file bigger, every time.**
2762
+ Outlines and attachments live in the model, not the COS graph, and are
2763
+ rebuilt on each save — claiming fresh object numbers as they went, so four
2764
+ objects and several hundred bytes of superseded copies accumulated per round
2765
+ trip. An incremental save was worse: an object rebuilt under a new number can
2766
+ never compare equal to the one it replaces, so the whole tree was appended
2767
+ every time. Each rebuild now takes over the numbers its previous copy
2768
+ occupied, which makes an unchanged save byte-for-byte identical and an
2769
+ unchanged incremental save append nothing whatsoever.
2770
+
2771
+ - **Saving a loaded document dropped every attachment's MIME type, description
2772
+ and dates.** They were read into the model at load and then ignored by the
2773
+ writer, so a round trip silently discarded them — and, incidentally, kept an
2774
+ otherwise untouched document from reproducing itself. What the file carried
2775
+ is now written back out, with anything the caller set taking precedence.
2776
+
2777
+ - **Every appended revision this library wrote had a malformed cross-reference
2778
+ section.** Entries are read by offset — a fixed twenty bytes each — and one
2779
+ byte too many put every entry after the first in a subsection out of step.
2780
+ Only the first still read, which is why a signature (whose appended objects
2781
+ are rarely consecutive) looked fine while an incremental save of two
2782
+ neighbouring objects did not: qpdf reported the file as damaged and rebuilt
2783
+ the table. The unit test measured each line after splitting on newlines, so
2784
+ it saw twenty bytes where the file had twenty-one; it now measures the
2785
+ section the way a reader indexes into it.
2786
+
2787
+ - **Re-saving a document opened with a password destroyed every string in it.**
2788
+ Strings are decrypted at load; without the writer putting the cipher back
2789
+ they were emitted in the clear under a trailer that still declared
2790
+ `/Encrypt`, so every reader dutifully "decrypted" them into noise — a title,
2791
+ a field name, a bookmark, an attachment name. qpdf read the title of such a
2792
+ file as an empty string. The writer now re-enciphers what it emits, and
2793
+ `tests/test_cross_validation.py` checks the result against qpdf.
2794
+
2795
+ - **A cross-reference stream with a PNG predictor could not be read at all.**
2796
+ `/Predictor 12` is what qpdf, Ghostscript and Acrobat write, so this is the
2797
+ shape most real cross-reference streams have. Two things stopped it: the
2798
+ decode limit was sized to the entries rather than to the predictor'd rows
2799
+ inflate actually produces, so the stream was rejected as oversized, and
2800
+ `/DecodeParms` reached the filter layer as a COS dictionary, whose
2801
+ `get("Predictor")` misses — the predictor was skipped in silence and the
2802
+ entries read one byte out of step. Object streams were unreachable in such a
2803
+ file as a result.
2804
+
2805
+ - **`Document.decrypt(password)` did not remove the protection.** It unlocked
2806
+ the document for reading and left `/Encrypt` in place, so the next save wrote
2807
+ an encrypted file — and, before the fix above, a corrupt one. It is the
2808
+ counterpart of `encrypt` again: the dictionary goes, `/O` and `/U` with it,
2809
+ and the next save is a plain file.
2810
+
2811
+ - **`MacRomanEncoding` was the Mac OS Roman character set, not the table PDF
2812
+ defines.** All 256 codes carried a name, where Annex D.2 defines 208 and
2813
+ leaves the rest to the font's own encoding — and once a name resolves,
2814
+ nothing looks at the font again. A font whose encoding put something else at
2815
+ `0xB0` had the glyph named `infinity` kept and the glyph it actually shows
2816
+ erased. The base table is now PDF's (with `space`, not `nbspace`, at `0xCA`),
2817
+ and the 36 extra Mac OS Roman names that are real glyphs are kept as a
2818
+ supplement consulted *after* the font — where they still resolve the codes a
2819
+ Mac-produced font leaves to convention.
2820
+
2821
+ - **`IPath` accepted path segments and dropped them.** The compatibility
2822
+ surfaces are meant to be constructible but to raise when used, so a caller
2823
+ finds out at the call rather than from a blank page;
2824
+ `IPath.append_cubic_bezier_curve` silently did nothing instead. It now raises
2825
+ `UnsupportedFeatureException` like every other placeholder. `FillMode` and
2826
+ `IMatrix` beside it are genuine value objects and are unchanged.
2827
+
2828
+ - **Pages were written without a resource dictionary.** `/Resources` is
2829
+ required and inheritable (ISO 32000-1 table 30), and qpdf reported every
2830
+ document this library produced as needing repair. It now goes on the page
2831
+ tree root, where a page that has none inherits it — putting an empty one on
2832
+ each page instead would shadow whatever a caller hung on an ancestor.
2833
+
2834
+ - **PDF/A-3 rejected the embedded files it exists to carry.** The check
2835
+ compared the requested level against the literal `"3"`, which no real level
2836
+ string ever equals, so `"3b"` fell on the prohibited side along with
2837
+ PDF/A-1 and -2. Asking for a PDF/A level that does not exist (`"4a"`, a typo)
2838
+ no longer falls back to PDF/A-1b either, which used to write metadata
2839
+ claiming a level nobody asked for.
2840
+
2841
+ - **A signature's `/ByteRange` left the `/Contents` delimiters inside the signed
2842
+ ranges.** Excluding only the hex digits still yields a verifiable signature,
2843
+ but a validator matches the gap against the `/Contents` string to prove the
2844
+ signature covers the document — and could not, reporting the coverage as
2845
+ indeterminate instead of "entire file" (confirmed with pyHanko before and
2846
+ after). The gap now runs from the `<` through the `>`.
2847
+
2848
+ - **A signature dictionary was written as `/Type /Signature`**, which is not a
2849
+ PDF object type; ISO 32000-1 table 252 says `/Sig`. `/ContactInfo` was
2850
+ written but never read back, so it was always `None` on a loaded signature.
2851
+
2852
+ - **A corrupt CFF2 INDEX could stall the optimizer.** A CFF2 INDEX counts its
2853
+ items in a uint32 rather than CFF1's uint16, so a malformed font could ask
2854
+ for four billion offsets out of a few bytes and be walked entry by entry. The
2855
+ count is now rejected when it cannot fit in the data that follows it.
2856
+
2857
+ - **A variable CFF2 font drew garbage, not its default master.** The CFF DICT
2858
+ parser stopped at CFF1's operator range, so the Top DICT's `vstore`
2859
+ (operator 24) was read as an operand and the ItemVariationStore was never
2860
+ found. With no region counts the `blend` operator could not tell a delta from
2861
+ a coordinate, and the deltas went on to be drawn as part of the outline. Both
2862
+ that and the two-byte length prefix in front of the store are fixed.
2863
+
2864
+ - **The optimizer recompresses the images it used to refuse.** Anything that
2865
+ was not 8-bit device gray/RGB/CMYK under a raster filter kept its original
2866
+ bytes however large: an Indexed, Lab, Separation or DeviceN image, a
2867
+ 1/2/4/16-bit one, a stencil mask, and every CCITT, JBIG2 or JPEG 2000
2868
+ payload -- which is most of a scanned document. Each is now brought to
2869
+ device samples first: a palette is folded into the samples (the space
2870
+ becomes `DeviceRGB`), a non-device space is converted through the same
2871
+ colour machinery the renderer uses, sub-byte and 16-bit samples are
2872
+ normalised to 8, and an opaque codec is decoded like any other filter. A
2873
+ stencil is a shape rather than a picture, so it is downscaled and re-packed
2874
+ at one bit per sample instead of being JPEG-encoded. The never-grow guard
2875
+ still has the last word, so an image is only rewritten when that actually
2876
+ saves bytes.
2877
+
2878
+ - **`GraphicsAbsorber` collects every mark a page makes.** It reported painted
2879
+ paths and placed XObject images and nothing else: text runs, inline
2880
+ (`BI`/`ID`/`EI`) images and `sh` shading fills were simply missing, a stroked
2881
+ path's box stopped at the geometry rather than covering the ink, and any
2882
+ colour set through a non-device space came back as `None`. All four are now
2883
+ covered. A text element carries the box its glyphs occupy, measured with the
2884
+ renderer's own font metrics -- so the reported box and the ink agree -- and
2885
+ invisible text (rendering mode 3 or 7) is still left out, because it puts
2886
+ nothing on the page. Only a pattern, which has no single colour, still
2887
+ reports no colour.
2888
+
2889
+ ### Fixed
2890
+
2891
+ - **An `/Indexed` fill colour was read as a grey level.** `Indexed` is not a
2892
+ shading colour space, so the shared converter had no branch for it and passed
2893
+ the palette *index* through as a colour component: `1 scn` in a two-entry
2894
+ palette painted white instead of the colour it names. The renderer and the
2895
+ graphics absorber both take the fix.
2896
+
2897
+ - **Layers can be created, written to and resolved.** Optional content was
2898
+ read-only: the layers a document declared could be listed and switched, but
2899
+ not created, not written to and never resolved. `Document.layers.add(name)`
2900
+ now creates a group (building the whole `/OCProperties` structure when the
2901
+ document had none), `Page.layer(layer)` is a context manager that marks
2902
+ everything authored inside it as belonging to that layer, and
2903
+ `Document.layers.remove(layer)` drops a group while leaving its content
2904
+ unconditionally visible.
2905
+ - **`Document.flatten_layers()` resolves optional content for good.** Switching
2906
+ a layer off changes what is *drawn*; the content stayed in the file and came
2907
+ back the moment somebody switched it on again -- a hidden draft watermark is
2908
+ still a draft watermark when the file leaves your hands. Flattening deletes
2909
+ what the configuration hides (marked content, XObject invocations and
2910
+ annotations alike) from the page's *existing* content streams rather than
2911
+ leaving them behind as unreferenced objects, drops every surviving `/OC`
2912
+ reference and marked-content wrapper, and removes `/OCProperties` -- leaving
2913
+ an ordinary PDF that renders byte-identically to what was visible.
2914
+
2915
+ - **Documents export as HTML and Markdown.** `DocFormat.HTML` and
2916
+ `DocFormat.MARKDOWN` were placeholders that raised. `Document.to_html()`,
2917
+ `to_markdown()`, `save_as_html()`, `save_as_markdown()` and
2918
+ `Document.save(path, DocFormat.HTML)` now produce real documents, as do the
2919
+ `HtmlSaveOptions` and `MarkdownSaveOptions` containers. This is a conversion
2920
+ to a *flowing document*, not a facsimile: the same layout analysis
2921
+ `auto_tag()` uses infers headings, paragraphs, bulleted and numbered lists,
2922
+ tables and figures, and the text is decoded exactly as `extract_text()`
2923
+ decodes it -- so the export and the tag tree agree by construction rather
2924
+ than by coincidence. Images are embedded through the same reconstruction
2925
+ `save_image()` performs, and Markdown is GFM escaped only where a character
2926
+ would otherwise change the meaning.
2927
+
2928
+ ### Fixed
2929
+
2930
+ - **A widely spaced table is no longer read as page columns.** The column
2931
+ splitter cut at the widest gap between text anchors, and a table's cells
2932
+ leave exactly the gap a two-column page does -- so a page with a table came
2933
+ out column-major, its prose interleaved with cell contents and the grid never
2934
+ reaching table detection. A gutter now has to be *whitespace*: a split is
2935
+ rejected when any line's extent runs across it, which the full-width prose
2936
+ above and below a table always does. `auto_tag()` gets the same fix.
2937
+
2938
+ - **Pages export as SVG.** `DocFormat.SVG` was a placeholder that raised.
2939
+ `Page.to_svg()`, `Page.save_as_svg()`, `Document.save_as_svg()` and
2940
+ `Document.save(path, DocFormat.SVG)` now write real vectors. The exporter
2941
+ subclasses the rasterizer and replaces only its paint sinks, so every
2942
+ operator, transform and resource lookup is the same code that renders the
2943
+ page -- which is what keeps the two outputs agreeing. Paths carry their fill
2944
+ rule, strokes their width, dash pattern, cap and join, clips become
2945
+ `<clipPath>`, text becomes glyph outlines, images embedded PNGs placed by
2946
+ their matrix, and axial/radial shadings SVG gradients; a mesh or
2947
+ function shading is sampled into an image rather than dropped. Verified by
2948
+ rendering the output with cairo and comparing against this library's own
2949
+ raster.
2950
+
2951
+ ### Fixed
2952
+
2953
+ - **`Q` restores the clipping path.** The renderer intersected one global mask
2954
+ and never gave it back, so everything after a `q … W n … Q` stayed clipped to
2955
+ a region that had already ended -- which is most documents with a figure in
2956
+ them. The clip is graphics state (ISO 32000-1 8.4.4) and is now saved and
2957
+ restored with it, at no copying cost: `q` stacks a reference and the clip
2958
+ builder makes a new mask instead of editing in place.
2959
+
2960
+ - **JPEG 2000 decodes without Pillow.** `/JPXDecode` needed the optional
2961
+ `images` extra, and without it the filter raised, the stream decoder fell
2962
+ back to handing the *raw codestream* to its caller, and the rasterizer
2963
+ painted those compressed bytes as if they were samples -- a page of noise
2964
+ where the scan should be. `aspose_pdf.engine.jpeg2000` is a pure-Python
2965
+ decoder covering the JP2 container and the bare codestream, tier-2 packet
2966
+ decoding (tag trees, all five progression orders, precincts, quality layers,
2967
+ tiles, SOP/EPH markers), the EBCOT tier-1 block decoder over the MQ
2968
+ arithmetic coder, the 5/3 and 9/7 wavelets, both colour transforms and
2969
+ component subsampling. The reversible path is lossless -- it reproduces the
2970
+ encoder's input exactly -- and the irreversible path agrees with OpenJPEG to
2971
+ within a step or two per channel; both were cross-checked against it over
2972
+ tiles, precincts, progression orders, code-block sizes, layers and bit depths.
2973
+ Pillow is still preferred when installed, being several hundred times faster
2974
+ on a full-size scan, and an image that neither decoder can read is now left
2975
+ undrawn rather than painted as noise.
2976
+
2977
+ - **Public-key encryption (`/Adobe.PubSec`).** A document encrypted for
2978
+ certificate recipients could not be opened at all -- not a degraded read, a
2979
+ hard failure -- and there was no way to produce one.
2980
+ `Document.encrypt_for_recipients([Recipient(cert), ...])` now writes one and
2981
+ `Document(source, certificate=..., private_key=...)` opens one. There is no
2982
+ password: a random seed is wrapped in a CMS `EnvelopedData` per recipient and
2983
+ the file key is a hash over that seed and every recipient blob. Each
2984
+ recipient carries its **own** permissions -- one reader may print and another
2985
+ only read the same file -- which no password scheme can express. AES-256
2986
+ (`adbe.pkcs7.s5`), AES-128 and RC4-128 (`adbe.pkcs7.s4`) are written; on read,
2987
+ `/Recipients` is found in the crypt filter or the dictionary depending on
2988
+ `/V`, and PKCS#1 v1.5 or OAEP key transport over AES-CBC or 3DES-CBC content
2989
+ encryption is accepted. Encrypting to a certificate whose `keyUsage` forbids
2990
+ key transport is refused rather than producing a file the recipient cannot
2991
+ open. Verified in both directions against pyHanko and, for the CMS layer,
2992
+ OpenSSL.
2993
+
2994
+ - **Non-embedded fonts can be drawn with real faces.** The renderer only ever
2995
+ had the bundled Latin and symbol substitutes, so a font with no embedded
2996
+ program that was not one of the Standard 14 drew glyph boxes -- and a
2997
+ composite (Type0/CID) font had no substitute path at all, which is every
2998
+ East Asian PDF that leaves the system fonts unembedded. Assigning a
2999
+ `FontSubstitutionOptions` (to `Document.font_substitution`, or per call to
3000
+ `Page.render` / `Document.render_page`) points the renderer at font
3001
+ directories, font programs supplied as bytes, or the platform's own fonts
3002
+ (`FontSubstitutionOptions.system()`). A face is resolved by the document's
3003
+ `/BaseFont` name against the real `name` tables of the indexed fonts, then --
3004
+ for a composite font -- by the well-known families of its character
3005
+ collection, so a PDF naming `SimSun` renders on a machine that only has
3006
+ `PingFang SC`, and finally by `cmap` coverage of the text itself. CIDs reach
3007
+ Unicode through the font's own `/ToUnicode` and Adobe's bundled
3008
+ CID-to-Unicode table for the collection. Advances still come from the PDF's
3009
+ `/Widths` / `/W`, so a substitute changes which glyphs are drawn, never where
3010
+ they sit (a simple font that omits `/Widths` for a code still falls back to
3011
+ the face's own advance, as it did for the bundled substitutes). Indexing reads only each face's table directory, `name` table and
3012
+ `OS/2` ranges -- about 1200 system faces in under a second -- and pulls a
3013
+ whole program (lifting a TrueType Collection face out of its collection) only
3014
+ for a face that wins. Discovery is opt-in: without options rendering is
3015
+ byte-for-byte what it was, and independent of what the machine has installed.
3016
+ `SystemFontSource` also now looks in macOS's `/System/Library/AssetsV2`,
3017
+ where downloadable system fonts (PingFang, Hiragino and the other CJK
3018
+ families) are installed.
3019
+
3020
+ - **Optional content (layers) is honoured.** Nothing in the package looked at
3021
+ `/OC`, so a hidden layer — a draft watermark, an alternate language, a CAD
3022
+ overlay — was painted and extracted like any other content. Rendering, text
3023
+ extraction and `GraphicsAbsorber` now skip content in a group the default
3024
+ configuration turns off: marked-content sections (with nested `BDC`/`BMC`
3025
+ tracked to the matching `EMC`), image and form XObjects carrying their own
3026
+ `/OC`, and annotations. `/OCMD` is resolved under its `/P` policy and simple
3027
+ `/VE` expressions, and `/BaseState` is honoured.
3028
+ - **`Document.layers`.** Lists the document's optional content groups with
3029
+ their names and state; setting `layer.visible` rewrites the default
3030
+ configuration's `/ON`/`/OFF`, which the renderer, the extractor and a later
3031
+ `save()` all follow.
3032
+ - **Rendered pages encode to compressed TIFF, JPEG, greyscale and bilevel.** A
3033
+ raster could only be written as PNG or as an *uncompressed* RGB TIFF — an A4
3034
+ page at 300 dpi came to about 25 MB of file for a page of text. `to_tiff()`
3035
+ now Deflate-compresses by default (`compression="none"` keeps the raw strip),
3036
+ `to_jpeg(quality=…)` uses the bundled encoder, and `mode="gray"`/`"bilevel"`
3037
+ (with a `threshold=`) cut a text page down further; `save()` picks the format
3038
+ from the suffix, `.jpg`/`.jpeg` included. Both encoders record the render
3039
+ resolution — TIFF in its resolution tags, JPEG as the JFIF pixel density.
3040
+ - **`Document.save_as_tiff()` writes a multi-page TIFF.** Every page becomes one
3041
+ image in a single file (`pages=` selects and orders them), rendered and
3042
+ encoded one at a time so only the compressed result accumulates.
3043
+ - **`GraphicsAbsorber` actually absorbs graphics.** `visit()` cleared its
3044
+ collection and returned nothing at all, while the documentation described it
3045
+ as collecting a page's graphic elements. It now walks the content stream the
3046
+ way the rasterizer does — tracking `q`/`Q`/`cm`, descending into form
3047
+ XObjects with each `/Matrix` composed, bounded in depth and terminating on a
3048
+ form that draws itself — and reports each painted path and placed image as a
3049
+ `GraphicElement`: bounding box in page space (curves bounded exactly, not by
3050
+ their control points), paint operation, image resource name, device fill and
3051
+ stroke colour, and stroke width in page space. Text, inline images and `sh`
3052
+ fills are out of scope and documented as such.
3053
+ - **`Document.encrypt(..., algorithm=…)`.** The engine has always supported
3054
+ AES-256, AES-128 and RC4, but the public API could only reach the default.
3055
+ Names are normalised (`aes256`, `AES_128`, `rc4-128`), and an unrecognised
3056
+ one raises `PdfSecurityException` instead of silently falling back to a
3057
+ weaker cipher.
3058
+
3059
+ - **The page rasterizer draws annotations.** It rendered page content only, so
3060
+ every highlight, stamp, sticky note and form-field widget was missing from a
3061
+ rendered page even with its `/AP` present. Each visible annotation's normal
3062
+ appearance is now composited, placed by fitting its `/Matrix`-transformed
3063
+ `/BBox` to `/Rect` (ISO 32000-1 12.5.5), with `/AS` selecting among appearance
3064
+ states and Hidden/NoView annotations and `Popup` subtypes skipped. A malformed
3065
+ annotation is skipped rather than allowed to abort the page.
3066
+ `Page.render(draw_annotations=False)` (also on `Document.render_page`) keeps
3067
+ the old content-only behaviour.
3068
+
3069
+ - **Standard icons for `Text` and `FileAttachment` annotations.** Two of the
3070
+ most common annotations in a reviewed document could not synthesise an
3071
+ appearance at all — `generate_appearances()` declined and left them with no
3072
+ `/AP`. Both now draw their standard `/Name` icon as vector artwork (`Comment`,
3073
+ `Key`, `Note`, `Help`, `NewParagraph`, `Paragraph`, `Insert`; `PushPin`,
3074
+ `Graph`, `Paperclip`, `Tag`), honouring `/C`, squared and centred in the
3075
+ annotation rectangle, with an unknown name falling back to the subtype's
3076
+ default the way a viewer does.
3077
+
3078
+ - **`Page.add_image()` accepts every PNG form.** Adam7 **interlacing** and bit
3079
+ depths **1/2/4/16** were hard rejections; an ordinary interlaced or 16-bit PNG
3080
+ could not be embedded at all. The decoder now reassembles the seven interlace
3081
+ passes and normalises each allowed depth to 8 bits per sample — 16-bit keeps
3082
+ the high byte, sub-byte greyscale is scaled to full range, and palette indices
3083
+ are looked up rather than scaled, which would have corrupted the colour. Bit
3084
+ depths a colour type does not allow (ISO 15948 table 11) are now rejected with
3085
+ a specific message instead of being decoded as garbage.
3086
+
3087
+ - **PDF/A-1 conversion drops inert transparency groups.** A `/Group /S
3088
+ /Transparency` on a page or form XObject is removed when nothing it reaches
3089
+ actually uses transparency — no ExtGState soft mask, non-Normal blend or alpha
3090
+ below 1, no image `/SMask`/`/Mask`, no nested group. Producers stamp such
3091
+ groups routinely, so this converts a real class of documents losslessly. The
3092
+ scan is conservative (every ExtGState in a resource dictionary counts, not just
3093
+ the ones the content selects), and transparency that is genuinely used stays
3094
+ and is reported: flattening it correctly requires compositing the page against
3095
+ its backdrop — rasterizing away live text and vectors — which is the caller's
3096
+ decision, not a silent repair.
3097
+ - **Push-button icons.** `Form.add_push_button(icon=…)` takes JPEG or
3098
+ non-interlaced 8-bit PNG bytes, wraps them in a form XObject as `/MK /I` (an
3099
+ icon must be a *form*, not an image), and draws it into the normal, rollover
3100
+ and down faces scaled proportionally and centred. `/MK /IF` is written to
3101
+ match the baked appearance, and `/MK /TP` is 1 for an icon alone or 2 with the
3102
+ caption below it.
3103
+ - **Submit and reset form actions.** `SubmitFormAction` and `ResetFormAction`
3104
+ join the typed action API, covering the `/Fields` name list, the
3105
+ include/exclude flag, and the FDF / HTML / XFDF / PDF submit format. Action
3106
+ serialization now emits arrays and integers, not only strings.
3107
+
3108
+ - **The optimizer's DPI target follows form XObjects.** Display size was
3109
+ measured from page-level placements only, so an image drawn inside a form was
3110
+ invisible to `image_target_dpi` and kept its full resolution. Forms are now
3111
+ descended into, composing each form's `/Matrix` with the CTM at its `Do`, with
3112
+ bounded nesting and cycle detection.
3113
+ - **ICC-based CMYK images are recompressed.** `/ICCBased` with `/N 4` is CMYK,
3114
+ which the JPEG encoder already handled; only the colour-space probe rejected
3115
+ it.
3116
+ - **An inverting `/Decode` array is folded into the samples.** `[1 0]` per
3117
+ component — the common form on inverted scans — is reproduced exactly by
3118
+ inverting the samples and dropping the array, so such images are no longer
3119
+ skipped. Other sample remappings are still left alone.
3120
+ - **Masks are downscaled with the image that carries them.** A mask was skipped
3121
+ outright, so a full-resolution soft mask survived a downscale of its image.
3122
+ Masks now follow the image's display size but are never JPEG-encoded.
3123
+
3124
+ ### Fixed
3125
+
3126
+ - **`ImagePlacementAbsorber` found nothing when handed a page.** It understood
3127
+ only the internal engine object, so `visit(document.pages[0])` — the obvious
3128
+ call, and the one the docs describe — returned an empty list. It now accepts a
3129
+ `Page` (that page's images), a `Document`, or the engine object, and still
3130
+ accepts objects that carry image data directly.
3131
+ - **Image placement rectangles were scaled by the raster's pixel size.** An
3132
+ image is painted into the unit square of its own space (ISO 32000-1 8.9.5.2),
3133
+ so a 200×100 image drawn 100pt wide reported a 20000×5000pt rectangle.
3134
+ `ImagePlacement.resolution` now reports real DPI — pixels over the size drawn
3135
+ on the page — instead of a hardcoded 72.
3136
+ - **Pages that reuse a resource name lost all but the last image.** Resource
3137
+ names are page-local and most producers restart at `/Im0` on every page, but
3138
+ images were stored under the bare name, so each collision overwrote the
3139
+ previous image's bytes, size and metadata. Images are now keyed uniquely per
3140
+ document (a name taken by a different object gets a numbered suffix) while a
3141
+ single XObject shared by several pages still stores one copy.
3142
+ - **Encrypted PDFs written by other tools could not be opened.** Only AES-256
3143
+ worked. A 128-bit RC4 document (`/V 2 /R 3`) was misread as AES-128 because
3144
+ the cipher was guessed from `/V`/`/R` alone instead of the crypt filter
3145
+ `/StmF` selects, and AES-128 (`AESV2`) failed because the file key was used
3146
+ directly as the object key: ISO 32000-1 Algorithm 1 derives a per-object key
3147
+ from the object and generation number, with a `sAlT` suffix for AES. Both are
3148
+ implemented now, and qpdf-produced fixtures for every standard-handler
3149
+ flavour — RC4-40, RC4-128, AES-128, AES-256 revision 5 and 6 — are part of
3150
+ the test suite. A failure to decrypt raises `PdfSecurityException` rather
3151
+ than a bare cipher `ValueError`.
3152
+ - **Encrypted documents this library wrote were readable only by this library.**
3153
+ Three defects compounded: the `/Encrypt` dictionary declared a revision whose
3154
+ key derivation was not the one used (`/V 5 /R 5` for keys built with the
3155
+ revision 6 algorithm 2.B, `/V 1 /R 2` for a 128-bit RC4 key), `/Perms` and
3156
+ `/Length` were missing, and — worst — the trailer `/ID` was regenerated at
3157
+ save time while the key had been derived from a different one, so every
3158
+ conforming reader computed a different key and rejected the correct password.
3159
+ The dictionary is now written to match the keys, the `/ID` is bound to the
3160
+ derivation, and `/Perms` follows the revision 6 layout. Output opens in qpdf
3161
+ with either password for all three algorithms.
3162
+ - **Strings in an encrypted document.** They were neither encrypted on write
3163
+ nor decrypted on read, so a title or annotation text sat in the clear inside
3164
+ an otherwise encrypted file. Strings are now encrypted with their object's
3165
+ key (a signature's `/Contents` and the `/Encrypt` dictionary excepted) and
3166
+ decrypted as objects are materialised, including for documents produced
3167
+ elsewhere.
3168
+ - **Images and form XObjects vanished from an encrypted document.** Loading with
3169
+ a password decrypts the page contents and then cleared the key, but every
3170
+ other stream -- images, form XObjects, appearance streams -- is decoded on
3171
+ demand from the COS graph and still needed it, so a rendered page of an
3172
+ encrypted PDF came out with its images missing. The key the graph needs is
3173
+ now kept for as long as those bytes are around, separately from the
3174
+ writer-facing one.
3175
+ - **A document protected by an owner password only would not open.** An empty
3176
+ user password is a valid password that every reader tries before asking for
3177
+ one; loading demanded a password and raised instead.
3178
+ - **AES-256 revision 5 documents were rejected.** Password verification always
3179
+ used the revision 6 hardened hash; revision 5 — the deprecated Adobe
3180
+ extension, still found in the wild — is a single SHA-256 and is now handled
3181
+ by revision.
3182
+
3183
+ - **The PDF/A checker accepted DeviceCMYK under an sRGB output intent.** It only
3184
+ required *some* structurally valid ICC profile, where ISO 19005-1 6.2.3.3 ties
3185
+ each device colour family to the output intent's own space. The destination
3186
+ profile's colour space is now read from the ICC header and matched against the
3187
+ device colour actually used (DeviceGray is satisfied by any intent).
3188
+ - **Device colour set by operator went undetected.** The scan looked only for
3189
+ the names `/DeviceRGB`/`/DeviceGray`/`/DeviceCMYK`, missing `k`/`K`, `rg`/`RG`
3190
+ and `g`/`G` — the most common way content selects a device space — so most
3191
+ non-conformant colour was never reported. `DeviceGray` is also tracked
3192
+ separately from `DeviceRGB` now; folding them together would flag a valid
3193
+ CMYK-intent document that only draws gray.
3194
+
3195
+ ## [0.1.0] - 2026-08-19
3196
+
3197
+ ### Added
3198
+
3199
+ - **Every Adobe predefined CJK CMap is now bundled** — 141 names across Japan1,
3200
+ Korea1, GB1 and CNS1 (both `-H` and `-V`), up from 8 name pairs. Text
3201
+ extraction, editing, redaction geometry and glyph rendering now work without
3202
+ `/ToUnicode` for the whole Unicode family (`UCS2`/`UTF8`/`UTF16`/`UTF32`,
3203
+ including `HW` and `JIS2004` variants) and the legacy encodings (`RKSJ`,
3204
+ `EUC`, `UHC`, `Johab`, `GBK`/`GBK2K`, `B5`, `ETen`, `HKscs`, the `pc`/`pv`/`ms`
3205
+ platform variants). The `Adobe-<Ordering>-<N>` CMaps stay excluded: their
3206
+ codes already are CIDs, not an encoding.
3207
+ - **Unicode-keyed CMaps take the scalar from the code**, not from
3208
+ code → CID → Unicode. This already applied to `-UTF16-`; it now covers
3209
+ `-UCS2-`, `-UTF8-` and `-UTF32-` too. Those codes *are* the character, so text
3210
+ and code stay a bijection and a replacement can be written back — Adobe maps
3211
+ both U+2F47 and U+65E5 to Japan1 CID 3284, which previously made such
3212
+ characters extractable but not replaceable under those names.
3213
+
3214
+ - **Font subsetting covers predefined encodings.** Simple fonts now resolve a
3215
+ used code through `/Differences`, then the predefined base encoding, then the
3216
+ font program's own built-in encoding — one shared step for TrueType, CFF and
3217
+ Type 1. This lifts three limitations: a simple CFF with a PDF `/Encoding`
3218
+ override, a simple CFF carrying a predefined (Standard) encoding, and a Type 1
3219
+ font whose codes need a predefined base encoding were all left whole before.
3220
+ A base encoding outside the bundled tables (`MacExpertEncoding`, unrecognised
3221
+ names) still bails, so a used glyph is never erased.
3222
+
3223
+ ### Changed
3224
+
3225
+ - **The bundled CMap tables are split one file per character collection**,
3226
+ behind a small index. A document names exactly one collection through its
3227
+ `CIDSystemInfo`, so only that file is decompressed; a single combined file
3228
+ would make one CJK document pay for all four. `supported_cmap_names()` — called
3229
+ while parsing every composite font — is now answered from the index alone
3230
+ instead of loading the whole bundle. Despite carrying 8.8x more CMaps, the
3231
+ worst-case resolve loads ~35 MB where the old combined bundle loaded ~24 MB.
3232
+ Only the index digest is pinned in code; it pins each collection file in turn.
3233
+ `scripts/build_cmap_data.py` now takes `--output-dir` instead of `--output`,
3234
+ and refuses to emit a collection whose `usecmap` bases are not all in it.
3235
+
3236
+ - **Signing an authored signature field.** `engine.sign_field.sign_field()`
3237
+ fills the `/FT /Sig` field created by `Form.add_signature_field()` as an
3238
+ incremental update: the original bytes are emitted verbatim, so a signature
3239
+ already in the document stays valid and the surrounding COS structure
3240
+ (widgets, other fields, outlines, annotations) is preserved instead of being
3241
+ rebuilt. Several fields can be signed in turn. Covers `adbe.pkcs7.detached`
3242
+ and PAdES (`pades=True`), an embedded chain, local/network timestamps, and
3243
+ DocMDP certification (writing `/Perms /DocMDP`). Previously the only signing
3244
+ path rebuilt the whole file and synthesised its own single field, so an
3245
+ authored field could not be signed at all.
3246
+ - **Signature seed values and field locks.** `Form.add_signature_field()`
3247
+ accepts `seed_value=` (`/SV`: `filter`, `sub_filter`, `digest_method`,
3248
+ `reasons`, and `required` naming the entries whose ISO 32000-1 table 234
3249
+ `/Ff` bit makes them binding) and `lock=` (`/Lock`: `action` of
3250
+ `All`/`Include`/`Exclude` with `fields`). At signing time a required
3251
+ `/SubFilter` or `/Reasons` is enforced, and a `/Lock` becomes a **FieldMDP**
3252
+ signature reference.
3253
+
3254
+ - **PDF/A conversion rewrites CMYK images to DeviceRGB.** `convert_to_pdfa`
3255
+ already normalized DeviceCMYK *content* colour; it now also converts CMYK
3256
+ **image XObjects** — `/DeviceCMYK` and ICC-CMYK (`/ICCBased` `/N 4`), raw or
3257
+ `DCTDecode` — to 8-bit `DeviceRGB` re-encoded with `FlateDecode`, dropping the
3258
+ stale `/Decode`/`/DecodeParms`. Pixels go through the same decode path the
3259
+ renderer uses (Adobe de-inversion, YCCK), so the page looks the same.
3260
+ Transparency remains reported, not converted.
3261
+ - **PDF/A conversion repoints `/Separation` and `/DeviceN` off CMYK.** A space
3262
+ whose alternate resolves to DeviceCMYK or ICC-CMYK now gets a resampled Type 0
3263
+ tint transform over `DeviceRGB` (PDF has no way to compose the original
3264
+ transform with a CMYK→RGB conversion). The space keeps its kind, colorant
3265
+ names and component count, so content streams selecting it need no rewriting,
3266
+ and a Separation/DeviceN image keeps its tint samples. A linear transform is
3267
+ reproduced exactly; a curved one to within a step or two per channel.
3268
+ - The page renderer now draws glyphs for Type0 fonts whose `/Encoding` names one
3269
+ of the eight bundled predefined CJK CMaps, instead of falling back to boxes. It
3270
+ splits the show string on the CMap's mixed single/double-byte codespaces, maps
3271
+ each code to a CID against the descendant `CIDSystemInfo`, and fills the real
3272
+ outlines through `CIDToGIDMap` (CIDFontType2) or the CFF charset
3273
+ (CIDFontType0). Unbundled names, embedded CMap streams, and mismatched
3274
+ collections still render as boxes.
3275
+ - `Document(...)` no longer discards its arguments. It now loads the supplied
3276
+ source with the same semantics and errors as `Document.load_from(...)`, and
3277
+ rejects unknown arguments, so `Document("input.pdf")` can no longer return an
3278
+ empty document. `aspose_pdf.generated.document.Document` gained the same
3279
+ signature.
3280
+ - Compatibility placeholders for unimplemented formats now fail explicitly
3281
+ through the new `UnsupportedFeatureException` (also a `NotImplementedError`):
3282
+ load options such as `SvgLoadOptions` are rejected by the constructor and
3283
+ `load_from()`, and `Document.save(destination, save_format)` rejects
3284
+ `SaveFormat.PPTX`, non-PDF `DocFormat` members, `HtmlSaveOptions`,
3285
+ `MarkdownSaveOptions`, and `PrinterSettings` before writing anything.
3286
+ - Documented the full inventory of unimplemented compatibility surfaces in
3287
+ `supported-features.md`.
3288
+ - Extended the scheduled security audit to install and audit the `text-layout`
3289
+ extra (uharfbuzz, python-bidi, fonttools) alongside `images` and `woff2`, and
3290
+ made the minimal-install CI job assert those three modules are absent too, so
3291
+ every optional runtime dependency is covered by both gates.
3292
+ - Added function-based PDF shadings with multidimensional sampled functions,
3293
+ shading matrix/domain/bounds/background handling, and Separation/DeviceN
3294
+ alternate-colour conversion.
3295
+ - Replaced fixed patch-mesh tessellation with bounded device-scale-adaptive
3296
+ subdivision and added composite preview for common CMYK and spot overprint
3297
+ cases through `/OP`, `/op`, and `/OPM`.
3298
+ - Extended the attachment API with associated-file relationships and typed
3299
+ removal: `Document.add_attachment(..., relationship=...)` writes a validated
3300
+ `/AFRelationship` (`AF_RELATIONSHIPS`), `FileSpecification` exposes a
3301
+ `relationship` field read back from the file spec, and
3302
+ `Document.remove_attachment(name)` deletes an embedded file (dropping the
3303
+ `/Names /EmbeddedFiles` tree when the last one is removed). Re-adding a name
3304
+ now fully supersedes any previously loaded metadata.
3305
+ - Added public signature-field authoring: `Form.add_signature_field(name, page,
3306
+ rect, ...)` creates an empty `/FT /Sig` field with a page widget, sets the
3307
+ AcroForm `/SigFlags` SignaturesExist bit (preserving existing bits), renders an
3308
+ empty box, and carries no value until signed. Signature fields round-trip,
3309
+ report as a `signature` field type (new `FieldType.SIGNATURE`), and can be
3310
+ removed like any other field. Signing an authored field and seed-value/lock
3311
+ dictionaries remain out of scope.
3312
+ - Added a public byte-preserving incremental save: `Document.save(...,
3313
+ incremental=True)` emits the original file bytes verbatim and appends only the
3314
+ objects added or modified since load as a new revision chained through
3315
+ `/Prev`. Change detection compares each object's canonical (key-sorted)
3316
+ serialization against a re-parse of the original, so unchanged objects are not
3317
+ re-emitted and an existing signature's byte range stays intact. Documents
3318
+ built from scratch fall back to a full write; encrypted or to-be-signed
3319
+ documents are rejected.
3320
+ - Corrected the overprint composite preview to work in the subtractive device
3321
+ colorant model: a non-zero source colorant now replaces the backdrop colorant
3322
+ while a zero-tint colorant leaves it untouched (nonzero-overprint semantics for
3323
+ DeviceCMYK/DeviceGray, colorant isolation for Separation/DeviceN). The previous
3324
+ blanket `Multiply` darkened untouched colorants and could not replace a
3325
+ colorant with a lighter tint of itself.
3326
+ - Corrected project metadata links and added minimal-install CI coverage.
3327
+ - Added bounded fuzz targets and a redistributable parser corpus, replaced the
3328
+ skipped signature-extraction placeholder with an end-to-end test, and applied
3329
+ `PdfLoadLimits` to authored PNG and WOFF/WOFF2 decoding paths.
3330
+
3331
+ ### Fixed
3332
+
3333
+ - **`PdfSignature.valid` returned `True` for tampered documents.** Its digest
3334
+ comparison went through `cryptography`'s `load_der_pkcs7_signed_data`, which is
3335
+ absent in some releases; that absence — and any error while walking the signed
3336
+ attributes — was treated as success, so a document whose signed bytes had been
3337
+ modified reported `valid is True` while `validate()` correctly reported
3338
+ INVALID. Verification now goes through the same engine path `validate()` uses:
3339
+ the digest algorithm comes from the CMS, the `messageDigest` attribute is
3340
+ checked against the covered bytes, and the **signature value is verified**
3341
+ against the signer certificate — which the old code never did at all. This also
3342
+ fixes SHA-384/SHA-512 signatures, which the hardcoded SHA-256 comparison could
3343
+ never match.
3344
+ - **Subsetting resolved base encodings through the stdlib codecs**, which
3345
+ disagree with PDF's tables on real codes. MacRomanEncoding `0xDB` is
3346
+ `currency`, but `mac_roman` decodes it as the euro sign — so the subsetter
3347
+ reasoned about the wrong glyph. WinAnsiEncoding `0xA0`/`0xAD` are `space` and
3348
+ `hyphen`, where `cp1252` gives NBSP and a soft hyphen, whose scalars are absent
3349
+ from most fonts and made the subsetter give up and embed the whole font.
3350
+ Resolution now goes through the bundled Adobe tables and the Adobe Glyph List.
3351
+ - **PDF/A conversion never reached form XObjects.** The CMYK content walker
3352
+ tested `_get_page_resources()` — which returns a converted plain `dict` — with
3353
+ `isinstance(..., PdfDictionary)`, a condition that can never hold, so DeviceCMYK
3354
+ inside a form XObject was silently left in place. It now walks the live COS
3355
+ resource dictionary (following inherited `/Resources`).
3356
+ - **Attachments in nested embedded-file name trees are no longer invisible.**
3357
+ `/Names /EmbeddedFiles` was read only as a flat `/Names` array, so a document
3358
+ whose tree another producer balanced into `/Kids` sub-nodes (ISO 32000-1
3359
+ 7.9.6) reported *no attachments at all* — silently, with no error. The tree is
3360
+ now walked in full, preserving its order, with depth, cumulative entry count,
3361
+ and revisited nodes bounded by the shared `PdfLoadLimits` budget.
3362
+
3363
+ - **Typed action/destination API** (`aspose_pdf.interactive`): destination value
3364
+ objects (`FitDestination`, `XYZDestination`, `FitH/V/R/B`…) and actions
3365
+ (`GoToAction`, `URIAction`, `GoToRAction`, `NamedAction`, `JavaScriptAction`,
3366
+ `LaunchAction`), wired to link annotations (`Page.add_link(rect, target)`),
3367
+ outline items (`OutlineItem(..., destination=…)`, replacing the previous
3368
+ fixed fit-to-page `/Dest`), and push-button widgets
3369
+ (`Form.add_push_button(..., action=…)`). Serialized to COS `/A` and `/Dest`.
3370
+ - **Push-button visual states.** Push buttons now generate normal/rollover/down
3371
+ (`/AP` `N`/`R`/`D`) appearances, and `Form.add_push_button` accepts
3372
+ `border_color` / `background` (`/MK` `/BC` / `/BG`); the rollover and down faces
3373
+ are shaded variants of the background.
3374
+ - **Type0 (CID) field fonts.** `Form.add_text_field(font=…)` embeds a Type0 font
3375
+ in the AcroForm `/DR` and bakes a CID-encoded `/AP`, so a non-Latin field value
3376
+ renders through the embedded CID font. (`generate_appearances` leaves the baked
3377
+ Type0 appearance intact; Type0 rich text `/RC` falls back to the plain `/DA`.)
3378
+ - **PDF/A conversion embeds Standard-14 fonts and normalizes DeviceCMYK.**
3379
+ `convert_to_pdfa` now embeds non-embedded Standard-14 and Symbol/ZapfDingbats
3380
+ fonts with the bundled metric-compatible substitute (synthesizing `/Widths`
3381
+ from that face) even without a `font_lookup_directory`, and rewrites
3382
+ DeviceCMYK content color (`k`/`K` and `/DeviceCMYK` fills/strokes) to RGB using
3383
+ the renderer's device conversion so appearance is unchanged. CMYK image
3384
+ XObjects, Separation/DeviceN, ICC-CMYK, and transparency stay reported, not
3385
+ converted.
3386
+ - **Composite (Type0) text renders through embedded (stream) CMaps.** A font
3387
+ whose `/Encoding` is a CMap *stream* (rather than Identity or a bundled
3388
+ predefined name) is decoded with the same parser extraction uses and fills the
3389
+ descendant font's real glyphs instead of boxes. Named predefined CMaps outside
3390
+ the eight bundled tables remain boxed (no bundled code→CID table).
3391
+ - **True vertical writing (`WMode 1`).** Vertical CMaps — bundled or embedded —
3392
+ now offset each glyph by its `/W2`/`/DW2` position vector and advance the text
3393
+ downward by the vertical displacement, instead of advancing horizontally.
3394
+ - Bundled the full **Adobe Glyph List** (4281 names) as a deterministic,
3395
+ integrity-checked data file, and resolve glyph names through the Adobe AGL
3396
+ algorithm (exact list, `.`-suffix stripping, `_` ligature components, then the
3397
+ algorithmic `uniXXXX`/`uXXXX` forms). Text extraction and code→Unicode now
3398
+ handle named glyphs such as `aacute`, `Euro`, `afii10017`, and `f_f_i`, not
3399
+ just `uniXXXX`. Generated by `scripts/build_agl_data.py`; no runtime fontTools.
3400
+ - Simple **CFF and Type 1 fonts under a predefined encoding now render real
3401
+ glyphs instead of boxes.** Added CFF charset `name→gid` resolution and the
3402
+ bundled Standard/WinAnsi/MacRoman code→name tables, so a font whose `/Encoding`
3403
+ names a base encoding (or supplies `/Differences`) resolves through the font's
3404
+ own charset. Expert/MacExpert encodings remain boxed.
3405
+ - **CFF2 outline programs are rasterized** (default instance): the parser reads
3406
+ the CFF2 header, 32-bit INDEXes, FDArray/FDSelect, and Type 2 charstrings, and
3407
+ collapses variable-font `blend`/`vsindex` to the default master (region deltas
3408
+ dropped). CFF2 tables are also extracted from an OpenType wrapper. CFF2 is left
3409
+ whole by the optimizer and still rejected for authoring.
3410
+ - **WOFF2 font collections** (`ttcf`) are reconstructed into a TrueType
3411
+ Collection (shared tables deduplicated) via a new `build_ttc` assembler, after
3412
+ which the existing TTC face selection applies.
3413
+ - `Document.replace_text` / `Page.replace_text` now shape right-to-left and
3414
+ complex-script replacements (HarfBuzz + Unicode bidi) instead of encoding them
3415
+ code-for-code, and match such phrases in their stored visual order. A
3416
+ replacement is reused in the run's own embedded font when that font already
3417
+ carries every shaped glyph (an embedded, Identity-encoded `CIDFontType2` whose
3418
+ shaped advances match `/W` with no positioning adjustment); otherwise a
3419
+ shaping-capable `font=` is embedded and the replacement drawn at the match
3420
+ baseline, with optional `layout=TextLayoutOptions(...)` for direction, script,
3421
+ and features. When neither path applies the edit raises rather than emit
3422
+ misshaped glyphs, and reshaping needs the optional `text-layout` extra. Simple
3423
+ LTR/ASCII replacements keep the previous exact byte-splice.
3424
+ - The page renderer can join complex-script runs that fall back to a bundled
3425
+ substitute face, drawing cursive-connected forms instead of isolated glyphs
3426
+ (order-preserving, so nothing is reordered or repositioned), controlled by the
3427
+ new `shape_substitute_text` flag on `Document.render_page` / `Page.render`
3428
+ (default on). It needs the `text-layout` extra and is active only when the
3429
+ substitute face covers the script; the bundled Liberation/DejaVu faces cover
3430
+ Latin and symbol only, so it is a safe no-op for other scripts today. Embedded
3431
+ fonts are unaffected — their glyphs are already final.
3432
+
3433
+ [0.1.0]: https://github.com/aspose-pdf-foss/Aspose-PDF-FOSS-for-Python/releases/tag/v0.1.0