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.
Files changed (265) hide show
  1. aspose_pdf/__init__.py +367 -0
  2. aspose_pdf/_compat_surface.py +142 -0
  3. aspose_pdf/_export_files.py +108 -0
  4. aspose_pdf/_pending_signatures.py +222 -0
  5. aspose_pdf/_version.py +17 -0
  6. aspose_pdf/actions.py +273 -0
  7. aspose_pdf/annotations/__init__.py +1614 -0
  8. aspose_pdf/annotations/pdf3d.py +70 -0
  9. aspose_pdf/attachments.py +62 -0
  10. aspose_pdf/auto_links.py +375 -0
  11. aspose_pdf/block_extraction.py +223 -0
  12. aspose_pdf/cgm.py +7 -0
  13. aspose_pdf/clustering/__init__.py +13 -0
  14. aspose_pdf/clustering/hierarchical_agglomerative_clustering.py +151 -0
  15. aspose_pdf/color.py +241 -0
  16. aspose_pdf/destinations.py +164 -0
  17. aspose_pdf/document.py +5884 -0
  18. aspose_pdf/document_fonts.py +150 -0
  19. aspose_pdf/drawing.py +13 -0
  20. aspose_pdf/engine/__init__.py +1 -0
  21. aspose_pdf/engine/agl.py +243 -0
  22. aspose_pdf/engine/annotation_cleanup.py +407 -0
  23. aspose_pdf/engine/annotation_import.py +503 -0
  24. aspose_pdf/engine/annotation_json.py +313 -0
  25. aspose_pdf/engine/annotation_xfdf.py +405 -0
  26. aspose_pdf/engine/annotation_xfdf_import.py +389 -0
  27. aspose_pdf/engine/appearance.py +1361 -0
  28. aspose_pdf/engine/attachment_zip.py +306 -0
  29. aspose_pdf/engine/attachment_zip_import.py +387 -0
  30. aspose_pdf/engine/auto_crop.py +101 -0
  31. aspose_pdf/engine/auto_tag.py +1451 -0
  32. aspose_pdf/engine/blank_pages.py +198 -0
  33. aspose_pdf/engine/blank_split.py +73 -0
  34. aspose_pdf/engine/bookmark_json.py +714 -0
  35. aspose_pdf/engine/bookmark_split.py +167 -0
  36. aspose_pdf/engine/ccitt.py +457 -0
  37. aspose_pdf/engine/ccitt_tables.py +231 -0
  38. aspose_pdf/engine/cert_chain.py +306 -0
  39. aspose_pdf/engine/cff_outlines.py +928 -0
  40. aspose_pdf/engine/cms.py +562 -0
  41. aspose_pdf/engine/comparison_html.py +435 -0
  42. aspose_pdf/engine/conformance.py +2748 -0
  43. aspose_pdf/engine/contact_sheet.py +215 -0
  44. aspose_pdf/engine/content_authoring.py +1347 -0
  45. aspose_pdf/engine/content_bounds.py +67 -0
  46. aspose_pdf/engine/content_isolation.py +180 -0
  47. aspose_pdf/engine/content_stream_parser.py +2318 -0
  48. aspose_pdf/engine/cos.py +416 -0
  49. aspose_pdf/engine/data/__init__.py +81 -0
  50. aspose_pdf/engine/data/agl/LICENSE-ADOBE-AGL.txt +28 -0
  51. aspose_pdf/engine/data/agl/LICENSE-REPORTLAB.txt +31 -0
  52. aspose_pdf/engine/data/agl/README.md +44 -0
  53. aspose_pdf/engine/data/agl/glyphlist.json.zlib +0 -0
  54. aspose_pdf/engine/data/cmaps/LICENSE-ADOBE-CMAP.txt +28 -0
  55. aspose_pdf/engine/data/cmaps/LICENSE-ADOBE-MAPPING.txt +28 -0
  56. aspose_pdf/engine/data/cmaps/README.md +51 -0
  57. aspose_pdf/engine/data/cmaps/predefined_cmaps_CNS1.json.zlib +0 -0
  58. aspose_pdf/engine/data/cmaps/predefined_cmaps_GB1.json.zlib +0 -0
  59. aspose_pdf/engine/data/cmaps/predefined_cmaps_Japan1.json.zlib +0 -0
  60. aspose_pdf/engine/data/cmaps/predefined_cmaps_Korea1.json.zlib +0 -0
  61. aspose_pdf/engine/data/cmaps/predefined_cmaps_index.json.zlib +0 -0
  62. aspose_pdf/engine/data/fonts/LICENSE-DEJAVU.txt +187 -0
  63. aspose_pdf/engine/data/fonts/LICENSE-OFL.txt +102 -0
  64. aspose_pdf/engine/data/fonts/README.md +47 -0
  65. aspose_pdf/engine/data/fonts/dingbats.ttf.zlib +0 -0
  66. aspose_pdf/engine/data/fonts/mono-bold.ttf.zlib +0 -0
  67. aspose_pdf/engine/data/fonts/mono-bolditalic.ttf.zlib +0 -0
  68. aspose_pdf/engine/data/fonts/mono-italic.ttf.zlib +0 -0
  69. aspose_pdf/engine/data/fonts/mono-regular.ttf.zlib +0 -0
  70. aspose_pdf/engine/data/fonts/sans-bold.ttf.zlib +0 -0
  71. aspose_pdf/engine/data/fonts/sans-bolditalic.ttf.zlib +0 -0
  72. aspose_pdf/engine/data/fonts/sans-italic.ttf.zlib +0 -0
  73. aspose_pdf/engine/data/fonts/sans-regular.ttf.zlib +0 -0
  74. aspose_pdf/engine/data/fonts/serif-bold.ttf.zlib +0 -0
  75. aspose_pdf/engine/data/fonts/serif-bolditalic.ttf.zlib +0 -0
  76. aspose_pdf/engine/data/fonts/serif-italic.ttf.zlib +0 -0
  77. aspose_pdf/engine/data/fonts/serif-regular.ttf.zlib +0 -0
  78. aspose_pdf/engine/data/fonts/symbol.ttf.zlib +0 -0
  79. aspose_pdf/engine/data/number.py +78 -0
  80. aspose_pdf/engine/data/types.py +156 -0
  81. aspose_pdf/engine/data/xmp.py +1458 -0
  82. aspose_pdf/engine/dct.py +854 -0
  83. aspose_pdf/engine/dss.py +658 -0
  84. aspose_pdf/engine/duplicate_pages.py +67 -0
  85. aspose_pdf/engine/encryption.py +967 -0
  86. aspose_pdf/engine/field_appearance.py +310 -0
  87. aspose_pdf/engine/file_output.py +98 -0
  88. aspose_pdf/engine/filters.py +958 -0
  89. aspose_pdf/engine/flatten.py +388 -0
  90. aspose_pdf/engine/font_authoring.py +609 -0
  91. aspose_pdf/engine/font_inventory.py +211 -0
  92. aspose_pdf/engine/font_resolver.py +829 -0
  93. aspose_pdf/engine/font_subset.py +545 -0
  94. aspose_pdf/engine/font_subset_cff.py +963 -0
  95. aspose_pdf/engine/font_subset_type1.py +114 -0
  96. aspose_pdf/engine/font_variations.py +159 -0
  97. aspose_pdf/engine/form_batch.py +134 -0
  98. aspose_pdf/engine/form_csv.py +267 -0
  99. aspose_pdf/engine/form_csv_export.py +131 -0
  100. aspose_pdf/engine/form_data.py +364 -0
  101. aspose_pdf/engine/form_field_edit.py +306 -0
  102. aspose_pdf/engine/form_fields.py +253 -0
  103. aspose_pdf/engine/form_json.py +89 -0
  104. aspose_pdf/engine/form_schema.py +246 -0
  105. aspose_pdf/engine/form_validation.py +395 -0
  106. aspose_pdf/engine/glyph_outlines.py +465 -0
  107. aspose_pdf/engine/graphics_absorb.py +1061 -0
  108. aspose_pdf/engine/grid_split.py +80 -0
  109. aspose_pdf/engine/gvar.py +467 -0
  110. aspose_pdf/engine/headings.py +273 -0
  111. aspose_pdf/engine/image_export.py +875 -0
  112. aspose_pdf/engine/image_import.py +219 -0
  113. aspose_pdf/engine/image_resample.py +116 -0
  114. aspose_pdf/engine/image_resolution.py +376 -0
  115. aspose_pdf/engine/image_zip.py +339 -0
  116. aspose_pdf/engine/imposition.py +233 -0
  117. aspose_pdf/engine/incremental_update.py +477 -0
  118. aspose_pdf/engine/inline_image.py +337 -0
  119. aspose_pdf/engine/interleave.py +188 -0
  120. aspose_pdf/engine/io.py +53 -0
  121. aspose_pdf/engine/jbig2.py +186 -0
  122. aspose_pdf/engine/jpeg2000.py +2124 -0
  123. aspose_pdf/engine/jpeg_encoder.py +706 -0
  124. aspose_pdf/engine/jpx.py +211 -0
  125. aspose_pdf/engine/link_extraction.py +378 -0
  126. aspose_pdf/engine/link_urls.py +252 -0
  127. aspose_pdf/engine/link_validation.py +555 -0
  128. aspose_pdf/engine/lists.py +307 -0
  129. aspose_pdf/engine/logging.py +18 -0
  130. aspose_pdf/engine/merge_bookmarks.py +180 -0
  131. aspose_pdf/engine/optional_content.py +1323 -0
  132. aspose_pdf/engine/page_count_split.py +60 -0
  133. aspose_pdf/engine/page_labels.py +295 -0
  134. aspose_pdf/engine/page_zip.py +219 -0
  135. aspose_pdf/engine/parser_exceptions.py +125 -0
  136. aspose_pdf/engine/pdf_matrix.py +90 -0
  137. aspose_pdf/engine/pdf_parser_cos.py +1611 -0
  138. aspose_pdf/engine/pdf_writer_cos.py +544 -0
  139. aspose_pdf/engine/predefined_cmaps.py +635 -0
  140. aspose_pdf/engine/primitives/__init__.py +5 -0
  141. aspose_pdf/engine/primitives/matrix.py +31 -0
  142. aspose_pdf/engine/pubsec.py +898 -0
  143. aspose_pdf/engine/rasterized_copy.py +171 -0
  144. aspose_pdf/engine/rasterizer.py +5483 -0
  145. aspose_pdf/engine/redaction.py +140 -0
  146. aspose_pdf/engine/revocation.py +495 -0
  147. aspose_pdf/engine/rich_text.py +415 -0
  148. aspose_pdf/engine/sanitize.py +412 -0
  149. aspose_pdf/engine/sfnt.py +247 -0
  150. aspose_pdf/engine/shading.py +2317 -0
  151. aspose_pdf/engine/sign_field.py +689 -0
  152. aspose_pdf/engine/signature_appearance.py +302 -0
  153. aspose_pdf/engine/signature_profiles.py +206 -0
  154. aspose_pdf/engine/signature_revisions.py +476 -0
  155. aspose_pdf/engine/signature_validator.py +313 -0
  156. aspose_pdf/engine/signed_changes.py +208 -0
  157. aspose_pdf/engine/signing.py +288 -0
  158. aspose_pdf/engine/simple_pdf.py +21416 -0
  159. aspose_pdf/engine/spread_split.py +330 -0
  160. aspose_pdf/engine/stamps.py +414 -0
  161. aspose_pdf/engine/std_font_data.py +201 -0
  162. aspose_pdf/engine/std_fonts.py +97 -0
  163. aspose_pdf/engine/std_metrics.py +364 -0
  164. aspose_pdf/engine/stream_output.py +22 -0
  165. aspose_pdf/engine/stroke.py +228 -0
  166. aspose_pdf/engine/svg_export.py +1162 -0
  167. aspose_pdf/engine/symbol_encodings.py +260 -0
  168. aspose_pdf/engine/tables.py +420 -0
  169. aspose_pdf/engine/text_blocks.py +467 -0
  170. aspose_pdf/engine/text_comparison.py +146 -0
  171. aspose_pdf/engine/text_edit.py +1936 -0
  172. aspose_pdf/engine/text_export.py +758 -0
  173. aspose_pdf/engine/text_faces.py +275 -0
  174. aspose_pdf/engine/text_highlight.py +197 -0
  175. aspose_pdf/engine/text_layout.py +574 -0
  176. aspose_pdf/engine/text_locate.py +423 -0
  177. aspose_pdf/engine/text_metrics.py +86 -0
  178. aspose_pdf/engine/text_outlines.py +150 -0
  179. aspose_pdf/engine/text_region.py +476 -0
  180. aspose_pdf/engine/text_selection.py +53 -0
  181. aspose_pdf/engine/text_split.py +141 -0
  182. aspose_pdf/engine/tiff_import.py +130 -0
  183. aspose_pdf/engine/timestamp.py +327 -0
  184. aspose_pdf/engine/toc.py +407 -0
  185. aspose_pdf/engine/type1_outlines.py +471 -0
  186. aspose_pdf/engine/viewer_preferences.py +275 -0
  187. aspose_pdf/engine/visual_comparison.py +318 -0
  188. aspose_pdf/engine/webp_encoder.py +107 -0
  189. aspose_pdf/engine/woff.py +302 -0
  190. aspose_pdf/engine/woff2.py +674 -0
  191. aspose_pdf/engine/word_extraction.py +235 -0
  192. aspose_pdf/exceptions.py +148 -0
  193. aspose_pdf/facades.py +1569 -0
  194. aspose_pdf/font_registry.py +149 -0
  195. aspose_pdf/font_repository.py +311 -0
  196. aspose_pdf/font_substitution.py +115 -0
  197. aspose_pdf/forms.py +1688 -0
  198. aspose_pdf/generated/__init__.py +3 -0
  199. aspose_pdf/generated/document.py +14 -0
  200. aspose_pdf/generated/forms.py +12 -0
  201. aspose_pdf/generated/pdfa.py +12 -0
  202. aspose_pdf/generated/security.py +33 -0
  203. aspose_pdf/geometry.py +149 -0
  204. aspose_pdf/graphics.py +256 -0
  205. aspose_pdf/html.py +117 -0
  206. aspose_pdf/image_resolution.py +45 -0
  207. aspose_pdf/images.py +524 -0
  208. aspose_pdf/interactive.py +435 -0
  209. aspose_pdf/latex.py +14 -0
  210. aspose_pdf/layers.py +482 -0
  211. aspose_pdf/license_management.py +65 -0
  212. aspose_pdf/link_extraction.py +52 -0
  213. aspose_pdf/link_validation.py +29 -0
  214. aspose_pdf/lists.py +365 -0
  215. aspose_pdf/load_limits.py +173 -0
  216. aspose_pdf/load_options.py +63 -0
  217. aspose_pdf/logical_structure.py +34 -0
  218. aspose_pdf/lowcode.py +1598 -0
  219. aspose_pdf/markdown.py +38 -0
  220. aspose_pdf/ofd.py +7 -0
  221. aspose_pdf/optimization.py +51 -0
  222. aspose_pdf/outlines.py +271 -0
  223. aspose_pdf/page_labels.py +169 -0
  224. aspose_pdf/page_ranges.py +90 -0
  225. aspose_pdf/page_size.py +220 -0
  226. aspose_pdf/pages.py +1883 -0
  227. aspose_pdf/paths.py +306 -0
  228. aspose_pdf/pdf_consts.py +19 -0
  229. aspose_pdf/pdfa.py +285 -0
  230. aspose_pdf/pdfua.py +194 -0
  231. aspose_pdf/pdfx.py +267 -0
  232. aspose_pdf/permissions.py +340 -0
  233. aspose_pdf/presentation.py +170 -0
  234. aspose_pdf/primitives.py +22 -0
  235. aspose_pdf/printing.py +41 -0
  236. aspose_pdf/py.typed +0 -0
  237. aspose_pdf/recipients.py +78 -0
  238. aspose_pdf/sanitization.py +90 -0
  239. aspose_pdf/save_format.py +25 -0
  240. aspose_pdf/save_options.py +25 -0
  241. aspose_pdf/security/__init__.py +160 -0
  242. aspose_pdf/security/impl/__init__.py +1 -0
  243. aspose_pdf/security/impl/digests/__init__.py +1 -0
  244. aspose_pdf/signature.py +385 -0
  245. aspose_pdf/signature_appearance.py +263 -0
  246. aspose_pdf/stamps.py +563 -0
  247. aspose_pdf/svg.py +41 -0
  248. aspose_pdf/table_extraction.py +156 -0
  249. aspose_pdf/tables.py +272 -0
  250. aspose_pdf/tagged.py +254 -0
  251. aspose_pdf/text.py +762 -0
  252. aspose_pdf/text_block.py +286 -0
  253. aspose_pdf/text_comparison.py +31 -0
  254. aspose_pdf/text_layout.py +100 -0
  255. aspose_pdf/utils.py +74 -0
  256. aspose_pdf/validation.py +286 -0
  257. aspose_pdf/viewer_preferences.py +375 -0
  258. aspose_pdf/visual_comparison.py +53 -0
  259. aspose_pdf/visualization.py +118 -0
  260. aspose_pdf/xmp.py +75 -0
  261. aspose_pdf_foss_for_python-0.1.0.dist-info/METADATA +2233 -0
  262. aspose_pdf_foss_for_python-0.1.0.dist-info/RECORD +265 -0
  263. aspose_pdf_foss_for_python-0.1.0.dist-info/WHEEL +5 -0
  264. aspose_pdf_foss_for_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  265. 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