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.
- aspose_pdf_foss_for_python-0.1.0/CHANGELOG.md +3433 -0
- aspose_pdf_foss_for_python-0.1.0/LICENSE +21 -0
- aspose_pdf_foss_for_python-0.1.0/MANIFEST.in +4 -0
- aspose_pdf_foss_for_python-0.1.0/PKG-INFO +2233 -0
- aspose_pdf_foss_for_python-0.1.0/README.md +2183 -0
- aspose_pdf_foss_for_python-0.1.0/fuzz/README.md +20 -0
- aspose_pdf_foss_for_python-0.1.0/fuzz/__init__.py +1 -0
- aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/README.md +5 -0
- aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/content/text.txt +1 -0
- aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/cos/minimal.pdf +28 -0
- aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/cos/truncated.pdf +3 -0
- aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/filters/asciihex.txt +1 -0
- aspose_pdf_foss_for_python-0.1.0/fuzz/corpus/tokenizer/dictionary.txt +1 -0
- aspose_pdf_foss_for_python-0.1.0/fuzz/run.py +29 -0
- aspose_pdf_foss_for_python-0.1.0/fuzz/targets.py +114 -0
- aspose_pdf_foss_for_python-0.1.0/pyproject.toml +112 -0
- aspose_pdf_foss_for_python-0.1.0/scripts/build_agl_data.py +152 -0
- aspose_pdf_foss_for_python-0.1.0/scripts/build_cmap_data.py +507 -0
- aspose_pdf_foss_for_python-0.1.0/setup.cfg +4 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/__init__.py +367 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/_compat_surface.py +142 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/_export_files.py +108 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/_pending_signatures.py +222 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/_version.py +17 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/actions.py +273 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/annotations/__init__.py +1614 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/annotations/pdf3d.py +70 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/attachments.py +62 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/auto_links.py +375 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/block_extraction.py +223 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/cgm.py +7 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/clustering/__init__.py +13 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/clustering/hierarchical_agglomerative_clustering.py +151 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/color.py +241 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/destinations.py +164 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/document.py +5884 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/document_fonts.py +150 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/drawing.py +13 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/__init__.py +1 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/agl.py +243 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/annotation_cleanup.py +407 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/annotation_import.py +503 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/annotation_json.py +313 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/annotation_xfdf.py +405 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/annotation_xfdf_import.py +389 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/appearance.py +1361 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/attachment_zip.py +306 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/attachment_zip_import.py +387 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/auto_crop.py +101 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/auto_tag.py +1451 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/blank_pages.py +198 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/blank_split.py +73 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/bookmark_json.py +714 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/bookmark_split.py +167 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/ccitt.py +457 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/ccitt_tables.py +231 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/cert_chain.py +306 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/cff_outlines.py +928 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/cms.py +562 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/comparison_html.py +435 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/conformance.py +2748 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/contact_sheet.py +215 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/content_authoring.py +1347 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/content_bounds.py +67 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/content_isolation.py +180 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/content_stream_parser.py +2318 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/cos.py +416 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/__init__.py +81 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/agl/LICENSE-ADOBE-AGL.txt +28 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/agl/LICENSE-REPORTLAB.txt +31 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/agl/README.md +44 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/agl/glyphlist.json.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/LICENSE-ADOBE-CMAP.txt +28 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/LICENSE-ADOBE-MAPPING.txt +28 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/README.md +51 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/predefined_cmaps_CNS1.json.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/predefined_cmaps_GB1.json.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/predefined_cmaps_Japan1.json.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/predefined_cmaps_Korea1.json.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/cmaps/predefined_cmaps_index.json.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/LICENSE-DEJAVU.txt +187 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/LICENSE-OFL.txt +102 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/README.md +47 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/dingbats.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/mono-bold.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/mono-bolditalic.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/mono-italic.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/mono-regular.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/sans-bold.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/sans-bolditalic.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/sans-italic.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/sans-regular.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/serif-bold.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/serif-bolditalic.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/serif-italic.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/serif-regular.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/fonts/symbol.ttf.zlib +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/number.py +78 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/types.py +156 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/data/xmp.py +1458 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/dct.py +854 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/dss.py +658 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/duplicate_pages.py +67 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/encryption.py +967 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/field_appearance.py +310 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/file_output.py +98 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/filters.py +958 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/flatten.py +388 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_authoring.py +609 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_inventory.py +211 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_resolver.py +829 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_subset.py +545 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_subset_cff.py +963 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_subset_type1.py +114 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/font_variations.py +159 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_batch.py +134 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_csv.py +267 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_csv_export.py +131 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_data.py +364 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_field_edit.py +306 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_fields.py +253 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_json.py +89 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_schema.py +246 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/form_validation.py +395 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/glyph_outlines.py +465 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/graphics_absorb.py +1061 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/grid_split.py +80 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/gvar.py +467 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/headings.py +273 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/image_export.py +875 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/image_import.py +219 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/image_resample.py +116 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/image_resolution.py +376 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/image_zip.py +339 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/imposition.py +233 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/incremental_update.py +477 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/inline_image.py +337 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/interleave.py +188 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/io.py +53 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/jbig2.py +186 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/jpeg2000.py +2124 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/jpeg_encoder.py +706 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/jpx.py +211 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/link_extraction.py +378 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/link_urls.py +252 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/link_validation.py +555 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/lists.py +307 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/logging.py +18 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/merge_bookmarks.py +180 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/optional_content.py +1323 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/page_count_split.py +60 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/page_labels.py +295 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/page_zip.py +219 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/parser_exceptions.py +125 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/pdf_matrix.py +90 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/pdf_parser_cos.py +1611 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/pdf_writer_cos.py +544 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/predefined_cmaps.py +635 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/primitives/__init__.py +5 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/primitives/matrix.py +31 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/pubsec.py +898 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/rasterized_copy.py +171 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/rasterizer.py +5483 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/redaction.py +140 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/revocation.py +495 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/rich_text.py +415 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/sanitize.py +412 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/sfnt.py +247 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/shading.py +2317 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/sign_field.py +689 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signature_appearance.py +302 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signature_profiles.py +206 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signature_revisions.py +476 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signature_validator.py +313 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signed_changes.py +208 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/signing.py +288 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/simple_pdf.py +21416 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/spread_split.py +330 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/stamps.py +414 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/std_font_data.py +201 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/std_fonts.py +97 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/std_metrics.py +364 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/stream_output.py +22 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/stroke.py +228 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/svg_export.py +1162 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/symbol_encodings.py +260 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/tables.py +420 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_blocks.py +467 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_comparison.py +146 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_edit.py +1936 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_export.py +758 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_faces.py +275 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_highlight.py +197 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_layout.py +574 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_locate.py +423 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_metrics.py +86 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_outlines.py +150 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_region.py +476 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_selection.py +53 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/text_split.py +141 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/tiff_import.py +130 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/timestamp.py +327 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/toc.py +407 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/type1_outlines.py +471 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/viewer_preferences.py +275 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/visual_comparison.py +318 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/webp_encoder.py +107 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/woff.py +302 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/woff2.py +674 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/engine/word_extraction.py +235 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/exceptions.py +148 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/facades.py +1569 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/font_registry.py +149 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/font_repository.py +311 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/font_substitution.py +115 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/forms.py +1688 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/generated/__init__.py +3 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/generated/document.py +14 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/generated/forms.py +12 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/generated/pdfa.py +12 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/generated/security.py +33 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/geometry.py +149 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/graphics.py +256 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/html.py +117 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/image_resolution.py +45 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/images.py +524 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/interactive.py +435 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/latex.py +14 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/layers.py +482 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/license_management.py +65 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/link_extraction.py +52 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/link_validation.py +29 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/lists.py +365 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/load_limits.py +173 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/load_options.py +63 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/logical_structure.py +34 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/lowcode.py +1598 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/markdown.py +38 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/ofd.py +7 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/optimization.py +51 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/outlines.py +271 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/page_labels.py +169 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/page_ranges.py +90 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/page_size.py +220 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/pages.py +1883 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/paths.py +306 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/pdf_consts.py +19 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/pdfa.py +285 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/pdfua.py +194 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/pdfx.py +267 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/permissions.py +340 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/presentation.py +170 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/primitives.py +22 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/printing.py +41 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/py.typed +0 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/recipients.py +78 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/sanitization.py +90 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/save_format.py +25 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/save_options.py +25 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/security/__init__.py +160 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/security/impl/__init__.py +1 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/security/impl/digests/__init__.py +1 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/signature.py +385 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/signature_appearance.py +263 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/stamps.py +563 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/svg.py +41 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/table_extraction.py +156 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/tables.py +272 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/tagged.py +254 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/text.py +762 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/text_block.py +286 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/text_comparison.py +31 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/text_layout.py +100 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/utils.py +74 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/validation.py +286 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/viewer_preferences.py +375 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/visual_comparison.py +53 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/visualization.py +118 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf/xmp.py +75 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf_foss_for_python.egg-info/PKG-INFO +2233 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf_foss_for_python.egg-info/SOURCES.txt +677 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf_foss_for_python.egg-info/dependency_links.txt +1 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf_foss_for_python.egg-info/requires.txt +30 -0
- aspose_pdf_foss_for_python-0.1.0/src/aspose_pdf_foss_for_python.egg-info/top_level.txt +1 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_acc01_font_embedding.py +142 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_add_image_png_variants.py +266 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_aes_key_derivation.py +291 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_agl.py +103 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_algorithm_2b_v5r6.py +140 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_appearance_decorations.py +148 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_appearance_generation.py +389 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_cleanup.py +655 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_icon_appearances.py +157 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_import.py +808 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_json.py +572 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_property_roundtrip.py +183 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_references.py +101 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_xfdf.py +609 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotation_xfdf_import.py +566 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_annotations.py +182 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_api_surface.py +24 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_appearance_subtypes_documented.py +140 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_appearance_text_encoding.py +127 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_artifacts.py +649 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_associated_files.py +267 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_attachment_metadata.py +500 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_attachment_name_tree.py +169 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_attachment_zip.py +626 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_attachment_zip_import.py +810 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_attachments.py +181 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_10_trailer_nested_dict.py +60 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_11_tokenizer_null.py +35 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_12_lazy_object_parse_errors.py +36 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_13_object_stream.py +124 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_16_lazy_encryption.py +61 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_17_exceptions.py +109 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_18_encrypted_exception_types.py +77 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_19_content_stream_text.py +104 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_20_content_stream_token_memory.py +107 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_21_fonts_cmap_tounicode.py +128 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_22_jbig2.py +56 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_23_jpx_ccitt.py +77 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_24_incremental_update.py +94 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_25_encryption_v_and_password.py +110 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_26_decrypt_lazy.py +70 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_27_incremental_byte_range.py +34 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_28_signatures_compromise_detector.py +86 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_29_pdfa_heuristic.py +61 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_30_pdfua_heuristic.py +142 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_33_mmap_dispose.py +68 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_34_dispose_close_consistency.py +171 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_35_float_precision.py +87 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_36_stream_boundaries.py +111 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_37_stream_decoder.py +67 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_38_attachments.py +159 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_39_outlines.py +223 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_40_annotations_appearance.py +107 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_41_generated_document.py +106 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_42_logging.py +143 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_5_lazy_cos.py +87 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_6_from_bytes.py +52 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_7_parse_exception_handling.py +86 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_audit_issue_9_xref_stream.py +126 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_authored_color_spaces.py +293 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_authoring_resource_limits.py +154 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_auto_crop.py +423 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_auto_links.py +585 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_auto_tag.py +732 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_auto_tag_limits.py +81 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_auto_tag_structure.py +500 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_blank_page_removal.py +384 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_blank_pages.py +433 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_blank_split.py +670 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_block_extraction.py +435 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_bookmark_json.py +945 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_bookmark_split.py +464 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_ccitt_decoder.py +125 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_ccitt_fidelity.py +404 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cff2_variable.py +172 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cff_predefined_encoding.py +99 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cff_rasterization.py +512 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cjk_cmap_rendering.py +356 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cmap_build_data.py +65 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cmap_resource_limits.py +161 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cmap_syntax.py +249 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_color_space_conversion.py +359 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_comparison_html.py +455 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_compatibility_surfaces.py +333 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_complex_text_layout.py +443 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_contact_sheet.py +564 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_content_append_isolation.py +390 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_content_authoring.py +175 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_content_bounds.py +523 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_conversion_leaves_nothing_behind.py +217 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cos.py +77 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cos_comments.py +110 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cos_preservation.py +73 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cos_tree_resource_limits.py +316 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_crop_box_intersection.py +122 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cross_validation.py +446 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_curve_fidelity.py +315 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_cyclic_resources.py +187 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_damaged_structures.py +277 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_dct.py +378 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_document_fonts.py +562 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_document_lifecycle.py +266 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_document_metadata.py +304 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_document_signing.py +635 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_document_with_no_pages.py +158 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_duplicate_pages.py +538 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_embed_fonts.py +615 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_embedded_file_mime.py +90 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_encrypted_cos_writer.py +636 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_encrypted_pdf_validation.py +97 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_encryption_aes.py +53 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_encryption_task7.py +19 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_engine_v0.py +43 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_engine_v1.py +88 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_error_handling.py +126 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_export_save_options.py +345 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_extractor_facade.py +167 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_facades.py +601 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_field_alignment.py +398 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_field_appearance_and_actions.py +483 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_field_appearance_generation.py +1084 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_field_font_glyphs.py +340 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_field_type0_font.py +148 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_field_value_appearance.py +236 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_file_editor_insert.py +219 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_file_editor_io.py +292 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_file_header.py +242 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_file_output.py +234 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_fill_rules.py +290 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_filters.py +339 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_filters_decoding.py +112 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_filters_encoding.py +163 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_flattening_task5.py +18 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_font_authoring_formats.py +288 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_font_source.py +374 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_font_subset.py +744 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_font_subset_cff.py +700 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_font_subset_cff2.py +591 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_font_subset_type1.py +243 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_font_substitution.py +599 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_batch.py +474 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_creation.py +353 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_csv.py +381 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_csv_export.py +347 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_data.py +467 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_field_editing.py +564 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_field_lifecycle.py +141 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_field_properties.py +380 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_field_types.py +143 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_font_resources.py +88 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_json.py +371 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_reset.py +349 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_schema.py +412 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_validation.py +741 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_form_xobject_text.py +439 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_forms.py +164 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_forms_icons_and_submit.py +223 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_fuzz_corpus.py +26 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_generate_outlines.py +441 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_generated_appearance_rendering.py +260 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_generated_reexports.py +75 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_glyph_rasterization.py +382 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_graphics_absorber.py +534 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_grid_split.py +498 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_gvar_variable_truetype.py +544 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_hybrid_reference_files.py +161 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_codec_limits.py +133 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_dimensions.py +59 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_export.py +463 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_extraction.py +139 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_masking.py +360 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_optimize_coverage.py +543 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_placement_absorber.py +208 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_placement_editing.py +409 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_recompress.py +455 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_resample.py +60 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_resolution.py +568 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_image_zip.py +699 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_incremental_object_numbers.py +162 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_incremental_save.py +289 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_incremental_update.py +162 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_inert_surfaces.py +232 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_info_values.py +401 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_inline_images.py +416 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_integration_final.py +43 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_interactive_actions.py +148 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_interleave.py +806 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_jbig2_decoder.py +117 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_jpeg2000_decoder.py +624 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_jpeg2000_packed_headers.py +287 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_jpeg_encoder.py +249 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_jpx_task8.py +21 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_layer_authoring.py +546 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_layer_configurations.py +599 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_layer_radio_groups.py +189 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_lazy_images.py +115 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_legacy_writer_surface.py +123 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_link_destinations.py +446 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_link_extraction.py +660 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_link_urls.py +589 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_link_validation.py +811 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_lists.py +711 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_literal_strings.py +177 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_load_from_stream.py +294 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_load_limits.py +543 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_logging_task10.py +18 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_lowcode.py +444 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_lowcode_catalogue.py +541 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_lowcode_resource_limits.py +167 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_lowcode_static_image_import.py +371 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_lowcode_tiff_import.py +421 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_macroman_encoding.py +422 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_matrix_composition.py +221 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_media_box_fallback.py +148 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_merge.py +466 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_merge_bookmarks.py +401 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_merged_structure_keys.py +246 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_merged_structure_tree.py +398 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_merging_task4.py +104 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_metadata.py +73 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_named_destinations.py +509 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_names.py +183 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_numbers.py +307 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_object_generations.py +319 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_opt_01_gc.py +81 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_optimization_options.py +551 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_optimization_task6.py +21 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_optional_content.py +322 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_outline_named_destinations.py +187 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_outline_targets.py +438 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_owner_password_long_user.py +157 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_p0_bugfixes.py +146 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_package_metadata.py +74 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_count_split.py +654 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_extraction.py +337 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_import.py +361 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_import_references.py +169 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_labels.py +409 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_number_labels.py +267 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_ranges.py +191 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_rendering.py +251 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_resize.py +513 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_rotation_cropbox.py +112 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_size_and_boxes.py +524 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_stamp_and_imposition.py +461 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_subset_adoption.py +213 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_transparency.py +198 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_page_zip.py +561 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pages.py +592 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_password.py +67 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_path_authoring.py +632 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pattern_paint.py +303 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pattern_space.py +293 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdf_cos_parser_stream_sources.py +89 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdf_extractor_bind_password.py +59 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdf_file_editor_last_exception.py +54 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdf_header_position.py +133 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa.py +55 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa4_info.py +195 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa4_pdfua2.py +631 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_cmyk_images.py +206 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_cmyk_normalize.py +145 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_conformance_extended.py +579 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_conversion.py +495 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_embed_fonts.py +355 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_output_intent_match.py +201 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_separation_devicen.py +340 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_task9.py +21 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_transparency_groups.py +210 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfa_validation_result.py +386 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfua2_document_root.py +153 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfua_conformance.py +434 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfua_mcid_coverage.py +152 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pdfx_conformance.py +1428 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_permissions.py +498 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_png_transparency.py +165 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_predefined_cjk_cmaps.py +769 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_public_key_encryption.py +866 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_pushbutton_appearance.py +70 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_raster_output_formats.py +351 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_rasterized_copy.py +642 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_rasterized_copy_modes.py +360 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_rc4_external.py +57 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_redaction_overlay.py +136 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_redaction_saving.py +403 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_refactored_simple_pdf.py +64 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_render_annotations.py +251 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_render_clip.py +359 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_repair_task2.py +30 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_resource_limits_ccitt_jbig2.py +89 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_revision_chain.py +337 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_rich_text.py +235 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_rounded_rect.py +203 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_sanitize.py +572 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_save_stability.py +232 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_save_to_stream.py +203 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_scan_conversion.py +200 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_security.py +36 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_selective_flatten.py +784 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_shading.py +894 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_shading_resource_limits.py +255 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_sign_authored_field.py +499 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_appearance.py +788 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_certification.py +71 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_chain_validation.py +106 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_field_authoring.py +149 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_pades.py +551 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_revision_policy.py +350 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_revocation.py +139 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_seed_value.py +601 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_timestamp.py +65 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_trust_boundaries.py +715 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_validation.py +459 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signature_verification.py +138 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signatures_through_document.py +244 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signing.py +35 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_signing_e2e.py +36 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_smoke_imports.py +22 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_soft_mask_rendering.py +364 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_spread_split.py +554 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_stamps.py +643 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_standard14_rendering.py +341 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_standard_font_text.py +185 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_standard_security_handler.py +363 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_std_fonts.py +40 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_stencil_masks.py +474 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_stream_cmap_render.py +218 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_stream_lengths.py +127 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_streaming.py +561 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_streaming_parity.py +255 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_stroke_rendering.py +259 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_structure_validation.py +236 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_subset_drops_unused_tables.py +119 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_svg_compositing.py +560 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_svg_export.py +408 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_svg_selectable_text.py +344 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_table_extraction.py +458 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_table_of_contents.py +434 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_tables.py +594 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_tagged_remediation.py +180 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_artifacts.py +513 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_block_pagination.py +358 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_blocks.py +770 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_comparison.py +535 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_cross_operator.py +192 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_cross_position.py +181 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_limits.py +125 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_multi_tj.py +126 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_regex.py +494 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_type0.py +246 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_type0_encodings.py +150 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_edit_type0_reconstruct.py +356 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_editing.py +119 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_exclude_regions.py +533 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_export.py +400 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_export_resources.py +345 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_extraction.py +82 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_extraction_task3.py +18 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_fragment_absorber.py +493 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_fragment_geometry.py +249 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_highlight.py +515 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_lines.py +245 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_outlines.py +566 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_page_selection.py +466 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_region.py +476 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_render_modes.py +313 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_reshape_edit.py +360 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_run_separation.py +200 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_split.py +565 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_strings.py +212 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_text_word_gaps.py +146 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_tiling_pattern.py +121 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_truncated_stream_recovery.py +124 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_txt01_fallback.py +43 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_type1_rasterization.py +351 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_type3_fonts.py +214 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_typed_annotations.py +513 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_unicode_font_authoring.py +428 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_validation_task1.py +32 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_validator_bytearray_limits.py +220 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_verify_aes_generation.py +47 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_viewer_preferences.py +443 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_visual_comparison.py +658 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_visual_ignore_regions.py +585 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_webp_output.py +329 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_whitespace_extraction.py +229 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_woff.py +303 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_woff2.py +233 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_woff2_collections.py +150 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_word_extraction.py +615 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_wrapper_attributes.py +134 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_xmp_info_sync.py +418 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_xmp_namespace.py +174 -0
- aspose_pdf_foss_for_python-0.1.0/tests/test_xmp_packet.py +924 -0
- 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
|