aspose-pdf-foss-for-python 0.1.0__py3-none-any.whl
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/__init__.py +367 -0
- aspose_pdf/_compat_surface.py +142 -0
- aspose_pdf/_export_files.py +108 -0
- aspose_pdf/_pending_signatures.py +222 -0
- aspose_pdf/_version.py +17 -0
- aspose_pdf/actions.py +273 -0
- aspose_pdf/annotations/__init__.py +1614 -0
- aspose_pdf/annotations/pdf3d.py +70 -0
- aspose_pdf/attachments.py +62 -0
- aspose_pdf/auto_links.py +375 -0
- aspose_pdf/block_extraction.py +223 -0
- aspose_pdf/cgm.py +7 -0
- aspose_pdf/clustering/__init__.py +13 -0
- aspose_pdf/clustering/hierarchical_agglomerative_clustering.py +151 -0
- aspose_pdf/color.py +241 -0
- aspose_pdf/destinations.py +164 -0
- aspose_pdf/document.py +5884 -0
- aspose_pdf/document_fonts.py +150 -0
- aspose_pdf/drawing.py +13 -0
- aspose_pdf/engine/__init__.py +1 -0
- aspose_pdf/engine/agl.py +243 -0
- aspose_pdf/engine/annotation_cleanup.py +407 -0
- aspose_pdf/engine/annotation_import.py +503 -0
- aspose_pdf/engine/annotation_json.py +313 -0
- aspose_pdf/engine/annotation_xfdf.py +405 -0
- aspose_pdf/engine/annotation_xfdf_import.py +389 -0
- aspose_pdf/engine/appearance.py +1361 -0
- aspose_pdf/engine/attachment_zip.py +306 -0
- aspose_pdf/engine/attachment_zip_import.py +387 -0
- aspose_pdf/engine/auto_crop.py +101 -0
- aspose_pdf/engine/auto_tag.py +1451 -0
- aspose_pdf/engine/blank_pages.py +198 -0
- aspose_pdf/engine/blank_split.py +73 -0
- aspose_pdf/engine/bookmark_json.py +714 -0
- aspose_pdf/engine/bookmark_split.py +167 -0
- aspose_pdf/engine/ccitt.py +457 -0
- aspose_pdf/engine/ccitt_tables.py +231 -0
- aspose_pdf/engine/cert_chain.py +306 -0
- aspose_pdf/engine/cff_outlines.py +928 -0
- aspose_pdf/engine/cms.py +562 -0
- aspose_pdf/engine/comparison_html.py +435 -0
- aspose_pdf/engine/conformance.py +2748 -0
- aspose_pdf/engine/contact_sheet.py +215 -0
- aspose_pdf/engine/content_authoring.py +1347 -0
- aspose_pdf/engine/content_bounds.py +67 -0
- aspose_pdf/engine/content_isolation.py +180 -0
- aspose_pdf/engine/content_stream_parser.py +2318 -0
- aspose_pdf/engine/cos.py +416 -0
- aspose_pdf/engine/data/__init__.py +81 -0
- aspose_pdf/engine/data/agl/LICENSE-ADOBE-AGL.txt +28 -0
- aspose_pdf/engine/data/agl/LICENSE-REPORTLAB.txt +31 -0
- aspose_pdf/engine/data/agl/README.md +44 -0
- aspose_pdf/engine/data/agl/glyphlist.json.zlib +0 -0
- aspose_pdf/engine/data/cmaps/LICENSE-ADOBE-CMAP.txt +28 -0
- aspose_pdf/engine/data/cmaps/LICENSE-ADOBE-MAPPING.txt +28 -0
- aspose_pdf/engine/data/cmaps/README.md +51 -0
- aspose_pdf/engine/data/cmaps/predefined_cmaps_CNS1.json.zlib +0 -0
- aspose_pdf/engine/data/cmaps/predefined_cmaps_GB1.json.zlib +0 -0
- aspose_pdf/engine/data/cmaps/predefined_cmaps_Japan1.json.zlib +0 -0
- aspose_pdf/engine/data/cmaps/predefined_cmaps_Korea1.json.zlib +0 -0
- aspose_pdf/engine/data/cmaps/predefined_cmaps_index.json.zlib +0 -0
- aspose_pdf/engine/data/fonts/LICENSE-DEJAVU.txt +187 -0
- aspose_pdf/engine/data/fonts/LICENSE-OFL.txt +102 -0
- aspose_pdf/engine/data/fonts/README.md +47 -0
- aspose_pdf/engine/data/fonts/dingbats.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/mono-bold.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/mono-bolditalic.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/mono-italic.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/mono-regular.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/sans-bold.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/sans-bolditalic.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/sans-italic.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/sans-regular.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/serif-bold.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/serif-bolditalic.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/serif-italic.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/serif-regular.ttf.zlib +0 -0
- aspose_pdf/engine/data/fonts/symbol.ttf.zlib +0 -0
- aspose_pdf/engine/data/number.py +78 -0
- aspose_pdf/engine/data/types.py +156 -0
- aspose_pdf/engine/data/xmp.py +1458 -0
- aspose_pdf/engine/dct.py +854 -0
- aspose_pdf/engine/dss.py +658 -0
- aspose_pdf/engine/duplicate_pages.py +67 -0
- aspose_pdf/engine/encryption.py +967 -0
- aspose_pdf/engine/field_appearance.py +310 -0
- aspose_pdf/engine/file_output.py +98 -0
- aspose_pdf/engine/filters.py +958 -0
- aspose_pdf/engine/flatten.py +388 -0
- aspose_pdf/engine/font_authoring.py +609 -0
- aspose_pdf/engine/font_inventory.py +211 -0
- aspose_pdf/engine/font_resolver.py +829 -0
- aspose_pdf/engine/font_subset.py +545 -0
- aspose_pdf/engine/font_subset_cff.py +963 -0
- aspose_pdf/engine/font_subset_type1.py +114 -0
- aspose_pdf/engine/font_variations.py +159 -0
- aspose_pdf/engine/form_batch.py +134 -0
- aspose_pdf/engine/form_csv.py +267 -0
- aspose_pdf/engine/form_csv_export.py +131 -0
- aspose_pdf/engine/form_data.py +364 -0
- aspose_pdf/engine/form_field_edit.py +306 -0
- aspose_pdf/engine/form_fields.py +253 -0
- aspose_pdf/engine/form_json.py +89 -0
- aspose_pdf/engine/form_schema.py +246 -0
- aspose_pdf/engine/form_validation.py +395 -0
- aspose_pdf/engine/glyph_outlines.py +465 -0
- aspose_pdf/engine/graphics_absorb.py +1061 -0
- aspose_pdf/engine/grid_split.py +80 -0
- aspose_pdf/engine/gvar.py +467 -0
- aspose_pdf/engine/headings.py +273 -0
- aspose_pdf/engine/image_export.py +875 -0
- aspose_pdf/engine/image_import.py +219 -0
- aspose_pdf/engine/image_resample.py +116 -0
- aspose_pdf/engine/image_resolution.py +376 -0
- aspose_pdf/engine/image_zip.py +339 -0
- aspose_pdf/engine/imposition.py +233 -0
- aspose_pdf/engine/incremental_update.py +477 -0
- aspose_pdf/engine/inline_image.py +337 -0
- aspose_pdf/engine/interleave.py +188 -0
- aspose_pdf/engine/io.py +53 -0
- aspose_pdf/engine/jbig2.py +186 -0
- aspose_pdf/engine/jpeg2000.py +2124 -0
- aspose_pdf/engine/jpeg_encoder.py +706 -0
- aspose_pdf/engine/jpx.py +211 -0
- aspose_pdf/engine/link_extraction.py +378 -0
- aspose_pdf/engine/link_urls.py +252 -0
- aspose_pdf/engine/link_validation.py +555 -0
- aspose_pdf/engine/lists.py +307 -0
- aspose_pdf/engine/logging.py +18 -0
- aspose_pdf/engine/merge_bookmarks.py +180 -0
- aspose_pdf/engine/optional_content.py +1323 -0
- aspose_pdf/engine/page_count_split.py +60 -0
- aspose_pdf/engine/page_labels.py +295 -0
- aspose_pdf/engine/page_zip.py +219 -0
- aspose_pdf/engine/parser_exceptions.py +125 -0
- aspose_pdf/engine/pdf_matrix.py +90 -0
- aspose_pdf/engine/pdf_parser_cos.py +1611 -0
- aspose_pdf/engine/pdf_writer_cos.py +544 -0
- aspose_pdf/engine/predefined_cmaps.py +635 -0
- aspose_pdf/engine/primitives/__init__.py +5 -0
- aspose_pdf/engine/primitives/matrix.py +31 -0
- aspose_pdf/engine/pubsec.py +898 -0
- aspose_pdf/engine/rasterized_copy.py +171 -0
- aspose_pdf/engine/rasterizer.py +5483 -0
- aspose_pdf/engine/redaction.py +140 -0
- aspose_pdf/engine/revocation.py +495 -0
- aspose_pdf/engine/rich_text.py +415 -0
- aspose_pdf/engine/sanitize.py +412 -0
- aspose_pdf/engine/sfnt.py +247 -0
- aspose_pdf/engine/shading.py +2317 -0
- aspose_pdf/engine/sign_field.py +689 -0
- aspose_pdf/engine/signature_appearance.py +302 -0
- aspose_pdf/engine/signature_profiles.py +206 -0
- aspose_pdf/engine/signature_revisions.py +476 -0
- aspose_pdf/engine/signature_validator.py +313 -0
- aspose_pdf/engine/signed_changes.py +208 -0
- aspose_pdf/engine/signing.py +288 -0
- aspose_pdf/engine/simple_pdf.py +21416 -0
- aspose_pdf/engine/spread_split.py +330 -0
- aspose_pdf/engine/stamps.py +414 -0
- aspose_pdf/engine/std_font_data.py +201 -0
- aspose_pdf/engine/std_fonts.py +97 -0
- aspose_pdf/engine/std_metrics.py +364 -0
- aspose_pdf/engine/stream_output.py +22 -0
- aspose_pdf/engine/stroke.py +228 -0
- aspose_pdf/engine/svg_export.py +1162 -0
- aspose_pdf/engine/symbol_encodings.py +260 -0
- aspose_pdf/engine/tables.py +420 -0
- aspose_pdf/engine/text_blocks.py +467 -0
- aspose_pdf/engine/text_comparison.py +146 -0
- aspose_pdf/engine/text_edit.py +1936 -0
- aspose_pdf/engine/text_export.py +758 -0
- aspose_pdf/engine/text_faces.py +275 -0
- aspose_pdf/engine/text_highlight.py +197 -0
- aspose_pdf/engine/text_layout.py +574 -0
- aspose_pdf/engine/text_locate.py +423 -0
- aspose_pdf/engine/text_metrics.py +86 -0
- aspose_pdf/engine/text_outlines.py +150 -0
- aspose_pdf/engine/text_region.py +476 -0
- aspose_pdf/engine/text_selection.py +53 -0
- aspose_pdf/engine/text_split.py +141 -0
- aspose_pdf/engine/tiff_import.py +130 -0
- aspose_pdf/engine/timestamp.py +327 -0
- aspose_pdf/engine/toc.py +407 -0
- aspose_pdf/engine/type1_outlines.py +471 -0
- aspose_pdf/engine/viewer_preferences.py +275 -0
- aspose_pdf/engine/visual_comparison.py +318 -0
- aspose_pdf/engine/webp_encoder.py +107 -0
- aspose_pdf/engine/woff.py +302 -0
- aspose_pdf/engine/woff2.py +674 -0
- aspose_pdf/engine/word_extraction.py +235 -0
- aspose_pdf/exceptions.py +148 -0
- aspose_pdf/facades.py +1569 -0
- aspose_pdf/font_registry.py +149 -0
- aspose_pdf/font_repository.py +311 -0
- aspose_pdf/font_substitution.py +115 -0
- aspose_pdf/forms.py +1688 -0
- aspose_pdf/generated/__init__.py +3 -0
- aspose_pdf/generated/document.py +14 -0
- aspose_pdf/generated/forms.py +12 -0
- aspose_pdf/generated/pdfa.py +12 -0
- aspose_pdf/generated/security.py +33 -0
- aspose_pdf/geometry.py +149 -0
- aspose_pdf/graphics.py +256 -0
- aspose_pdf/html.py +117 -0
- aspose_pdf/image_resolution.py +45 -0
- aspose_pdf/images.py +524 -0
- aspose_pdf/interactive.py +435 -0
- aspose_pdf/latex.py +14 -0
- aspose_pdf/layers.py +482 -0
- aspose_pdf/license_management.py +65 -0
- aspose_pdf/link_extraction.py +52 -0
- aspose_pdf/link_validation.py +29 -0
- aspose_pdf/lists.py +365 -0
- aspose_pdf/load_limits.py +173 -0
- aspose_pdf/load_options.py +63 -0
- aspose_pdf/logical_structure.py +34 -0
- aspose_pdf/lowcode.py +1598 -0
- aspose_pdf/markdown.py +38 -0
- aspose_pdf/ofd.py +7 -0
- aspose_pdf/optimization.py +51 -0
- aspose_pdf/outlines.py +271 -0
- aspose_pdf/page_labels.py +169 -0
- aspose_pdf/page_ranges.py +90 -0
- aspose_pdf/page_size.py +220 -0
- aspose_pdf/pages.py +1883 -0
- aspose_pdf/paths.py +306 -0
- aspose_pdf/pdf_consts.py +19 -0
- aspose_pdf/pdfa.py +285 -0
- aspose_pdf/pdfua.py +194 -0
- aspose_pdf/pdfx.py +267 -0
- aspose_pdf/permissions.py +340 -0
- aspose_pdf/presentation.py +170 -0
- aspose_pdf/primitives.py +22 -0
- aspose_pdf/printing.py +41 -0
- aspose_pdf/py.typed +0 -0
- aspose_pdf/recipients.py +78 -0
- aspose_pdf/sanitization.py +90 -0
- aspose_pdf/save_format.py +25 -0
- aspose_pdf/save_options.py +25 -0
- aspose_pdf/security/__init__.py +160 -0
- aspose_pdf/security/impl/__init__.py +1 -0
- aspose_pdf/security/impl/digests/__init__.py +1 -0
- aspose_pdf/signature.py +385 -0
- aspose_pdf/signature_appearance.py +263 -0
- aspose_pdf/stamps.py +563 -0
- aspose_pdf/svg.py +41 -0
- aspose_pdf/table_extraction.py +156 -0
- aspose_pdf/tables.py +272 -0
- aspose_pdf/tagged.py +254 -0
- aspose_pdf/text.py +762 -0
- aspose_pdf/text_block.py +286 -0
- aspose_pdf/text_comparison.py +31 -0
- aspose_pdf/text_layout.py +100 -0
- aspose_pdf/utils.py +74 -0
- aspose_pdf/validation.py +286 -0
- aspose_pdf/viewer_preferences.py +375 -0
- aspose_pdf/visual_comparison.py +53 -0
- aspose_pdf/visualization.py +118 -0
- aspose_pdf/xmp.py +75 -0
- aspose_pdf_foss_for_python-0.1.0.dist-info/METADATA +2233 -0
- aspose_pdf_foss_for_python-0.1.0.dist-info/RECORD +265 -0
- aspose_pdf_foss_for_python-0.1.0.dist-info/WHEEL +5 -0
- aspose_pdf_foss_for_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- aspose_pdf_foss_for_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Selective appearance baking without discarding other interactive objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from aspose_pdf.exceptions import PdfParseException, PdfValidationException
|
|
11
|
+
|
|
12
|
+
from .content_isolation import isolation_for
|
|
13
|
+
from .cos import (
|
|
14
|
+
PdfArray,
|
|
15
|
+
PdfDictionary,
|
|
16
|
+
PdfIndirectReference,
|
|
17
|
+
PdfName,
|
|
18
|
+
PdfNumber,
|
|
19
|
+
PdfStream,
|
|
20
|
+
PdfString,
|
|
21
|
+
decode_pdf_text_string,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_ATTRIBUTES = ("FT", "Ff", "V", "DA", "Q", "RV", "Opt", "MaxLen")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _selection(
|
|
28
|
+
values: Any,
|
|
29
|
+
*,
|
|
30
|
+
names: bool,
|
|
31
|
+
budget: Any,
|
|
32
|
+
page_count: int | None = None,
|
|
33
|
+
) -> set:
|
|
34
|
+
label = "fields" if names else "pages"
|
|
35
|
+
if isinstance(values, (str, bytes, bytearray, Mapping)) or not isinstance(
|
|
36
|
+
values, Iterable
|
|
37
|
+
):
|
|
38
|
+
raise TypeError(f"{label} must be an iterable")
|
|
39
|
+
result = set()
|
|
40
|
+
for count, value in enumerate(values, 1):
|
|
41
|
+
budget.check(count, "max_container_items", f"flatten {label}")
|
|
42
|
+
if names:
|
|
43
|
+
if not isinstance(value, str):
|
|
44
|
+
raise TypeError("field names must be strings")
|
|
45
|
+
else:
|
|
46
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
47
|
+
raise TypeError("page indices must be integers")
|
|
48
|
+
if value < 0 or (page_count is not None and value >= page_count):
|
|
49
|
+
raise IndexError(f"Page index out of range: {value}")
|
|
50
|
+
result.add(value)
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class _Node:
|
|
56
|
+
entry: PdfDictionary
|
|
57
|
+
attributes: dict
|
|
58
|
+
children: list
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _FieldTree:
|
|
62
|
+
"""Index widgets once and prune only branches emptied by successful baking."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, pdf: Any):
|
|
65
|
+
self.pdf = pdf
|
|
66
|
+
self.nodes: list[_Node] = []
|
|
67
|
+
self.names: dict[str, int] = {}
|
|
68
|
+
self.owners: dict[int, int | None] = {}
|
|
69
|
+
self.by_id: dict[int, _Node] = {}
|
|
70
|
+
root = pdf._resolve(pdf._cos_doc.trailer.mapping.get(PdfName("Root")))
|
|
71
|
+
self.root = root if isinstance(root, PdfDictionary) else None
|
|
72
|
+
acro = (
|
|
73
|
+
pdf._resolve(root.mapping.get(PdfName("AcroForm"))) if self.root else None
|
|
74
|
+
)
|
|
75
|
+
self.acro = acro if isinstance(acro, PdfDictionary) else None
|
|
76
|
+
self.roots = (
|
|
77
|
+
self._array(acro.mapping.get(PdfName("Fields"))) if self.acro else []
|
|
78
|
+
)
|
|
79
|
+
# Resolve calculation-order entries before any page is edited, so
|
|
80
|
+
# pruning committed widgets cannot discover a late parser/budget error.
|
|
81
|
+
co = pdf._resolve(acro.mapping.get(PdfName("CO"))) if self.acro else None
|
|
82
|
+
self.calculation_order = (
|
|
83
|
+
[(ref, id(pdf._resolve(ref))) for ref in self._array(co)]
|
|
84
|
+
if isinstance(co, PdfArray)
|
|
85
|
+
else None
|
|
86
|
+
)
|
|
87
|
+
inherited = (
|
|
88
|
+
{key: acro.mapping.get(PdfName(key)) for key in ("DA", "Q")}
|
|
89
|
+
if self.acro
|
|
90
|
+
else {}
|
|
91
|
+
)
|
|
92
|
+
stack = [(ref, "", inherited, None, 1) for ref in reversed(self.roots)]
|
|
93
|
+
nonterminal = set()
|
|
94
|
+
candidates = []
|
|
95
|
+
while stack:
|
|
96
|
+
reference, prefix, attributes, owner, depth = stack.pop()
|
|
97
|
+
pdf._load_budget.check(depth, "max_nesting_depth", "flatten field depth")
|
|
98
|
+
entry = pdf._resolve(reference)
|
|
99
|
+
if not isinstance(entry, PdfDictionary) or isinstance(entry, PdfStream):
|
|
100
|
+
raise PdfParseException("Invalid form field dictionary")
|
|
101
|
+
key = id(entry)
|
|
102
|
+
if key in self.by_id:
|
|
103
|
+
raise PdfParseException(
|
|
104
|
+
"Form field tree contains a cycle or repeated field"
|
|
105
|
+
)
|
|
106
|
+
pdf._load_budget.check(
|
|
107
|
+
len(self.nodes) + 1, "max_container_items", "flatten field nodes"
|
|
108
|
+
)
|
|
109
|
+
attributes = dict(attributes)
|
|
110
|
+
for attr in _ATTRIBUTES:
|
|
111
|
+
if PdfName(attr) in entry.mapping:
|
|
112
|
+
attributes[attr] = entry.mapping[PdfName(attr)]
|
|
113
|
+
title = pdf._resolve(entry.mapping.get(PdfName("T")))
|
|
114
|
+
name = prefix
|
|
115
|
+
if isinstance(title, PdfString):
|
|
116
|
+
pdf._load_budget.check(
|
|
117
|
+
len(title.value), "max_object_bytes", "flatten field name"
|
|
118
|
+
)
|
|
119
|
+
partial = decode_pdf_text_string(title)
|
|
120
|
+
name = f"{prefix}.{partial}" if prefix else partial
|
|
121
|
+
pdf._load_budget.check(
|
|
122
|
+
len(name), "max_object_bytes", "flatten field name"
|
|
123
|
+
)
|
|
124
|
+
if owner is not None:
|
|
125
|
+
nonterminal.add(owner)
|
|
126
|
+
owner = key
|
|
127
|
+
candidates.append((name, key))
|
|
128
|
+
children = self._array(entry.mapping.get(PdfName("Kids")))
|
|
129
|
+
node = _Node(entry, attributes, children)
|
|
130
|
+
self.nodes.append(node)
|
|
131
|
+
self.by_id[key] = node
|
|
132
|
+
if pdf._resolve(entry.mapping.get(PdfName("Subtype"))) == PdfName("Widget"):
|
|
133
|
+
self.owners[key] = owner
|
|
134
|
+
pdf._load_budget.check(
|
|
135
|
+
len(stack) + len(children), "max_container_items", "flatten field stack"
|
|
136
|
+
)
|
|
137
|
+
stack.extend(
|
|
138
|
+
(ref, name, attributes, owner, depth + 1) for ref in reversed(children)
|
|
139
|
+
)
|
|
140
|
+
for name, key in candidates:
|
|
141
|
+
if key in nonterminal:
|
|
142
|
+
continue
|
|
143
|
+
if name in self.names:
|
|
144
|
+
raise PdfParseException(
|
|
145
|
+
"Form contains duplicate fully qualified field names"
|
|
146
|
+
)
|
|
147
|
+
self.names[name] = key
|
|
148
|
+
|
|
149
|
+
def _array(self, value: Any) -> list:
|
|
150
|
+
value = self.pdf._resolve(value)
|
|
151
|
+
if not isinstance(value, PdfArray):
|
|
152
|
+
return []
|
|
153
|
+
self.pdf._load_budget.check(
|
|
154
|
+
len(value.items), "max_container_items", "flatten field array"
|
|
155
|
+
)
|
|
156
|
+
return value.items
|
|
157
|
+
|
|
158
|
+
def generate(self, widget: PdfDictionary) -> None:
|
|
159
|
+
node = self.by_id.get(id(widget))
|
|
160
|
+
if node is None or self.acro is None:
|
|
161
|
+
return
|
|
162
|
+
pdf, attrs = self.pdf, node.attributes
|
|
163
|
+
|
|
164
|
+
def number(key: str, default: int = 0) -> int:
|
|
165
|
+
value = pdf._resolve(attrs.get(key))
|
|
166
|
+
if isinstance(value, PdfNumber) and math.isfinite(value.value):
|
|
167
|
+
return int(value.value)
|
|
168
|
+
return default
|
|
169
|
+
|
|
170
|
+
ft = pdf._resolve(attrs.get("FT"))
|
|
171
|
+
pdf._set_widget_appearance(
|
|
172
|
+
widget,
|
|
173
|
+
ft.name.lstrip("/") if isinstance(ft, PdfName) else None,
|
|
174
|
+
number("Ff"),
|
|
175
|
+
number("Q"),
|
|
176
|
+
attrs.get("V"),
|
|
177
|
+
pdf._get_cos_string(pdf._resolve(attrs.get("DA"))),
|
|
178
|
+
self.acro,
|
|
179
|
+
attrs.get("Opt"),
|
|
180
|
+
attrs.get("RV"),
|
|
181
|
+
keep_existing=not pdf._need_appearances_requested(),
|
|
182
|
+
max_length=number("MaxLen") or None,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def prune(self, baked: set[int]) -> None:
|
|
186
|
+
if not baked or self.acro is None:
|
|
187
|
+
return
|
|
188
|
+
removed = set(baked)
|
|
189
|
+
for node in reversed(self.nodes):
|
|
190
|
+
if not node.children:
|
|
191
|
+
continue
|
|
192
|
+
kept = [
|
|
193
|
+
ref
|
|
194
|
+
for ref in node.children
|
|
195
|
+
if id(self.pdf._resolve(ref)) not in removed
|
|
196
|
+
]
|
|
197
|
+
if len(kept) == len(node.children):
|
|
198
|
+
continue
|
|
199
|
+
if kept:
|
|
200
|
+
node.entry.mapping[PdfName("Kids")] = PdfArray(kept)
|
|
201
|
+
else:
|
|
202
|
+
removed.add(id(node.entry))
|
|
203
|
+
roots = [ref for ref in self.roots if id(self.pdf._resolve(ref)) not in removed]
|
|
204
|
+
self.acro.mapping[PdfName("Fields")] = PdfArray(roots)
|
|
205
|
+
if self.calculation_order is not None:
|
|
206
|
+
self.acro.mapping[PdfName("CO")] = PdfArray(
|
|
207
|
+
[ref for ref, key in self.calculation_order if key not in removed]
|
|
208
|
+
)
|
|
209
|
+
if not roots and PdfName("XFA") not in self.acro.mapping:
|
|
210
|
+
self.root.mapping.pop(PdfName("AcroForm"), None)
|
|
211
|
+
self.pdf._cos_doc.trailer.mapping.pop(PdfName("AcroForm"), None)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _appearance(pdf: Any, annot: PdfDictionary) -> tuple[Any, PdfStream] | None:
|
|
215
|
+
ap = pdf._resolve(annot.mapping.get(PdfName("AP")))
|
|
216
|
+
if not isinstance(ap, PdfDictionary):
|
|
217
|
+
return None
|
|
218
|
+
reference = ap.mapping.get(PdfName("N"))
|
|
219
|
+
normal = pdf._resolve(reference)
|
|
220
|
+
if isinstance(normal, PdfDictionary) and not isinstance(normal, PdfStream):
|
|
221
|
+
state = pdf._resolve(annot.mapping.get(PdfName("AS")))
|
|
222
|
+
reference = normal.mapping.get(
|
|
223
|
+
state if isinstance(state, PdfName) else PdfName("Off")
|
|
224
|
+
)
|
|
225
|
+
normal = pdf._resolve(reference)
|
|
226
|
+
return (reference, normal) if isinstance(normal, PdfStream) else None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def flatten(
|
|
230
|
+
pdf: Any,
|
|
231
|
+
*,
|
|
232
|
+
fields: bool | Iterable[str] = True,
|
|
233
|
+
annotations: bool = True,
|
|
234
|
+
pages: Iterable[int] | None = None,
|
|
235
|
+
strict: bool = True,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Bake selected widgets/annotations; preserve anything without a usable AP."""
|
|
238
|
+
pdf._ensure_not_disposed()
|
|
239
|
+
if not isinstance(annotations, bool) or not isinstance(strict, bool):
|
|
240
|
+
raise TypeError("annotations and strict must be booleans")
|
|
241
|
+
selected = (
|
|
242
|
+
fields
|
|
243
|
+
if isinstance(fields, bool)
|
|
244
|
+
else _selection(fields, names=True, budget=pdf._load_budget)
|
|
245
|
+
)
|
|
246
|
+
page_set = (
|
|
247
|
+
range(len(pdf.pages))
|
|
248
|
+
if pages is None
|
|
249
|
+
else _selection(
|
|
250
|
+
pages,
|
|
251
|
+
names=False,
|
|
252
|
+
budget=pdf._load_budget,
|
|
253
|
+
page_count=len(pdf.pages),
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
if not selected and not annotations:
|
|
257
|
+
return
|
|
258
|
+
pdf._ensure_cos()
|
|
259
|
+
tree = _FieldTree(pdf) if selected else None
|
|
260
|
+
if tree is not None and not isinstance(selected, bool):
|
|
261
|
+
if strict:
|
|
262
|
+
for name in selected:
|
|
263
|
+
if name not in tree.names:
|
|
264
|
+
raise KeyError(f"Field '{name}' not found")
|
|
265
|
+
selected = {tree.names[name] for name in selected if name in tree.names}
|
|
266
|
+
if not page_set:
|
|
267
|
+
return
|
|
268
|
+
baked = set()
|
|
269
|
+
try:
|
|
270
|
+
for pi in sorted(page_set):
|
|
271
|
+
baked.update(_flatten_page(pdf, pi, tree, selected, annotations))
|
|
272
|
+
finally:
|
|
273
|
+
# A later page can fail its limits or content checks. Only widgets
|
|
274
|
+
# actually removed from page Annots may be pruned from the form tree.
|
|
275
|
+
if tree is not None:
|
|
276
|
+
tree.prune(baked)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _flatten_page(
|
|
280
|
+
pdf: Any,
|
|
281
|
+
pi: int,
|
|
282
|
+
tree: _FieldTree | None,
|
|
283
|
+
selected: bool | set[int],
|
|
284
|
+
annotations: bool,
|
|
285
|
+
) -> set[int]:
|
|
286
|
+
"""Commit one page and return only its successfully baked widget IDs."""
|
|
287
|
+
page = pdf._get_page_dict(pi)
|
|
288
|
+
if not isinstance(page, PdfDictionary):
|
|
289
|
+
return set()
|
|
290
|
+
annots = pdf._resolve(page.mapping.get(PdfName("Annots")))
|
|
291
|
+
if not isinstance(annots, PdfArray):
|
|
292
|
+
return set()
|
|
293
|
+
pdf._load_budget.check(
|
|
294
|
+
len(annots.items), "max_container_items", "flatten page annotations"
|
|
295
|
+
)
|
|
296
|
+
kept, drawings = [], []
|
|
297
|
+
page_baked = set()
|
|
298
|
+
for ai, reference in enumerate(annots.items):
|
|
299
|
+
annot = pdf._resolve(reference)
|
|
300
|
+
if not isinstance(annot, PdfDictionary):
|
|
301
|
+
kept.append(reference)
|
|
302
|
+
continue
|
|
303
|
+
widget = pdf._resolve(annot.mapping.get(PdfName("Subtype"))) == PdfName(
|
|
304
|
+
"Widget"
|
|
305
|
+
)
|
|
306
|
+
chosen = (
|
|
307
|
+
selected is True or bool(tree and tree.owners.get(id(annot)) in selected)
|
|
308
|
+
if widget
|
|
309
|
+
else annotations
|
|
310
|
+
)
|
|
311
|
+
if not chosen:
|
|
312
|
+
kept.append(reference)
|
|
313
|
+
continue
|
|
314
|
+
flags = pdf._resolve(annot.mapping.get(PdfName("F")))
|
|
315
|
+
if (
|
|
316
|
+
isinstance(flags, PdfNumber)
|
|
317
|
+
and math.isfinite(flags.value)
|
|
318
|
+
and int(flags.value) & ((1 << 1) | (1 << 5))
|
|
319
|
+
):
|
|
320
|
+
# Hidden/NoView objects must keep their viewing/printing semantics.
|
|
321
|
+
kept.append(reference)
|
|
322
|
+
continue
|
|
323
|
+
rect = pdf._resolve(annot.mapping.get(PdfName("Rect")))
|
|
324
|
+
values = (
|
|
325
|
+
[pdf._resolve(v) for v in rect.items]
|
|
326
|
+
if isinstance(rect, PdfArray) and len(rect.items) == 4
|
|
327
|
+
else []
|
|
328
|
+
)
|
|
329
|
+
if len(values) != 4 or not all(
|
|
330
|
+
isinstance(v, PdfNumber) and math.isfinite(v.value) for v in values
|
|
331
|
+
):
|
|
332
|
+
kept.append(reference)
|
|
333
|
+
continue
|
|
334
|
+
coords = [float(v.value) for v in values]
|
|
335
|
+
if widget and tree is not None:
|
|
336
|
+
tree.generate(annot)
|
|
337
|
+
elif not widget:
|
|
338
|
+
pdf.generate_annotation_appearance(pi, ai)
|
|
339
|
+
appearance = _appearance(pdf, annot)
|
|
340
|
+
if appearance is None:
|
|
341
|
+
kept.append(reference)
|
|
342
|
+
continue
|
|
343
|
+
ap_ref, normal = appearance
|
|
344
|
+
matrix = pdf._flatten_appearance_matrix(normal, coords)
|
|
345
|
+
if not all(math.isfinite(value) for value in matrix):
|
|
346
|
+
kept.append(reference)
|
|
347
|
+
continue
|
|
348
|
+
drawings.append((ap_ref, normal, matrix))
|
|
349
|
+
if widget:
|
|
350
|
+
page_baked.add(id(annot))
|
|
351
|
+
if not drawings:
|
|
352
|
+
return set()
|
|
353
|
+
current = pdf.get_page_content(pi)
|
|
354
|
+
isolation = isolation_for(current, budget=pdf._load_budget)
|
|
355
|
+
if not isolation.complete:
|
|
356
|
+
raise PdfValidationException(
|
|
357
|
+
f"Cannot flatten page {pi}: its graphics state cannot be safely isolated"
|
|
358
|
+
)
|
|
359
|
+
inherited = pdf._resolve_resources_cos(page)
|
|
360
|
+
resources = PdfDictionary(
|
|
361
|
+
dict(inherited.mapping) if isinstance(inherited, PdfDictionary) else {}
|
|
362
|
+
)
|
|
363
|
+
old_xobjects = pdf._resolve(resources.mapping.get(PdfName("XObject")))
|
|
364
|
+
xobjects = PdfDictionary(
|
|
365
|
+
dict(old_xobjects.mapping) if isinstance(old_xobjects, PdfDictionary) else {}
|
|
366
|
+
)
|
|
367
|
+
resources.mapping[PdfName("XObject")] = xobjects
|
|
368
|
+
content = bytearray(isolation.closing)
|
|
369
|
+
xi = 0
|
|
370
|
+
for ap_ref, normal, matrix in drawings:
|
|
371
|
+
while PdfName(f"FlatAnnot{xi}") in xobjects.mapping:
|
|
372
|
+
xi += 1
|
|
373
|
+
name = PdfName(f"FlatAnnot{xi}")
|
|
374
|
+
if not isinstance(ap_ref, PdfIndirectReference):
|
|
375
|
+
ap_ref = pdf._cos_doc.register_object(normal)
|
|
376
|
+
xobjects.mapping[name] = ap_ref
|
|
377
|
+
content.extend(
|
|
378
|
+
(
|
|
379
|
+
f"\nq\n{' '.join(f'{v:g}' for v in matrix)} cm\n{name.name} Do\nQ\n"
|
|
380
|
+
).encode("ascii")
|
|
381
|
+
)
|
|
382
|
+
page.mapping[PdfName("Resources")] = resources
|
|
383
|
+
pdf._append_content_to_cos_page(pi, bytes(content), opening=isolation.opening)
|
|
384
|
+
if not pdf._lazy:
|
|
385
|
+
pdf.page_contents[pi] = isolation.opening + current + b"\n" + bytes(content)
|
|
386
|
+
pdf._extracted_text = None
|
|
387
|
+
page.mapping[PdfName("Annots")] = PdfArray(kept)
|
|
388
|
+
return page_baked
|