1# Copyright (c) 2006, Mathieu Fenniak
2# All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are
6# met:
7#
8# * Redistributions of source code must retain the above copyright notice,
9# this list of conditions and the following disclaimer.
10# * Redistributions in binary form must reproduce the above copyright notice,
11# this list of conditions and the following disclaimer in the documentation
12# and/or other materials provided with the distribution.
13# * The name of the author may not be used to endorse or promote products
14# derived from this software without specific prior written permission.
15#
16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26# POSSIBILITY OF SUCH DAMAGE.
27
28__author__ = "Mathieu Fenniak"
29__author_email__ = "biziqe@mathieu.fenniak.net"
30
31import logging
32import os
33import re
34import sys
35from collections.abc import Iterable, Sequence
36from io import BytesIO
37from math import ceil
38from typing import (
39 Any,
40 Callable,
41 NamedTuple,
42 Optional,
43 Union,
44 cast,
45)
46
47from .. import get_configuration
48from .._protocols import PdfReaderProtocol, PdfWriterProtocol, XmpInformationProtocol
49from .._utils import (
50 WHITESPACES,
51 BinaryStreamType,
52 StreamType,
53 deprecation_no_replacement,
54 logger_warning,
55 read_non_whitespace,
56 read_until_regex,
57 read_until_whitespace,
58 skip_over_comment,
59 skip_over_comments,
60)
61from ..constants import (
62 CheckboxRadioButtonAttributes,
63 FieldDictionaryAttributes,
64 OutlineFontFlag,
65 StreamAttributes,
66)
67from ..constants import FilterTypes as FT
68from ..constants import TypArguments as TA
69from ..constants import TypFitArguments as TF
70from ..errors import STREAM_TRUNCATED_PREMATURELY, LimitReachedError, PdfReadError, PdfStreamError
71from ._base import (
72 BooleanObject,
73 ByteStringObject,
74 FloatObject,
75 IndirectObject,
76 NameObject,
77 NullObject,
78 NumberObject,
79 PdfObject,
80 TextStringObject,
81 is_null_or_none,
82)
83from ._fit import Fit
84from ._image_inline import (
85 extract_inline__ascii85_decode,
86 extract_inline__ascii_hex_decode,
87 extract_inline__dct_decode,
88 extract_inline__run_length_decode,
89 extract_inline_default,
90)
91from ._utils import StreamParsingBudget, read_hex_string_from_stream, read_string_from_stream
92
93if sys.version_info >= (3, 11):
94 from typing import Self
95else:
96 from typing_extensions import Self
97
98logger = logging.getLogger(__name__)
99
100IndirectPattern = re.compile(rb"[+-]?(\d+)\s+(\d+)\s+R[^a-zA-Z]")
101
102# Direct objects of these types are neither streams nor indirect objects.
103# Large arrays consist mostly of them, for example in the /ParentTree of the
104# structure tree, so the clone loops check the exact type first and skip the
105# slower isinstance() checks.
106_SCALAR_TYPES = frozenset(
107 (BooleanObject, ByteStringObject, FloatObject, NameObject, NullObject, NumberObject, TextStringObject)
108)
109
110
111class ArrayObject(list[Any], PdfObject):
112 def replicate(
113 self,
114 pdf_dest: PdfWriterProtocol,
115 ) -> "ArrayObject":
116 arr = cast(
117 "ArrayObject",
118 self._reference_clone(ArrayObject(), pdf_dest, False),
119 )
120 for data in self:
121 if hasattr(data, "replicate"):
122 arr.append(data.replicate(pdf_dest))
123 else:
124 arr.append(data)
125 return arr
126
127 def clone(
128 self,
129 pdf_dest: PdfWriterProtocol,
130 force_duplicate: bool = False,
131 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
132 ) -> "ArrayObject":
133 """Clone object into pdf_dest."""
134 try:
135 if self.indirect_reference.pdf == pdf_dest and not force_duplicate: # type: ignore[union-attr]
136 return self
137 except Exception:
138 pass
139 arr = cast(
140 "ArrayObject",
141 self._reference_clone(ArrayObject(), pdf_dest, force_duplicate=True),
142 )
143 for data in self:
144 if type(data) in _SCALAR_TYPES:
145 # Same as the generic case below, but without the isinstance() checks.
146 arr.append(data.clone(pdf_dest, force_duplicate, ignore_fields))
147 elif isinstance(data, StreamObject):
148 dup = data._reference_clone(
149 data.clone(pdf_dest, force_duplicate, ignore_fields),
150 pdf_dest,
151 force_duplicate,
152 )
153 arr.append(dup.indirect_reference)
154 elif isinstance(data, IndirectObject) and isinstance(resolved := data.get_object(), StreamObject):
155 dup = data._reference_clone(
156 resolved.clone(pdf_dest, force_duplicate=True, ignore_fields=ignore_fields),
157 pdf_dest,
158 force_duplicate,
159 )
160 arr.append(dup.indirect_reference)
161 elif hasattr(data, "clone"):
162 arr.append(data.clone(pdf_dest, force_duplicate, ignore_fields))
163 else:
164 arr.append(data)
165 return arr
166
167 def hash_bin(self) -> int:
168 """
169 Used to detect modified object.
170
171 Returns:
172 Hash considering type and value.
173
174 """
175 return hash((self.__class__, tuple(x.hash_bin() for x in self)))
176
177 def items(self) -> Iterable[Any]:
178 """Emulate DictionaryObject.items for a list (index, object)."""
179 return enumerate(self)
180
181 def _to_lst(self, lst: Any) -> list[Any]:
182 # Convert to list, internal
183 result: list[Any]
184 if isinstance(lst, (list, tuple, set)):
185 result = list(lst)
186 elif isinstance(lst, PdfObject):
187 result = [lst]
188 elif isinstance(lst, str):
189 if lst[0] == "/":
190 result = [NameObject(lst)]
191 else:
192 result = [TextStringObject(lst)]
193 elif isinstance(lst, bytes):
194 result = [ByteStringObject(lst)]
195 else: # for numbers,...
196 result = [lst]
197 return result
198
199 def __add__(self, lst: Any) -> "ArrayObject":
200 """
201 Allow extension by adding list or add one element only
202
203 Args:
204 lst: any list, tuples are extended the list.
205 other types(numbers,...) will be appended.
206 if str is passed it will be converted into TextStringObject
207 or NameObject (if starting with "/")
208 if bytes is passed it will be converted into ByteStringObject
209
210 Returns:
211 ArrayObject with all elements
212
213 """
214 temp = ArrayObject(self)
215 temp.extend(self._to_lst(lst))
216 return temp
217
218 def __iadd__(self, lst: Any) -> Self:
219 """
220 Allow extension by adding list or add one element only
221
222 Args:
223 lst: any list, tuples are extended the list.
224 other types(numbers,...) will be appended.
225 if str is passed it will be converted into TextStringObject
226 or NameObject (if starting with "/")
227 if bytes is passed it will be converted into ByteStringObject
228
229 """
230 self.extend(self._to_lst(lst))
231 return self
232
233 def __isub__(self, lst: Any) -> Self:
234 """Allow to remove items"""
235 for x in self._to_lst(lst):
236 try:
237 index = self.index(x)
238 del self[index]
239 except ValueError:
240 pass
241 return self
242
243 def write_to_stream(
244 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
245 ) -> None:
246 if encryption_key is not None: # deprecated
247 deprecation_no_replacement(
248 "the encryption_key parameter of write_to_stream", "5.0.0"
249 )
250 stream.write(b"[")
251 for data in self:
252 stream.write(b" ")
253 data.write_to_stream(stream)
254 stream.write(b" ]")
255
256 @staticmethod
257 def _check_limits(*, remaining_elements: int, parsing_budget: Optional[StreamParsingBudget]) -> int:
258 remaining_elements -= 1
259 if remaining_elements < 0:
260 raise LimitReachedError("Reached maximum array element count.")
261 if parsing_budget is not None:
262 parsing_budget.remaining_budget -= 1
263 if parsing_budget.remaining_budget < 0:
264 raise LimitReachedError("Reached maximum number of tokens.")
265 return remaining_elements
266
267 @staticmethod
268 def read_from_stream(
269 stream: StreamType,
270 pdf: Optional[PdfReaderProtocol],
271 forced_encoding: Union[str, list[str], dict[int, str], None] = None,
272 *,
273 parsing_budget: Optional[StreamParsingBudget] = None,
274 ) -> "ArrayObject":
275 arr = ArrayObject()
276 tmp = stream.read(1)
277 if tmp != b"[":
278 raise PdfReadError("Could not read array")
279 remaining_elements = get_configuration().array_maximum_element_count
280 while True:
281 # skip leading whitespace
282 tok = stream.read(1)
283 while tok.isspace() or tok in WHITESPACES:
284 tok = stream.read(1)
285 if tok == b"":
286 break
287 if tok == b"%":
288 stream.seek(-1, 1)
289 skip_over_comment(stream)
290 continue
291 # check for array ending
292 if tok == b"]":
293 break
294
295 remaining_elements = ArrayObject._check_limits(
296 remaining_elements=remaining_elements, parsing_budget=parsing_budget
297 )
298 if tok == b"n":
299 # Shortcut for null, as some arrays consist mostly of nulls
300 rest = stream.read(3)
301 if rest == b"ull":
302 arr.append(NullObject())
303 continue
304 stream.seek(-len(rest), 1)
305 stream.seek(-1, 1)
306 # read and append object
307 arr.append(read_object(stream, pdf, forced_encoding, parsing_budget=parsing_budget))
308 return arr
309
310
311class DictionaryObject(dict[Any, Any], PdfObject):
312 def replicate(
313 self,
314 pdf_dest: PdfWriterProtocol,
315 ) -> "DictionaryObject":
316 d__ = cast(
317 "DictionaryObject",
318 self._reference_clone(self.__class__(), pdf_dest, False),
319 )
320 for k, v in self.items():
321 d__[k.replicate(pdf_dest)] = (
322 v.replicate(pdf_dest) if hasattr(v, "replicate") else v
323 )
324 return d__
325
326 def clone(
327 self,
328 pdf_dest: PdfWriterProtocol,
329 force_duplicate: bool = False,
330 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
331 ) -> "DictionaryObject":
332 """Clone object into pdf_dest."""
333 try:
334 if self.indirect_reference.pdf == pdf_dest and not force_duplicate: # type: ignore[union-attr]
335 return self
336 except Exception:
337 pass
338
339 visited: set[tuple[int, int]] = set() # (idnum, generation)
340 d__ = cast(
341 "DictionaryObject",
342 self._reference_clone(self.__class__(), pdf_dest, force_duplicate),
343 )
344 if ignore_fields is None:
345 ignore_fields = []
346 if len(d__.keys()) == 0:
347 d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited)
348 return d__
349
350 def _clone(
351 self,
352 src: "DictionaryObject",
353 pdf_dest: PdfWriterProtocol,
354 force_duplicate: bool,
355 ignore_fields: Optional[Sequence[Union[str, int]]],
356 visited: set[tuple[int, int]], # (idnum, generation)
357 ) -> None:
358 """
359 Update the object from src.
360
361 Args:
362 src: "DictionaryObject":
363 pdf_dest:
364 force_duplicate:
365 ignore_fields:
366
367 """
368 # First we remove the ignore_fields
369 # that are for a limited number of levels
370 assert ignore_fields is not None
371 ignore_fields = list(ignore_fields)
372 x = 0
373 while x < len(ignore_fields):
374 if isinstance(ignore_fields[x], int):
375 if cast(int, ignore_fields[x]) <= 0:
376 del ignore_fields[x]
377 del ignore_fields[x]
378 continue
379 ignore_fields[x] -= 1 # type:ignore
380 x += 1
381 # Check if this is a chain list, we need to loop to prevent recur
382 if any(
383 field not in ignore_fields
384 and field in src
385 and isinstance(src.raw_get(field), IndirectObject)
386 and isinstance(src[field], DictionaryObject)
387 and (
388 src.get("/Type", None) is None
389 or cast(DictionaryObject, src[field]).get("/Type", None) is None
390 or src.get("/Type", None)
391 == cast(DictionaryObject, src[field]).get("/Type", None)
392 )
393 for field in ["/Next", "/Prev", "/N", "/V"]
394 ):
395 ignore_fields = list(ignore_fields)
396 for lst in (("/Next", "/Prev"), ("/N", "/V")):
397 for k in lst:
398 objs = []
399 if (
400 k in src
401 and k not in self
402 and isinstance(src.raw_get(k), IndirectObject)
403 and isinstance(src[k], DictionaryObject)
404 # If need to go further the idea is to check
405 # that the types are the same
406 and (
407 src.get("/Type", None) is None
408 or cast(DictionaryObject, src[k]).get("/Type", None) is None
409 or src.get("/Type", None)
410 == cast(DictionaryObject, src[k]).get("/Type", None)
411 )
412 ):
413 cur_obj: Optional[DictionaryObject] = cast(
414 "DictionaryObject", src[k]
415 )
416 prev_obj: Optional[DictionaryObject] = self
417 while cur_obj is not None:
418 clon = cast(
419 "DictionaryObject",
420 cur_obj._reference_clone(
421 cur_obj.__class__(), pdf_dest, force_duplicate
422 ),
423 )
424 # Check to see if we've previously processed our item
425 if clon.indirect_reference is not None:
426 idnum = clon.indirect_reference.idnum
427 generation = clon.indirect_reference.generation
428 if (idnum, generation) in visited:
429 cur_obj = None
430 break
431 visited.add((idnum, generation))
432 objs.append((cur_obj, clon))
433 assert prev_obj is not None
434 prev_obj[NameObject(k)] = clon.indirect_reference
435 prev_obj = clon
436 try:
437 if cur_obj == src:
438 cur_obj = None
439 else:
440 cur_obj = cast("DictionaryObject", cur_obj[k])
441 except Exception:
442 cur_obj = None
443 for s, c in objs:
444 c._clone(
445 s, pdf_dest, force_duplicate, ignore_fields, visited
446 )
447
448 for k, v in src.items():
449 if k not in ignore_fields:
450 # The type check does not change the result. It only skips the
451 # slower isinstance() check for the most common values.
452 if type(v) not in _SCALAR_TYPES and isinstance(v, StreamObject):
453 if not hasattr(v, "indirect_reference"):
454 v.indirect_reference = None
455 vv = v.clone(pdf_dest, force_duplicate, ignore_fields)
456 assert vv.indirect_reference is not None
457 self[k.clone(pdf_dest)] = vv.indirect_reference
458 elif k not in self:
459 self[NameObject(k)] = (
460 v.clone(pdf_dest, force_duplicate, ignore_fields)
461 if hasattr(v, "clone")
462 else v
463 )
464
465 def hash_bin(self) -> int:
466 """
467 Used to detect modified object.
468
469 Returns:
470 Hash considering type and value.
471
472 """
473 return hash(
474 (self.__class__, tuple(((k, v.hash_bin()) for k, v in self.items())))
475 )
476
477 def raw_get(self, key: Any) -> Any:
478 return dict.__getitem__(self, key)
479
480 def get_inherited(self, key: str, default: Any = None) -> Any:
481 """
482 Returns the value of a key or from the parent if not found.
483 If not found returns default.
484
485 Args:
486 key: string identifying the field to return
487
488 default: default value to return
489
490 Returns:
491 Current key or inherited one, otherwise default value.
492
493 """
494 current = self
495 visited: set[int] = set()
496
497 while True:
498 # Detect cyclic parent references
499 obj_id = id(current)
500 if obj_id in visited:
501 raise LimitReachedError(f"Detected cycle in /Parent hierarchy when retrieving value for key {key!r}.")
502 visited.add(obj_id)
503
504 if key in current:
505 return current[key]
506
507 if "/Parent" not in current:
508 return default
509
510 # Walk upward
511 current = cast(
512 "DictionaryObject",
513 current["/Parent"].get_object(),
514 )
515
516 def __setitem__(self, key: Any, value: Any) -> Any:
517 if not isinstance(key, PdfObject):
518 raise ValueError("Key must be a PdfObject")
519 if not isinstance(value, PdfObject):
520 raise ValueError("Value must be a PdfObject")
521 return dict.__setitem__(self, key, value)
522
523 def setdefault(self, key: Any, value: Optional[Any] = None) -> Any:
524 if not isinstance(key, PdfObject):
525 raise ValueError("Key must be a PdfObject")
526 if not isinstance(value, PdfObject):
527 raise ValueError("Value must be a PdfObject")
528 return dict.setdefault(self, key, value)
529
530 def __getitem__(self, key: Any) -> PdfObject:
531 return cast(PdfObject, dict.__getitem__(self, key).get_object())
532
533 @property
534 def xmp_metadata(self) -> Optional[XmpInformationProtocol]:
535 """
536 Retrieve XMP (Extensible Metadata Platform) data relevant to this
537 object, if available.
538
539 See Table 347 — Additional entries in a metadata stream dictionary.
540
541 Returns:
542 Returns a :class:`~pypdf.xmp.XmpInformation` instance
543 that can be used to access XMP metadata from the document. Can also
544 return None if no metadata was found on the document root.
545
546 """
547 from ..xmp import XmpInformation # noqa: PLC0415
548
549 metadata = self.get("/Metadata", None)
550 if is_null_or_none(metadata):
551 return None
552 assert metadata is not None, "mypy"
553 metadata = metadata.get_object()
554 return XmpInformation(metadata)
555
556 def write_to_stream(
557 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
558 ) -> None:
559 if encryption_key is not None: # deprecated
560 deprecation_no_replacement(
561 "the encryption_key parameter of write_to_stream", "5.0.0"
562 )
563 stream.write(b"<<\n")
564 for key, value in self.items():
565 if len(key) > 2 and key[1] == "%" and key[-1] == "%":
566 continue
567 key.write_to_stream(stream, encryption_key)
568 stream.write(b" ")
569 value.write_to_stream(stream)
570 stream.write(b"\n")
571 stream.write(b">>")
572
573 @classmethod
574 def _get_next_object_position(
575 cls, position_before: int, position_end: int, generations: list[int], pdf: PdfReaderProtocol
576 ) -> int:
577 out = position_end
578 for generation in generations:
579 for x in pdf.xref[generation].values():
580 if position_before < x <= position_end:
581 out = min(out, x)
582 return out
583
584 @classmethod
585 def _read_unsized_from_stream(
586 cls, *, stream: BinaryStreamType, pdf: PdfReaderProtocol, length: int,
587 ) -> bytes:
588 current_position = stream.tell()
589
590 # Determine stream size.
591 try:
592 stream.seek(0, os.SEEK_END)
593 stream_length = stream.tell()
594 finally:
595 stream.seek(current_position)
596
597 object_position = cls._get_next_object_position(
598 position_before=current_position, position_end=stream_length, generations=list(pdf.xref), pdf=pdf
599 )
600
601 bytes_to_read = object_position - current_position
602 if bytes_to_read >= length:
603 raise LimitReachedError(f"Requested length of {bytes_to_read} exceeds maximum allowed length.")
604
605 # Read until the next object position.
606 read_value = stream.read(bytes_to_read)
607 endstream_position = read_value.find(b"endstream")
608 if endstream_position < 0:
609 raise PdfReadError(
610 f"Unable to find 'endstream' marker for obj starting at {current_position}."
611 )
612 # 9 = len(b"endstream")
613 stream.seek(current_position + endstream_position + 9)
614 return read_value[: endstream_position - 1]
615
616 @staticmethod
617 def read_from_stream(
618 stream: StreamType,
619 pdf: Optional[PdfReaderProtocol],
620 forced_encoding: Union[str, list[str], dict[int, str], None] = None,
621 *,
622 parsing_budget: Optional[StreamParsingBudget] = None,
623 ) -> "DictionaryObject":
624 tmp = stream.read(2)
625 if tmp != b"<<":
626 raise PdfReadError(
627 f"Dictionary read error at byte {hex(stream.tell())}: "
628 "stream must begin with '<<'"
629 )
630 data: dict[Any, Any] = {}
631 remaining_count = get_configuration().dictionary_maximum_element_count
632 while True:
633 tok = read_non_whitespace(stream)
634 if tok == b"\x00":
635 continue
636 if tok == b"%":
637 stream.seek(-1, 1)
638 skip_over_comment(stream)
639 continue
640 if not tok:
641 raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
642
643 if tok == b">":
644 stream.read(1)
645 break
646 stream.seek(-1, 1)
647
648 remaining_count -= 1
649 if remaining_count < 0:
650 raise LimitReachedError("Reached maximum number of dictionary entries.")
651 if parsing_budget is not None:
652 parsing_budget.remaining_budget -= 2
653 if parsing_budget.remaining_budget < 0:
654 raise LimitReachedError("Reached maximum number of tokens.")
655
656 try:
657 try:
658 key = read_object(stream, pdf, parsing_budget=parsing_budget)
659 if isinstance(key, NullObject):
660 break
661 if not isinstance(key, NameObject):
662 raise PdfReadError(
663 f"Expecting a NameObject for key but found {key!r}"
664 )
665 except PdfReadError as exc:
666 if pdf is not None and pdf.strict:
667 raise
668 logger_warning("%(exception)r", source=__name__, exception=exc)
669 continue
670 tok = read_non_whitespace(stream)
671 stream.seek(-1, 1)
672 value = read_object(stream, pdf, forced_encoding, parsing_budget=parsing_budget)
673 except LimitReachedError:
674 # Always propagate security issues.
675 raise
676 except RecursionError as exc:
677 raise PdfReadError(exc.__repr__()) from exc
678 except Exception as exc:
679 if pdf is not None and pdf.strict:
680 raise PdfReadError(exc.__repr__()) from exc
681 logger_warning("%(exception)r", source=__name__, exception=exc)
682 retval = DictionaryObject()
683 retval.update(data)
684 return retval # return partial data
685
686 if key not in data:
687 data[key] = value
688 else:
689 # multiple definitions of key not permitted
690 msg = (
691 "Multiple definitions in dictionary at byte "
692 "%(position)s for key %(key)s"
693 )
694 values = {"position": hex(stream.tell()), "key": key}
695 if pdf is not None and pdf.strict:
696 raise PdfReadError(msg % values)
697 logger_warning(msg, source=__name__, **values)
698
699 DictionaryObject._read_stream_data(stream=stream, pdf=pdf, data=data)
700 if "__streamdata__" in data:
701 return StreamObject.initialize_from_dictionary(data)
702 retval = DictionaryObject()
703 retval.update(data)
704 return retval
705
706 @staticmethod
707 def _read_stream_data(
708 *,
709 stream: StreamType,
710 pdf: Optional[PdfReaderProtocol],
711 data: dict[Any, Any]
712 ) -> None:
713 pos = stream.tell()
714 s = read_non_whitespace(stream)
715 if s == b"s" and stream.read(5) == b"tream":
716 eol = stream.read(1)
717 # Occasional PDF file output has spaces after 'stream' keyword but before EOL.
718 # patch provided by Danial Sandler
719 while eol == b" ":
720 eol = stream.read(1)
721 if eol not in (b"\n", b"\r"):
722 raise PdfStreamError("Stream data must be followed by a newline")
723 if eol == b"\r" and stream.read(1) != b"\n":
724 stream.seek(-1, 1)
725 # this is a stream object, not a dictionary
726 if StreamAttributes.LENGTH not in data:
727 if pdf is not None and pdf.strict:
728 raise PdfStreamError("Stream length not defined")
729 logger_warning(
730 "Stream length not defined @pos=%(position)d",
731 source=__name__,
732 position=stream.tell(),
733 )
734 data[NameObject(StreamAttributes.LENGTH)] = NumberObject(-1)
735 length = data[StreamAttributes.LENGTH]
736 if isinstance(length, IndirectObject):
737 t = stream.tell()
738 assert pdf is not None, "mypy"
739 length = pdf.get_object(length)
740 stream.seek(t, 0)
741 if length is None: # if the PDF is damaged
742 length = -1
743 pstart = stream.tell()
744
745 from .._configuration import get_configuration # noqa: PLC0415
746 configuration = get_configuration()
747 if length >= 0:
748 if length > configuration.maximum_declared_stream_length:
749 raise LimitReachedError(f"Declared stream length of {length} exceeds maximum allowed length.")
750
751 data["__streamdata__"] = stream.read(length)
752 else:
753 data["__streamdata__"] = read_until_regex(
754 stream=stream, regex=re.compile(b"endstream"), length=configuration.maximum_declared_stream_length,
755 )
756 e = read_non_whitespace(stream)
757 ndstream = stream.read(8)
758 if (e + ndstream) != b"endstream":
759 # the odd PDF file has a length that is too long, so
760 # we need to read backwards to find the "endstream" ending.
761 # ReportLab (unknown version) generates files with this bug,
762 # and Python users into PDF files tend to be our audience.
763 # we need to do this to correct the streamdata and chop off
764 # an extra character.
765 pos = stream.tell()
766 stream.seek(-10, 1)
767 end = stream.read(9)
768 if end == b"endstream":
769 # we found it by looking back one character further.
770 data["__streamdata__"] = data["__streamdata__"][:-1]
771 elif pdf is not None and not pdf.strict:
772 stream.seek(pstart, 0)
773 data["__streamdata__"] = DictionaryObject._read_unsized_from_stream(
774 stream=stream, pdf=pdf, length=configuration.maximum_declared_stream_length
775 )
776 pos = stream.tell()
777 else:
778 stream.seek(pos, 0)
779 raise PdfReadError(
780 "Unable to find 'endstream' marker after stream at byte "
781 f"{hex(stream.tell())} (nd='{ndstream!r}', end='{end!r}')."
782 )
783 else:
784 stream.seek(pos, 0)
785
786
787class TreeObject(DictionaryObject):
788 def __init__(self, dct: Optional[DictionaryObject] = None) -> None:
789 DictionaryObject.__init__(self)
790 if dct:
791 self.update(dct)
792
793 def has_children(self) -> bool:
794 return "/First" in self
795
796 def __iter__(self) -> Any:
797 return self.children()
798
799 def children(self) -> Iterable[Any]:
800 if not self.has_children():
801 return
802
803 child_ref = self[NameObject("/First")]
804 last = self[NameObject("/Last")]
805 child = child_ref.get_object()
806 visited: set[int] = set()
807 while True:
808 child_id = id(child)
809 if child_id in visited:
810 logger_warning("Detected cycle in outline structure for %(child)s", source=__name__, child=child)
811 return
812 visited.add(child_id)
813
814 yield child
815
816 if child == last:
817 return
818 child_ref = child.get(NameObject("/Next")) # type: ignore[union-attr]
819 if is_null_or_none(child_ref):
820 return
821 child = child_ref.get_object()
822
823 def add_child(self, child: Any, pdf: PdfWriterProtocol) -> None:
824 self.insert_child(child, None, pdf)
825
826 def inc_parent_counter_default(
827 self, parent: Union[IndirectObject, "TreeObject", None], n: int
828 ) -> None:
829 if is_null_or_none(parent):
830 return
831 assert parent is not None, "mypy"
832 parent = cast("TreeObject", parent.get_object())
833 if "/Count" in parent:
834 parent[NameObject("/Count")] = NumberObject(
835 max(0, cast(int, parent[NameObject("/Count")]) + n)
836 )
837 self.inc_parent_counter_default(parent.get("/Parent", None), n)
838
839 def inc_parent_counter_outline(
840 self, parent: Union[IndirectObject, "TreeObject", None], n: int
841 ) -> None:
842 if is_null_or_none(parent):
843 return
844 assert parent is not None, "mypy"
845 parent = cast("TreeObject", parent.get_object())
846 # BooleanObject requires comparison with == not is
847 opn = parent.get("/%is_open%", True) == True # noqa: E712
848 c = cast(int, parent.get("/Count", 0))
849 if c < 0:
850 c = abs(c)
851 parent[NameObject("/Count")] = NumberObject((c + n) * (1 if opn else -1))
852 if not opn:
853 return
854 self.inc_parent_counter_outline(parent.get("/Parent", None), n)
855
856 def insert_child(
857 self,
858 child: Any,
859 before: Any,
860 pdf: PdfWriterProtocol,
861 inc_parent_counter: Optional[Callable[..., Any]] = None,
862 ) -> IndirectObject:
863 if inc_parent_counter is None:
864 inc_parent_counter = self.inc_parent_counter_default
865 child_obj = child.get_object()
866 assert child.indirect_reference is not None, "mypy"
867 child_reference: IndirectObject = child.indirect_reference
868
869 prev: Optional[DictionaryObject]
870 if "/First" not in self: # no child yet
871 self[NameObject("/First")] = child_reference
872 self[NameObject("/Count")] = NumberObject(0)
873 self[NameObject("/Last")] = child_reference
874 child_obj[NameObject("/Parent")] = self.indirect_reference
875 inc_parent_counter(self, child_obj.get("/Count", 1))
876 if "/Next" in child_obj:
877 del child_obj["/Next"]
878 if "/Prev" in child_obj:
879 del child_obj["/Prev"]
880 return child_reference
881 prev = cast("DictionaryObject", self["/Last"])
882
883 visited: set[int] = set()
884 while prev.indirect_reference != before:
885 prev_id = id(prev)
886 if prev_id in visited:
887 raise LimitReachedError("Detected cycle in tree structure.")
888 visited.add(prev_id)
889 if "/Next" in prev:
890 prev = cast("TreeObject", prev["/Next"])
891 continue
892
893 # append at the end
894 prev[NameObject("/Next")] = cast("TreeObject", child_reference)
895 child_obj[NameObject("/Prev")] = prev.indirect_reference
896 child_obj[NameObject("/Parent")] = self.indirect_reference
897 if "/Next" in child_obj:
898 del child_obj["/Next"]
899 self[NameObject("/Last")] = child_reference
900 inc_parent_counter(self, child_obj.get("/Count", 1))
901 return child_reference
902 try: # insert as first or in the middle
903 assert isinstance(prev["/Prev"], DictionaryObject)
904 prev["/Prev"][NameObject("/Next")] = child_reference
905 child_obj[NameObject("/Prev")] = prev["/Prev"]
906 except Exception: # it means we are inserting in first position
907 child_obj.pop("/Next", None)
908 child_obj[NameObject("/Next")] = prev
909 prev[NameObject("/Prev")] = child_reference
910 child_obj[NameObject("/Parent")] = self.indirect_reference
911 inc_parent_counter(self, child_obj.get("/Count", 1))
912 return child_reference
913
914 def _remove_node_from_tree(
915 self, *, previous: Any, previous_reference: Any, current: Any, last: Any
916 ) -> None:
917 """Adjust the pointers of the linked list and tree node count."""
918 next_reference = current.get("/Next", None)
919 if previous is None:
920 if next_reference:
921 # Removing first tree node
922 next_object = next_reference.get_object()
923 del next_object["/Prev"]
924 self[NameObject("/First")] = next_reference
925 self[NameObject("/Count")] = NumberObject(
926 self[NameObject("/Count")] - 1 # type: ignore[operator]
927 )
928
929 else:
930 # Removing only tree node
931 self[NameObject("/Count")] = NumberObject(0)
932 del self["/First"]
933 if NameObject("/Last") in self:
934 del self[NameObject("/Last")]
935 else:
936 if next_reference:
937 # Removing middle tree node
938 next_object = next_reference.get_object()
939 next_object[NameObject("/Prev")] = previous_reference
940 previous[NameObject("/Next")] = next_reference
941 else:
942 # Removing last tree node
943 assert current == last
944 del previous["/Next"]
945 self[NameObject("/Last")] = previous_reference
946 self[NameObject("/Count")] = NumberObject(self["/Count"] - 1) # type: ignore[operator]
947
948 def remove_child(self, child: Any) -> None:
949 child_object = child.get_object()
950
951 if "/Parent" not in child_object:
952 raise ValueError("Removed child does not appear to be a tree item")
953 if child_object["/Parent"] != self:
954 raise ValueError("Removed child is not a member of this tree")
955
956 found = False
957 previous_reference = None
958 previous = None
959 current_reference: Optional[Any] = self["/First"]
960 current: Optional[dict[str, Any]] = current_reference.get_object() # type: ignore[union-attr]
961 last_reference = self["/Last"]
962 last = last_reference.get_object()
963 visited: set[int] = set()
964 while current is not None:
965 if current == child_object:
966 TreeObject._remove_node_from_tree(
967 self, previous=previous, previous_reference=previous_reference, current=current, last=last
968 )
969 found = True
970 break
971 current_id = id(current)
972 if current_id in visited:
973 raise LimitReachedError("Detected cyclic /Next hierarchy.")
974 visited.add(current_id)
975
976 # Go to the next node.
977 previous_reference = current_reference
978 previous = current
979 if "/Next" not in current:
980 break
981 current_reference = current["/Next"]
982 current = current_reference.get_object()
983
984 if not found:
985 raise ValueError("Removal couldn't find item in tree")
986
987 _reset_node_tree_relationship(child_object)
988
989 def remove_from_tree(self) -> None:
990 """Remove the object from the tree it is in."""
991 if "/Parent" not in self:
992 raise ValueError("Removed child does not appear to be a tree item")
993 cast("TreeObject", self["/Parent"]).remove_child(self)
994
995 def empty_tree(self) -> None:
996 for child in self:
997 child_obj = child.get_object()
998 _reset_node_tree_relationship(child_obj)
999
1000 if NameObject("/Count") in self:
1001 del self[NameObject("/Count")]
1002 if NameObject("/First") in self:
1003 del self[NameObject("/First")]
1004 if NameObject("/Last") in self:
1005 del self[NameObject("/Last")]
1006
1007
1008def _reset_node_tree_relationship(child_obj: Any) -> None:
1009 """
1010 Call this after a node has been removed from a tree.
1011
1012 This resets the nodes attributes in respect to that tree.
1013
1014 Args:
1015 child_obj:
1016
1017 """
1018 del child_obj[NameObject("/Parent")]
1019 if NameObject("/Next") in child_obj:
1020 del child_obj[NameObject("/Next")]
1021 if NameObject("/Prev") in child_obj:
1022 del child_obj[NameObject("/Prev")]
1023
1024
1025class _FontFileCharacterMap(NamedTuple):
1026 """The character map pypdf._cmap derived from an embedded font program. See #4156."""
1027
1028 digest: bytes
1029 """SHA-256 digest of the decoded font data the map was derived from."""
1030 map_dict: dict[Any, Any]
1031 int_entry: list[int]
1032
1033
1034class StreamObject(DictionaryObject):
1035 _font_file_character_map: Optional[_FontFileCharacterMap] = None
1036 """
1037 The character map pypdf._cmap derived from this stream as an embedded font program. Parsing a CFF font
1038 program with fontTools is expensive, and the same program is reached from every font resource and page
1039 referencing it. See #4156.
1040 """
1041
1042 def __init__(self) -> None:
1043 self._data: bytes = b""
1044 self.decoded_self: Optional[DecodedStreamObject] = None
1045
1046 def replicate(
1047 self,
1048 pdf_dest: PdfWriterProtocol,
1049 ) -> "StreamObject":
1050 d__ = cast(
1051 "StreamObject",
1052 self._reference_clone(self.__class__(), pdf_dest, False),
1053 )
1054 d__._data = self._data
1055 try:
1056 decoded_self = self.decoded_self
1057 if decoded_self is None:
1058 self.decoded_self = None
1059 else:
1060 self.decoded_self = cast(
1061 "DecodedStreamObject", decoded_self.replicate(pdf_dest)
1062 )
1063 except Exception:
1064 pass
1065 for k, v in self.items():
1066 d__[k.replicate(pdf_dest)] = (
1067 v.replicate(pdf_dest) if hasattr(v, "replicate") else v
1068 )
1069 return d__
1070
1071 def _clone(
1072 self,
1073 src: DictionaryObject,
1074 pdf_dest: PdfWriterProtocol,
1075 force_duplicate: bool,
1076 ignore_fields: Optional[Sequence[Union[str, int]]],
1077 visited: set[tuple[int, int]],
1078 ) -> None:
1079 """
1080 Update the object from src.
1081
1082 Args:
1083 src:
1084 pdf_dest:
1085 force_duplicate:
1086 ignore_fields:
1087
1088 """
1089 self._data = cast("StreamObject", src)._data
1090 try:
1091 decoded_self = cast("StreamObject", src).decoded_self
1092 if decoded_self is None:
1093 self.decoded_self = None
1094 else:
1095 self.decoded_self = cast(
1096 "DecodedStreamObject",
1097 decoded_self.clone(pdf_dest, force_duplicate, ignore_fields),
1098 )
1099 except Exception:
1100 pass
1101 super()._clone(src, pdf_dest, force_duplicate, ignore_fields, visited)
1102
1103 def hash_bin(self) -> int:
1104 """
1105 Used to detect modified object.
1106
1107 Returns:
1108 Hash considering type and value.
1109
1110 """
1111 # Use _data to prevent errors on non-decoded streams.
1112 return hash((super().hash_bin(), self._data))
1113
1114 def get_data(self) -> bytes:
1115 return self._data
1116
1117 def set_data(self, data: bytes) -> None:
1118 self._data = data
1119
1120 def hash_value_data(self) -> bytes:
1121 data = super().hash_value_data()
1122 data += self.get_data()
1123 return data
1124
1125 def write_to_stream(
1126 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
1127 ) -> None:
1128 if encryption_key is not None: # deprecated
1129 deprecation_no_replacement(
1130 "the encryption_key parameter of write_to_stream", "5.0.0"
1131 )
1132 self[NameObject(StreamAttributes.LENGTH)] = NumberObject(len(self._data))
1133 DictionaryObject.write_to_stream(self, stream)
1134 del self[StreamAttributes.LENGTH]
1135 stream.write(b"\nstream\n")
1136 stream.write(self._data)
1137 stream.write(b"\nendstream")
1138
1139 @staticmethod
1140 def initialize_from_dictionary(
1141 data: dict[str, Any]
1142 ) -> Union["EncodedStreamObject", "DecodedStreamObject"]:
1143 retval: Union[EncodedStreamObject, DecodedStreamObject]
1144 if StreamAttributes.FILTER in data:
1145 retval = EncodedStreamObject()
1146 else:
1147 retval = DecodedStreamObject()
1148 retval._data = data["__streamdata__"]
1149 del data["__streamdata__"]
1150 if StreamAttributes.LENGTH in data:
1151 del data[StreamAttributes.LENGTH]
1152 retval.update(data)
1153 return retval
1154
1155 def flate_encode(self, level: int = -1) -> "EncodedStreamObject":
1156 from ..filters import FlateDecode # noqa: PLC0415
1157
1158 if StreamAttributes.FILTER in self:
1159 f = self[StreamAttributes.FILTER]
1160 if isinstance(f, ArrayObject):
1161 f = ArrayObject([NameObject(FT.FLATE_DECODE), *f])
1162 try:
1163 params = ArrayObject(
1164 [NullObject(), *self.get(StreamAttributes.DECODE_PARMS, ArrayObject())]
1165 )
1166 except TypeError:
1167 # case of error where the * operator is not working (not an array
1168 params = ArrayObject(
1169 [NullObject(), self.get(StreamAttributes.DECODE_PARMS, ArrayObject())]
1170 )
1171 else:
1172 f = ArrayObject([NameObject(FT.FLATE_DECODE), f])
1173 params = ArrayObject(
1174 [NullObject(), self.get(StreamAttributes.DECODE_PARMS, NullObject())]
1175 )
1176 else:
1177 f = NameObject(FT.FLATE_DECODE)
1178 params = None
1179 retval = EncodedStreamObject()
1180 retval.update(self)
1181 retval[NameObject(StreamAttributes.FILTER)] = f
1182 if params is not None:
1183 retval[NameObject(StreamAttributes.DECODE_PARMS)] = params
1184 retval._data = FlateDecode.encode(self._data, level)
1185 return retval
1186
1187 def decode_as_image(self, pillow_parameters: Union[dict[str, Any], None] = None) -> Any:
1188 """
1189 Try to decode the stream object as an image
1190
1191 Args:
1192 pillow_parameters: parameters provided to Pillow Image.save() method,
1193 cf. <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save>
1194
1195 Returns:
1196 a PIL image if proper decoding has been found
1197 Raises:
1198 Exception: Errors during decoding will be reported.
1199 It is recommended to catch exceptions to prevent
1200 stops in your program.
1201
1202 """
1203 from ._image_xobject import _xobj_to_image # noqa: PLC0415
1204
1205 if self.get("/Subtype", "") != "/Image":
1206 try:
1207 logger_warning( # pragma: no cover
1208 "%(indirect_reference)s does not seem to be an Image",
1209 source=__name__,
1210 indirect_reference=self.indirect_reference,
1211 )
1212 except AttributeError:
1213 logger_warning( # pragma: no cover
1214 "%(obj)r object does not seem to be an Image",
1215 source=__name__,
1216 obj=self,
1217 )
1218 extension, _, img = _xobj_to_image(self, pillow_parameters)
1219 if extension is None:
1220 return None # pragma: no cover
1221 return img
1222
1223
1224class DecodedStreamObject(StreamObject):
1225 pass
1226
1227
1228class EncodedStreamObject(StreamObject):
1229 def __init__(self) -> None:
1230 self.decoded_self: Optional[DecodedStreamObject] = None
1231
1232 # This overrides the parent method
1233 def get_data(self) -> bytes:
1234 from ..filters import decode_stream_data # noqa: PLC0415
1235
1236 if self.decoded_self is not None:
1237 # Cached version of decoded object
1238 return self.decoded_self.get_data()
1239
1240 # Create decoded object
1241 decoded = DecodedStreamObject()
1242 decoded.set_data(decode_stream_data(self))
1243 for key, value in self.items():
1244 if key not in (StreamAttributes.LENGTH, StreamAttributes.FILTER, StreamAttributes.DECODE_PARMS):
1245 decoded[key] = value
1246 self.decoded_self = decoded
1247 return decoded.get_data()
1248
1249 # This overrides the parent method:
1250 def set_data(self, data: bytes) -> None:
1251 from ..filters import FlateDecode # noqa: PLC0415
1252
1253 if self.get(StreamAttributes.FILTER, "") in (FT.FLATE_DECODE, [FT.FLATE_DECODE]):
1254 if not isinstance(data, bytes):
1255 raise TypeError("Data must be bytes")
1256 if self.decoded_self is None:
1257 self.get_data() # to create self.decoded_self
1258 assert self.decoded_self is not None, "mypy"
1259 self.decoded_self.set_data(data)
1260 super().set_data(FlateDecode.encode(data))
1261 else:
1262 raise PdfReadError(
1263 "Streams encoded with a filter different from FlateDecode are not supported"
1264 )
1265
1266
1267CONTENT_STREAM_ARRAY_MAX_LENGTH = 10_000
1268
1269
1270class ContentStream(DecodedStreamObject):
1271 """
1272 In order to be fast, this data structure can contain either:
1273
1274 * raw data in ._data
1275 * parsed stream operations in ._operations.
1276
1277 At any time, ContentStream object can either have both of those fields defined,
1278 or one field defined and the other set to None.
1279
1280 These fields are "rebuilt" lazily, when accessed:
1281
1282 * when .get_data() is called, if ._data is None, it is rebuilt from ._operations.
1283 * when .operations is called, if ._operations is None, it is rebuilt from ._data.
1284
1285 Conversely, these fields can be invalidated:
1286
1287 * when .set_data() is called, ._operations is set to None.
1288 * when .operations is set, ._data is set to None.
1289 """
1290 _OPERATOR_LENGTH_LIMIT = 128
1291
1292 def __init__(
1293 self,
1294 stream: Any,
1295 pdf: Any,
1296 forced_encoding: Union[str, list[str], dict[int, str], None] = None,
1297 ) -> None:
1298 self.pdf = pdf
1299 self._operations: list[tuple[Any, bytes]] = []
1300
1301 # stream may be a StreamObject or an ArrayObject containing
1302 # StreamObjects to be concatenated together.
1303 if stream is None:
1304 super().set_data(b"")
1305 else:
1306 stream = stream.get_object()
1307 if isinstance(stream, ArrayObject):
1308 from pypdf._configuration import get_configuration # noqa: PLC0415
1309 configuration = get_configuration()
1310
1311 if (stream_length := len(stream)) > CONTENT_STREAM_ARRAY_MAX_LENGTH:
1312 raise LimitReachedError(
1313 f"Array-based stream has {stream_length} > {CONTENT_STREAM_ARRAY_MAX_LENGTH} elements."
1314 )
1315 data = bytearray()
1316 length = 0
1317 for s in stream:
1318 s_resolved = s.get_object()
1319 if isinstance(s_resolved, NullObject):
1320 continue
1321 if not isinstance(s_resolved, StreamObject):
1322 # No need to emit an exception here for now - the PDF structure
1323 # seems to already be broken beforehand in these cases.
1324 logger_warning(
1325 "Expected StreamObject, got %(type_name)s instead. Data might be wrong.",
1326 source=__name__,
1327 type_name=type(s_resolved).__name__,
1328 )
1329 else:
1330 new_data = s_resolved.get_data()
1331 length += len(new_data)
1332 if length > configuration.array_based_stream_maximum_output_length:
1333 raise LimitReachedError(
1334 f"Array-based stream has at least {length} > "
1335 f"{configuration.array_based_stream_maximum_output_length} output bytes."
1336 )
1337 data += new_data
1338 if len(data) == 0 or data[-1:] != b"\n":
1339 # There should be no direct need to check for a change of one byte.
1340 length += 1
1341 data += b"\n"
1342 super().set_data(bytes(data))
1343 else:
1344 stream_data = stream.get_data()
1345 assert stream_data is not None
1346 super().set_data(stream_data)
1347 self.forced_encoding = forced_encoding
1348
1349 def replicate(
1350 self,
1351 pdf_dest: PdfWriterProtocol,
1352 ) -> "ContentStream":
1353 d__ = cast(
1354 "ContentStream",
1355 self._reference_clone(self.__class__(None, None), pdf_dest, False),
1356 )
1357 d__._data = self._data
1358 try:
1359 decoded_self = self.decoded_self
1360 if decoded_self is None:
1361 self.decoded_self = None
1362 else:
1363 self.decoded_self = cast(
1364 "DecodedStreamObject", decoded_self.replicate(pdf_dest)
1365 )
1366 except Exception:
1367 pass
1368 for k, v in self.items():
1369 d__[k.replicate(pdf_dest)] = (
1370 v.replicate(pdf_dest) if hasattr(v, "replicate") else v
1371 )
1372 return d__
1373 d__.set_data(self._data)
1374 d__.pdf = pdf_dest
1375 d__._operations = list(self._operations)
1376 d__.forced_encoding = self.forced_encoding
1377 return d__
1378
1379 def clone(
1380 self,
1381 pdf_dest: Any,
1382 force_duplicate: bool = False,
1383 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
1384 ) -> "ContentStream":
1385 """
1386 Clone object into pdf_dest.
1387
1388 Args:
1389 pdf_dest:
1390 force_duplicate:
1391 ignore_fields:
1392
1393 Returns:
1394 The cloned ContentStream
1395
1396 """
1397 try:
1398 if self.indirect_reference.pdf == pdf_dest and not force_duplicate: # type: ignore[union-attr]
1399 return self
1400 except Exception:
1401 pass
1402
1403 visited: set[tuple[int, int]] = set()
1404 d__ = cast(
1405 "ContentStream",
1406 self._reference_clone(
1407 self.__class__(None, None), pdf_dest, force_duplicate
1408 ),
1409 )
1410 if ignore_fields is None:
1411 ignore_fields = []
1412 d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited)
1413 return d__
1414
1415 def _clone(
1416 self,
1417 src: DictionaryObject,
1418 pdf_dest: PdfWriterProtocol,
1419 force_duplicate: bool,
1420 ignore_fields: Optional[Sequence[Union[str, int]]],
1421 visited: set[tuple[int, int]],
1422 ) -> None:
1423 """
1424 Update the object from src.
1425
1426 Args:
1427 src:
1428 pdf_dest:
1429 force_duplicate:
1430 ignore_fields:
1431
1432 """
1433 src_cs = cast("ContentStream", src)
1434 super().set_data(src_cs._data)
1435 self.pdf = pdf_dest
1436 self._operations = list(src_cs._operations)
1437 self.forced_encoding = src_cs.forced_encoding
1438 # no need to call DictionaryObjection or anything
1439 # like super(DictionaryObject,self)._clone(src, pdf_dest, force_duplicate, ignore_fields, visited)
1440
1441 @staticmethod
1442 def _check_limits(
1443 *,
1444 remaining_elements: Optional[int] = None,
1445 parsing_budget: StreamParsingBudget,
1446 token_decrement: int
1447 ) -> Optional[int]:
1448 if remaining_elements is not None:
1449 remaining_elements -= 1
1450 if remaining_elements < 0:
1451 raise LimitReachedError("Reached maximum number of tokens.")
1452 parsing_budget.remaining_budget -= token_decrement
1453 if parsing_budget.remaining_budget < 0:
1454 raise LimitReachedError("Reached maximum number of tokens per stream.")
1455 return remaining_elements
1456
1457 def _parse_content_stream(self, stream: StreamType) -> None:
1458 # 7.8.2 Content Streams
1459 stream.seek(0, 0)
1460 operands: list[Union[int, str, PdfObject]] = []
1461 parsing_budget = StreamParsingBudget(remaining_budget=get_configuration().stream_parsing_work_maximum_count)
1462 while True:
1463 peek = read_non_whitespace(stream)
1464 if peek in (b"", 0):
1465 break
1466 stream.seek(-1, 1)
1467 if peek.isalpha() or peek in (b"'", b'"'):
1468 operator = read_until_regex(
1469 stream=stream, regex=NameObject.delimiter_pattern, length=self._OPERATOR_LENGTH_LIMIT
1470 )
1471 if operator == b"BI":
1472 # begin inline image - a completely different parsing
1473 # mechanism is required, of course... thanks buddy...
1474 assert operands == []
1475 self._check_limits(parsing_budget=parsing_budget, token_decrement=2)
1476 ii = self._read_inline_image(stream, parsing_budget=parsing_budget)
1477 self._operations.append((ii, b"INLINE IMAGE"))
1478 else:
1479 self._check_limits(parsing_budget=parsing_budget, token_decrement=1)
1480 self._operations.append((operands, operator))
1481 operands = []
1482 elif peek == b"%":
1483 # If we encounter a comment in the content stream, we have to
1484 # handle it here. Typically, read_object will handle
1485 # encountering a comment -- but read_object assumes that
1486 # following the comment must be the object we're trying to
1487 # read. In this case, it could be an operator instead.
1488 while peek not in (b"\r", b"\n", b""):
1489 peek = stream.read(1)
1490 else:
1491 self._check_limits(parsing_budget=parsing_budget, token_decrement=1)
1492 operands.append(read_object(stream, None, self.forced_encoding, parsing_budget=parsing_budget))
1493
1494 def _read_inline_image(self, stream: StreamType, parsing_budget: StreamParsingBudget) -> dict[str, Any]:
1495 # begin reading just after the "BI" - begin image
1496 # first read the dictionary of settings.
1497 settings = DictionaryObject()
1498 remaining_count: Optional[int] = get_configuration().dictionary_maximum_element_count
1499 while True:
1500 tok = read_non_whitespace(stream)
1501 if not tok:
1502 raise PdfReadError("Unexpected end of stream.")
1503 stream.seek(-1, 1)
1504 if tok == b"I":
1505 # "ID" - begin of image data
1506 break
1507 remaining_count = self._check_limits(
1508 remaining_elements=remaining_count, parsing_budget=parsing_budget, token_decrement=2
1509 )
1510
1511 key = read_object(stream, self.pdf, parsing_budget=parsing_budget)
1512 _ = read_non_whitespace(stream)
1513 stream.seek(-1, 1)
1514 value = read_object(stream, self.pdf, parsing_budget=parsing_budget)
1515 settings[key] = value
1516 # left at beginning of ID
1517 tmp = stream.read(3)
1518 assert tmp[:2] == b"ID"
1519 filtr = settings.get("/F", settings.get("/Filter", "not set"))
1520 savpos = stream.tell()
1521 if isinstance(filtr, list):
1522 filtr = filtr[0] if filtr else "not set" # used forencoding
1523 if not isinstance(filtr, str):
1524 # A valid filter is a name or an array of names; anything else
1525 # cannot match the abbreviations below, so treat it as unfiltered.
1526 filtr = "not set"
1527 if "AHx" in filtr or "ASCIIHexDecode" in filtr:
1528 data = extract_inline__ascii_hex_decode(stream)
1529 elif "A85" in filtr or "ASCII85Decode" in filtr:
1530 data = extract_inline__ascii85_decode(stream)
1531 elif "RL" in filtr or "RunLengthDecode" in filtr:
1532 data = extract_inline__run_length_decode(stream)
1533 elif "DCT" in filtr or "DCTDecode" in filtr:
1534 data = extract_inline__dct_decode(stream)
1535 elif filtr == "not set":
1536 cs = settings.get("/CS", "")
1537 if isinstance(cs, list):
1538 cs = cs[0] if cs else ""
1539 if not isinstance(cs, str):
1540 cs = ""
1541 if "RGB" in cs:
1542 lcs: float = 3
1543 elif "CMYK" in cs:
1544 lcs = 4
1545 else:
1546 bits = settings.get(
1547 "/BPC",
1548 8 if cs in {"/I", "/G", "/Indexed", "/DeviceGray"} else -1,
1549 )
1550 if isinstance(bits, (int, float)) and bits > 0:
1551 lcs = bits / 8.0
1552 else:
1553 data = extract_inline_default(stream)
1554 lcs = -1
1555 width = settings.get("/W")
1556 height = settings.get("/H")
1557 if lcs > 0 and isinstance(width, int) and isinstance(height, int):
1558 data = stream.read(ceil(width * lcs) * height)
1559 elif lcs > 0:
1560 # Without usable dimensions the raw sample length is unknown;
1561 # fall back to scanning for the `EI` marker.
1562 data = extract_inline_default(stream)
1563 # Move to the `EI` if possible.
1564 ei = read_non_whitespace(stream)
1565 stream.seek(-1, 1)
1566 else:
1567 data = extract_inline_default(stream)
1568
1569 ei = stream.read(3)
1570 # An `EI` at the very end of the stream yields only two bytes; rewinding
1571 # unconditionally would step back into the marker (#3468).
1572 if len(ei) == 3:
1573 stream.seek(-1, 1)
1574 ei_trailing = ei[2:3]
1575 if ei[:2] != b"EI" or (ei_trailing != b"" and ei_trailing not in WHITESPACES):
1576 # Deal with wrong/missing `EI` tags. Example: Wrong dimensions specified above.
1577 stream.seek(savpos, 0)
1578 data = extract_inline_default(stream)
1579 ei = stream.read(3)
1580 if len(ei) == 3:
1581 stream.seek(-1, 1)
1582 ei_trailing = ei[2:3]
1583 if ei[:2] != b"EI" or (
1584 ei_trailing != b"" and ei_trailing not in WHITESPACES
1585 ): # pragma: no cover
1586 # Check the same condition again. This should never fail as
1587 # edge cases are covered by `extract_inline_default` above,
1588 # but check this to make sure that we are behind the `EI` afterwards.
1589 raise PdfStreamError(
1590 f"Could not extract inline image, even using fallback. Expected 'EI', got {ei!r}"
1591 )
1592 return {"settings": settings, "data": data}
1593
1594 # This overrides the parent method
1595 def get_data(self) -> bytes:
1596 if not self._data:
1597 new_data = BytesIO()
1598 for operands, operator in self._operations:
1599 if operator == b"INLINE IMAGE":
1600 new_data.write(b"BI")
1601 dict_text = BytesIO()
1602 operands["settings"].write_to_stream(dict_text)
1603 new_data.write(dict_text.getvalue()[2:-2])
1604 new_data.write(b"ID ")
1605 new_data.write(operands["data"])
1606 new_data.write(b"EI")
1607 else:
1608 for op in operands:
1609 op.write_to_stream(new_data)
1610 new_data.write(b" ")
1611 new_data.write(operator)
1612 new_data.write(b"\n")
1613 self._data = new_data.getvalue()
1614 return self._data
1615
1616 # This overrides the parent method
1617 def set_data(self, data: bytes) -> None:
1618 super().set_data(data)
1619 self._operations = []
1620
1621 @property
1622 def operations(self) -> list[tuple[Any, bytes]]:
1623 if not self._operations and self._data:
1624 self._parse_content_stream(BytesIO(self._data))
1625 self._data = b""
1626 return self._operations
1627
1628 @operations.setter
1629 def operations(self, operations: list[tuple[Any, bytes]]) -> None:
1630 self._operations = operations
1631 self._data = b""
1632
1633 def isolate_graphics_state(self) -> None:
1634 if self._operations:
1635 self._operations.insert(0, ([], b"q"))
1636 self._operations.append(([], b"Q"))
1637 elif self._data:
1638 self._data = b"q\n" + self._data + b"\nQ\n"
1639
1640 # This overrides the parent method
1641 def write_to_stream(
1642 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
1643 ) -> None:
1644 if not self._data and self._operations:
1645 self.get_data() # this ensures ._data is rebuilt
1646 super().write_to_stream(stream, encryption_key)
1647
1648
1649# Bound comment scanning without consuming Python recursion depth.
1650_MAX_LEADING_COMMENTS = 1000
1651
1652
1653def read_object( # noqa: PLR0911
1654 stream: StreamType,
1655 pdf: Optional[PdfReaderProtocol],
1656 forced_encoding: Union[str, list[str], dict[int, str], None] = None,
1657 *,
1658 parsing_budget: Optional[StreamParsingBudget] = None,
1659) -> PdfObject:
1660 """
1661 Read the next object from the stream.
1662
1663 Args:
1664 stream: The stream to read from.
1665 pdf: The corresponding PDF file.
1666 forced_encoding: The encoding to enforce, if any.
1667 parsing_budget: The allowed parsing budget, optional. If set, only applies to dictionaries,
1668 arrays and content streams.
1669
1670 Returns:
1671 The parsed object.
1672 """
1673 tok = stream.read(1)
1674 stream.seek(-1, 1) # reset to start
1675 if tok == b"%":
1676 # Skip consecutive comments in a loop instead of recursing per comment.
1677 skip_over_comments(stream, limit=_MAX_LEADING_COMMENTS)
1678 tok = stream.read(1)
1679 stream.seek(-1, 1) # reset to start
1680 if tok == b"/":
1681 return NameObject.read_from_stream(stream, pdf)
1682 if tok == b"<":
1683 # hexadecimal string OR dictionary
1684 peek = stream.read(2)
1685 stream.seek(-2, 1) # reset to start
1686 if peek == b"<<":
1687 return DictionaryObject.read_from_stream(stream, pdf, forced_encoding, parsing_budget=parsing_budget)
1688 return read_hex_string_from_stream(stream, forced_encoding)
1689 if tok == b"[":
1690 return ArrayObject.read_from_stream(stream, pdf, forced_encoding, parsing_budget=parsing_budget)
1691 if tok in (b"t", b"f"):
1692 return BooleanObject.read_from_stream(stream)
1693 if tok == b"(":
1694 return read_string_from_stream(stream, forced_encoding)
1695 if tok == b"e" and stream.read(6) == b"endobj":
1696 return NullObject()
1697 if tok == b"n":
1698 return NullObject.read_from_stream(stream)
1699 if tok in b"0123456789+-.":
1700 # number object OR indirect reference
1701 peek = stream.read(20)
1702 match = IndirectPattern.match(peek)
1703 if match is not None:
1704 assert pdf is not None, "mypy"
1705 # Continue after the "R", the match includes the character following it
1706 stream.seek(match.end() - 1 - len(peek), 1)
1707 # The object number includes an optional sign
1708 return IndirectObject(int(peek[: match.end(1)]), int(match[2]), pdf)
1709 number_end = NumberObject.NumberPattern.search(peek)
1710 if number_end is not None:
1711 # The whole number is in the peek, there is no need to read it again
1712 num = peek[: number_end.start()]
1713 stream.seek(len(num) - len(peek), 1)
1714 if b"." in num:
1715 return FloatObject(num)
1716 return NumberObject(num)
1717 stream.seek(-len(peek), 1) # reset to start
1718 return NumberObject.read_from_stream(stream)
1719 pos = stream.tell()
1720 stream.seek(-20, 1)
1721 stream_extract = stream.read(80)
1722 stream.seek(pos)
1723 read_until_whitespace(stream)
1724 raise PdfReadError(
1725 f"Invalid Elementary Object starting with {tok!r} @{pos}: {stream_extract!r}"
1726 )
1727
1728
1729class Field(TreeObject):
1730 """
1731 A class representing a field dictionary.
1732
1733 This class is accessed through
1734 :meth:`get_fields()<pypdf.PdfReader.get_fields>`
1735 """
1736
1737 def __init__(self, data: DictionaryObject) -> None:
1738 DictionaryObject.__init__(self)
1739 field_attributes = (
1740 FieldDictionaryAttributes.attributes()
1741 + CheckboxRadioButtonAttributes.attributes()
1742 )
1743 self.indirect_reference = data.indirect_reference
1744 for attr in field_attributes:
1745 try:
1746 self[NameObject(attr)] = data[attr]
1747 except KeyError:
1748 pass
1749 if isinstance(self.get("/V"), EncodedStreamObject):
1750 d = cast(EncodedStreamObject, self[NameObject("/V")]).get_data()
1751 if isinstance(d, bytes):
1752 d_str = d.decode()
1753 elif d is None:
1754 d_str = ""
1755 else:
1756 raise Exception("Should never happen")
1757 self[NameObject("/V")] = TextStringObject(d_str)
1758
1759 # TABLE 8.69 Entries common to all field dictionaries
1760 @property
1761 def field_type(self) -> Optional[NameObject]:
1762 """Read-only property accessing the type of this field."""
1763 return self.get(FieldDictionaryAttributes.FT)
1764
1765 @property
1766 def parent(self) -> Optional[DictionaryObject]:
1767 """Read-only property accessing the parent of this field."""
1768 return self.get(FieldDictionaryAttributes.Parent)
1769
1770 @property
1771 def kids(self) -> Optional["ArrayObject"]:
1772 """Read-only property accessing the kids of this field."""
1773 return self.get(FieldDictionaryAttributes.Kids)
1774
1775 @property
1776 def name(self) -> Optional[str]:
1777 """Read-only property accessing the name of this field."""
1778 return self.get(FieldDictionaryAttributes.T)
1779
1780 @property
1781 def alternate_name(self) -> Optional[str]:
1782 """Read-only property accessing the alternate name of this field."""
1783 return self.get(FieldDictionaryAttributes.TU)
1784
1785 @property
1786 def mapping_name(self) -> Optional[str]:
1787 """
1788 Read-only property accessing the mapping name of this field.
1789
1790 This name is used by pypdf as a key in the dictionary returned by
1791 :meth:`get_fields()<pypdf.PdfReader.get_fields>`
1792 """
1793 return self.get(FieldDictionaryAttributes.TM)
1794
1795 @property
1796 def flags(self) -> Optional[int]:
1797 """
1798 Read-only property accessing the field flags, specifying various
1799 characteristics of the field (see Table 8.70 of the PDF 1.7 reference).
1800 """
1801 return self.get(FieldDictionaryAttributes.Ff)
1802
1803 @property
1804 def value(self) -> Optional[Any]:
1805 """
1806 Read-only property accessing the value of this field.
1807
1808 Format varies based on field type.
1809 """
1810 return self.get(FieldDictionaryAttributes.V)
1811
1812 @property
1813 def default_value(self) -> Optional[Any]:
1814 """Read-only property accessing the default value of this field."""
1815 return self.get(FieldDictionaryAttributes.DV)
1816
1817 @property
1818 def additional_actions(self) -> Optional[DictionaryObject]:
1819 """
1820 Read-only property accessing the additional actions dictionary.
1821
1822 This dictionary defines the field's behavior in response to trigger
1823 events. See Section 8.5.2 of the PDF 1.7 reference.
1824 """
1825 return self.get(FieldDictionaryAttributes.AA)
1826
1827
1828class Destination(TreeObject):
1829 """
1830 A class representing a destination within a PDF file.
1831
1832 See section 12.3.2 of the PDF 2.0 reference.
1833
1834 Args:
1835 title: Title of this destination.
1836 page: Reference to the page of this destination. Should
1837 be an instance of :class:`IndirectObject<pypdf.generic.IndirectObject>`.
1838 fit: How the destination is displayed.
1839
1840 Raises:
1841 PdfReadError: If destination type is invalid.
1842
1843 """
1844
1845 node: Optional[
1846 DictionaryObject
1847 ] = None # node provide access to the original Object
1848
1849 def remove_from_tree(self) -> None:
1850 """
1851 Remove the outline item this destination was built from.
1852
1853 `reader.outline` and `writer.outline` yield detached copies rather than
1854 the nodes themselves, so the copy never has a `/Parent` and removing it
1855 would be a no-op. `node` is the dictionary in the document.
1856 """
1857 if self.node is None:
1858 super().remove_from_tree()
1859 elif "/Parent" not in self.node:
1860 raise ValueError("Removed child does not appear to be a tree item")
1861 else:
1862 TreeObject.remove_child(
1863 cast("TreeObject", self.node["/Parent"]), self.node
1864 )
1865
1866 def __init__(
1867 self,
1868 title: Union[str, bytes],
1869 page: Union[NumberObject, IndirectObject, NullObject, DictionaryObject],
1870 fit: Fit,
1871 ) -> None:
1872 self._filtered_children: list[Any] = [] # used in PdfWriter
1873
1874 typ = fit.fit_type
1875 args = fit.fit_args
1876
1877 DictionaryObject.__init__(self)
1878 self[NameObject("/Title")] = TextStringObject(title)
1879 self[NameObject("/Page")] = page
1880 self[NameObject("/Type")] = typ
1881
1882 # from table 8.2 of the PDF 1.7 reference.
1883 if typ == "/XYZ":
1884 if len(args) < 1: # left is missing : should never occur
1885 args.append(NumberObject(0.0))
1886 if len(args) < 2: # top is missing
1887 args.append(NumberObject(0.0))
1888 if len(args) < 3: # zoom is missing
1889 args.append(NumberObject(0.0))
1890 # surplus arguments are ignored rather than failing the unpacking
1891 (
1892 self[NameObject(TA.LEFT)],
1893 self[NameObject(TA.TOP)],
1894 self[NameObject("/Zoom")],
1895 ) = args[:3]
1896 elif len(args) == 0:
1897 pass
1898 elif typ == TF.FIT_R:
1899 if len(args) == 4: # a wrong number of arguments degrades to null
1900 (
1901 self[NameObject(TA.LEFT)],
1902 self[NameObject(TA.BOTTOM)],
1903 self[NameObject(TA.RIGHT)],
1904 self[NameObject(TA.TOP)],
1905 ) = args
1906 else:
1907 (
1908 self[NameObject(TA.LEFT)],
1909 self[NameObject(TA.BOTTOM)],
1910 self[NameObject(TA.RIGHT)],
1911 self[NameObject(TA.TOP)],
1912 ) = (NullObject(), NullObject(), NullObject(), NullObject())
1913 elif typ in [TF.FIT_H, TF.FIT_BH]:
1914 try: # Prefer to be more robust not only to null parameters
1915 (self[NameObject(TA.TOP)],) = args
1916 except Exception:
1917 (self[NameObject(TA.TOP)],) = (NullObject(),)
1918 elif typ in [TF.FIT_V, TF.FIT_BV]:
1919 try: # Prefer to be more robust not only to null parameters
1920 (self[NameObject(TA.LEFT)],) = args
1921 except Exception:
1922 (self[NameObject(TA.LEFT)],) = (NullObject(),)
1923 elif typ in [TF.FIT, TF.FIT_B]:
1924 pass
1925 else:
1926 raise PdfReadError(f"Unknown Destination Type: {typ!r}")
1927
1928 @property
1929 def dest_array(self) -> "ArrayObject":
1930 return ArrayObject(
1931 [self.raw_get("/Page"), self["/Type"]]
1932 + [
1933 self[x]
1934 for x in ["/Left", "/Bottom", "/Right", "/Top", "/Zoom"]
1935 if x in self
1936 ]
1937 )
1938
1939 def write_to_stream(
1940 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
1941 ) -> None:
1942 if encryption_key is not None: # deprecated
1943 deprecation_no_replacement(
1944 "the encryption_key parameter of write_to_stream", "5.0.0"
1945 )
1946 stream.write(b"<<\n")
1947 key = NameObject("/D")
1948 key.write_to_stream(stream)
1949 stream.write(b" ")
1950 value = self.dest_array
1951 value.write_to_stream(stream)
1952
1953 key = NameObject("/S")
1954 key.write_to_stream(stream)
1955 stream.write(b" ")
1956 value_s = NameObject("/GoTo")
1957 value_s.write_to_stream(stream)
1958
1959 stream.write(b"\n")
1960 stream.write(b">>")
1961
1962 @property
1963 def title(self) -> Optional[str]:
1964 """Read-only property accessing the destination title."""
1965 return self.get("/Title")
1966
1967 @property
1968 def page(self) -> Optional[IndirectObject]:
1969 """Read-only property accessing the IndirectObject of the destination page."""
1970 return self.get("/Page")
1971
1972 @property
1973 def typ(self) -> Optional[str]:
1974 """Read-only property accessing the destination type."""
1975 return self.get("/Type")
1976
1977 @property
1978 def zoom(self) -> Optional[int]:
1979 """Read-only property accessing the zoom factor."""
1980 return self.get("/Zoom", None)
1981
1982 @property
1983 def left(self) -> Optional[FloatObject]:
1984 """Read-only property accessing the left horizontal coordinate."""
1985 return self.get("/Left", None)
1986
1987 @property
1988 def right(self) -> Optional[FloatObject]:
1989 """Read-only property accessing the right horizontal coordinate."""
1990 return self.get("/Right", None)
1991
1992 @property
1993 def top(self) -> Optional[FloatObject]:
1994 """Read-only property accessing the top vertical coordinate."""
1995 return self.get("/Top", None)
1996
1997 @property
1998 def bottom(self) -> Optional[FloatObject]:
1999 """Read-only property accessing the bottom vertical coordinate."""
2000 return self.get("/Bottom", None)
2001
2002 @property
2003 def color(self) -> Optional["ArrayObject"]:
2004 """Read-only property accessing the color in (R, G, B) with values 0.0-1.0."""
2005 return cast(
2006 "ArrayObject",
2007 self.get("/C", ArrayObject([FloatObject(0), FloatObject(0), FloatObject(0)])),
2008 )
2009
2010 @property
2011 def font_format(self) -> Optional[OutlineFontFlag]:
2012 """
2013 Read-only property accessing the font type.
2014
2015 1=italic, 2=bold, 3=both
2016 """
2017 return OutlineFontFlag(self.get("/F", 0))
2018
2019 @property
2020 def outline_count(self) -> Optional[int]:
2021 """
2022 Read-only property accessing the outline count.
2023
2024 positive = expanded
2025 negative = collapsed
2026 absolute value = number of visible descendants at all levels
2027 """
2028 return self.get("/Count", None)