Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/generic/_base.py: 47%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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.
27import binascii
28import codecs
29import hashlib
30import re
31import sys
32from collections.abc import Sequence
33from math import log10
34from struct import iter_unpack
35from typing import Any, Callable, ClassVar, Optional, Union, cast
37if sys.version_info[:2] >= (3, 10):
38 from typing import TypeGuard
39else:
40 from typing_extensions import TypeGuard # PEP 647
42if sys.version_info >= (3, 11):
43 from typing import Self
44else:
45 from typing_extensions import Self
47from .._codecs import _pdfdoc_encoding_rev
48from .._protocols import PdfWriterProtocol
49from .._utils import (
50 WHITESPACES,
51 StreamType,
52 classproperty,
53 deprecation_no_replacement,
54 deprecation_with_replacement,
55 logger_warning,
56 read_non_whitespace,
57 read_until_regex,
58)
59from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError
61__author__ = "Mathieu Fenniak"
62__author_email__ = "biziqe@mathieu.fenniak.net"
65class PdfObject:
66 # function for calculating a hash value
67 hash_func: Callable[..., "hashlib._Hash"] = hashlib.sha1
68 indirect_reference: Optional["IndirectObject"]
70 def hash_bin(self) -> int:
71 """
72 Used to detect modified object.
74 Returns:
75 Hash considering type and value.
77 """
78 raise NotImplementedError(
79 f"{self.__class__.__name__} does not implement .hash_bin() so far"
80 )
82 def hash_value_data(self) -> bytes:
83 return f"{self}".encode()
85 def hash_value(self) -> bytes:
86 return (
87 f"{self.__class__.__name__}:"
88 f"{self.hash_func(self.hash_value_data()).hexdigest()}"
89 ).encode()
91 def replicate(
92 self,
93 pdf_dest: PdfWriterProtocol,
94 ) -> "PdfObject":
95 """
96 Clone object into pdf_dest (PdfWriterProtocol which is an interface for PdfWriter)
97 without ensuring links. This is used in clone_document_from_root with incremental = True.
99 Args:
100 pdf_dest: Target to clone to.
102 Returns:
103 The cloned PdfObject
105 """
106 return self.clone(pdf_dest)
108 def clone(
109 self,
110 pdf_dest: PdfWriterProtocol,
111 force_duplicate: bool = False,
112 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
113 ) -> "PdfObject":
114 """
115 Clone object into pdf_dest (PdfWriterProtocol which is an interface for PdfWriter).
117 By default, this method will call ``_reference_clone`` (see ``_reference``).
120 Args:
121 pdf_dest: Target to clone to.
122 force_duplicate: By default, if the object has already been cloned and referenced,
123 the copy will be returned; when ``True``, a new copy will be created.
124 (Default value = ``False``)
125 ignore_fields: List/tuple of field names (for dictionaries) that will be ignored
126 during cloning (applies to children duplication as well). If fields are to be
127 considered for a limited number of levels, you have to add it as integer, for
128 example ``[1,"/B","/TOTO"]`` means that ``"/B"`` will be ignored at the first
129 level only but ``"/TOTO"`` on all levels.
131 Returns:
132 The cloned PdfObject
134 """
135 raise NotImplementedError(
136 f"{self.__class__.__name__} does not implement .clone so far"
137 )
139 def _reference_clone(
140 self, clone: "PdfObject", pdf_dest: PdfWriterProtocol, force_duplicate: bool = False
141 ) -> "PdfObject":
142 """
143 Reference the object within the _objects of pdf_dest only if
144 indirect_reference attribute exists (which means the objects was
145 already identified in xref/xobjstm) if object has been already
146 referenced do nothing.
148 Args:
149 clone:
150 pdf_dest:
152 Returns:
153 The clone
155 """
156 if not hasattr(self, "indirect_reference"):
157 # Direct object: nothing to reference.
158 # Returning early is much faster than handling the exceptions below.
159 return clone
160 try:
161 if (
162 not force_duplicate
163 and clone.indirect_reference is not None
164 and clone.indirect_reference.pdf == pdf_dest
165 ):
166 return clone
167 except Exception:
168 pass
169 ind = self.indirect_reference
170 if (
171 pdf_dest.incremental
172 and ind is not None
173 and ind.pdf == pdf_dest._reader
174 and ind.idnum <= len(pdf_dest._objects)
175 ):
176 i = ind.idnum
177 else:
178 i = len(pdf_dest._objects) + 1
179 if ind is not None:
180 if id(ind.pdf) not in pdf_dest._id_translated:
181 pdf_dest._id_translated[id(ind.pdf)] = {}
182 pdf_dest._id_translated[id(ind.pdf)]["PreventGC"] = ind.pdf
183 if (
184 not force_duplicate
185 and ind.idnum in pdf_dest._id_translated[id(ind.pdf)]
186 ):
187 obj = pdf_dest.get_object(
188 pdf_dest._id_translated[id(ind.pdf)][ind.idnum]
189 )
190 assert isinstance(obj, PdfObject), "mypy"
191 return obj
192 pdf_dest._id_translated[id(ind.pdf)][ind.idnum] = i
193 try:
194 pdf_dest._objects[i - 1] = clone
195 except IndexError:
196 pdf_dest._objects.append(clone)
197 i = len(pdf_dest._objects)
198 clone.indirect_reference = IndirectObject(i, 0, pdf_dest)
199 return clone
201 def get_object(self) -> Optional["PdfObject"]:
202 """Resolve indirect references."""
203 return self
205 def write_to_stream(
206 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
207 ) -> None:
208 raise NotImplementedError
211class NullObject(PdfObject):
212 def clone(
213 self,
214 pdf_dest: PdfWriterProtocol,
215 force_duplicate: bool = False,
216 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
217 ) -> "NullObject":
218 """Clone object into pdf_dest."""
219 if not hasattr(self, "indirect_reference"):
220 # Direct object: shortcut, as some arrays consist mostly of nulls
221 return NullObject()
222 return cast(
223 "NullObject", self._reference_clone(NullObject(), pdf_dest, force_duplicate)
224 )
226 def hash_bin(self) -> int:
227 """
228 Used to detect modified object.
230 Returns:
231 Hash considering type and value.
233 """
234 return hash((self.__class__,))
236 def write_to_stream(
237 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
238 ) -> None:
239 if encryption_key is not None: # deprecated
240 deprecation_no_replacement(
241 "the encryption_key parameter of write_to_stream", "5.0.0"
242 )
243 stream.write(b"null")
245 @staticmethod
246 def read_from_stream(stream: StreamType) -> "NullObject":
247 nulltxt = stream.read(4)
248 if nulltxt != b"null":
249 raise PdfReadError("Could not read Null object")
250 return NullObject()
252 def __repr__(self) -> str:
253 return "NullObject"
255 def __eq__(self, other: object) -> bool:
256 return isinstance(other, NullObject)
258 def __hash__(self) -> int:
259 return self.hash_bin()
262class BooleanObject(PdfObject):
263 value: bool
265 def __init__(self, value: Any) -> None:
266 self.value = value
268 def clone(
269 self,
270 pdf_dest: PdfWriterProtocol,
271 force_duplicate: bool = False,
272 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
273 ) -> "BooleanObject":
274 """Clone object into pdf_dest."""
275 return cast(
276 "BooleanObject",
277 self._reference_clone(BooleanObject(self.value), pdf_dest, force_duplicate),
278 )
280 def hash_bin(self) -> int:
281 """
282 Used to detect modified object.
284 Returns:
285 Hash considering type and value.
287 """
288 return hash((self.__class__, self.value))
290 def __eq__(self, o: object, /) -> bool:
291 if isinstance(o, BooleanObject):
292 return self.value == o.value
293 if isinstance(o, bool):
294 return self.value == o
295 return False
297 def __hash__(self) -> int:
298 return self.hash_bin()
300 def __repr__(self) -> str:
301 return "True" if self.value else "False"
303 def write_to_stream(
304 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
305 ) -> None:
306 if encryption_key is not None: # deprecated
307 deprecation_no_replacement(
308 "the encryption_key parameter of write_to_stream", "5.0.0"
309 )
310 if self.value:
311 stream.write(b"true")
312 else:
313 stream.write(b"false")
315 @staticmethod
316 def read_from_stream(stream: StreamType) -> "BooleanObject":
317 word = stream.read(4)
318 if word == b"true":
319 return BooleanObject(True)
320 if word == b"fals":
321 stream.read(1)
322 return BooleanObject(False)
323 raise PdfReadError("Could not read Boolean object")
326class IndirectObject(PdfObject):
327 _MAXIMUM_PART_LENGTH = 64
329 def __init__(self, idnum: int, generation: int, pdf: Any) -> None: # PdfReader
330 self.idnum = idnum
331 self.generation = generation
332 self.pdf = pdf
334 def __hash__(self) -> int:
335 return hash((self.idnum, self.generation, id(self.pdf)))
337 def hash_bin(self) -> int:
338 """
339 Used to detect modified object.
341 Returns:
342 Hash considering type and value.
344 """
345 return hash((self.__class__, self.idnum, self.generation, id(self.pdf)))
347 def replicate(
348 self,
349 pdf_dest: PdfWriterProtocol,
350 ) -> "PdfObject":
351 return IndirectObject(self.idnum, self.generation, pdf_dest)
353 def clone(
354 self,
355 pdf_dest: PdfWriterProtocol,
356 force_duplicate: bool = False,
357 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
358 ) -> "IndirectObject":
359 """Clone object into pdf_dest."""
360 if self.pdf == pdf_dest and not force_duplicate:
361 # Already duplicated and no extra duplication required
362 return self
363 if id(self.pdf) not in pdf_dest._id_translated:
364 pdf_dest._id_translated[id(self.pdf)] = {}
365 pdf_dest._id_translated[id(self.pdf)]["PreventGC"] = self.pdf
367 if self.idnum in pdf_dest._id_translated[id(self.pdf)]:
368 dup = pdf_dest.get_object(pdf_dest._id_translated[id(self.pdf)][self.idnum])
369 if force_duplicate:
370 assert dup is not None
371 assert dup.indirect_reference is not None
372 idref = dup.indirect_reference
373 return IndirectObject(idref.idnum, idref.generation, idref.pdf)
374 else:
375 obj = self.get_object()
376 # case observed : a pointed object can not be found
377 if obj is None:
378 # this normally
379 obj = NullObject()
380 assert isinstance(self, (IndirectObject,))
381 obj.indirect_reference = self
382 dup = pdf_dest._add_object(
383 obj.clone(pdf_dest, force_duplicate, ignore_fields)
384 )
385 assert isinstance(dup, PdfObject), "mypy"
386 assert dup.indirect_reference is not None, "mypy"
387 return dup.indirect_reference
389 @property
390 def indirect_reference(self) -> "IndirectObject": # type: ignore[override]
391 return self
393 def get_object(self) -> Optional["PdfObject"]:
394 obj: Optional[PdfObject] = self.pdf.get_object(self)
395 return obj
397 def __deepcopy__(self, memo: Any) -> "IndirectObject":
398 return IndirectObject(self.idnum, self.generation, self.pdf)
400 def _get_object_with_check(self) -> Optional["PdfObject"]:
401 o = self.get_object()
402 # the check is done here to not slow down get_object()
403 if isinstance(o, IndirectObject):
404 raise PdfStreamError(
405 f"{self.__repr__()} references an IndirectObject {o.__repr__()}"
406 )
407 return o
409 def __getattr__(self, name: str) -> Any:
410 # Attribute not found in object: look in pointed object
411 try:
412 return getattr(self._get_object_with_check(), name)
413 except AttributeError as exception:
414 raise AttributeError(
415 f"No attribute {name} found in IndirectObject or pointed object"
416 ) from exception
418 def __getitem__(self, key: Any) -> Any:
419 # items should be extracted from pointed Object
420 return self._get_object_with_check()[key] # type: ignore[index]
422 def __contains__(self, key: Any) -> bool:
423 return key in self._get_object_with_check() # type: ignore[operator]
425 def __iter__(self) -> Any:
426 return self._get_object_with_check().__iter__() # type: ignore[union-attr]
428 def __float__(self) -> str:
429 # in this case we are looking for the pointed data
430 return self.get_object().__float__() # type: ignore[union-attr, no-any-return]
432 def __int__(self) -> int:
433 # in this case we are looking for the pointed data
434 return self.get_object().__int__() # type: ignore[union-attr, no-any-return]
436 def __str__(self) -> str:
437 # in this case we are looking for the pointed data
438 return self.get_object().__str__()
440 def __repr__(self) -> str:
441 return f"IndirectObject({self.idnum!r}, {self.generation!r}, {id(self.pdf)})"
443 def __eq__(self, other: object) -> bool:
444 return (
445 other is not None
446 and isinstance(other, IndirectObject)
447 and self.idnum == other.idnum
448 and self.generation == other.generation
449 and self.pdf is other.pdf
450 )
452 def __ne__(self, other: object) -> bool:
453 return not self.__eq__(other)
455 def write_to_stream(
456 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
457 ) -> None:
458 if encryption_key is not None: # deprecated
459 deprecation_no_replacement(
460 "the encryption_key parameter of write_to_stream", "5.0.0"
461 )
462 stream.write(f"{self.idnum} {self.generation} R".encode())
464 @staticmethod
465 def read_from_stream(stream: StreamType, pdf: Any) -> "IndirectObject": # PdfReader
466 def read_part(name: str, skip_leading_whitespace: bool) -> bytes:
467 result = bytearray()
468 while True:
469 tok = stream.read(1)
470 if not tok:
471 raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
472 if tok.isspace() or tok in WHITESPACES:
473 if skip_leading_whitespace and not result:
474 continue
475 break
476 if len(result) >= IndirectObject._MAXIMUM_PART_LENGTH:
477 raise PdfReadError(f"{name} exceeds maximum length limit of {IndirectObject._MAXIMUM_PART_LENGTH}.")
478 result += tok
479 return bytes(result)
481 idnum = read_part("Object ID", skip_leading_whitespace=False)
482 generation = read_part("Generation number", skip_leading_whitespace=True)
484 r = read_non_whitespace(stream)
485 if r != b"R":
486 raise PdfReadError(
487 f"Error reading indirect object reference at byte {hex(stream.tell())}"
488 )
489 try:
490 return IndirectObject(int(idnum), int(generation), pdf)
491 except (ValueError, OverflowError) as exception:
492 raise PdfReadError(
493 f"Invalid indirect object reference ({idnum!r} {generation!r} R): {exception}"
494 ) from exception
497FLOAT_WRITE_PRECISION = 8 # shall be min 5 digits max, allow user adj
500class FloatObject(float, PdfObject):
501 def __new__(
502 cls, value: Any = "0.0", context: Optional[Any] = None
503 ) -> Self:
504 try:
505 value = float(value)
506 return float.__new__(cls, value)
507 except Exception as exception:
508 # If this isn't a valid decimal (happens in malformed PDFs)
509 # fallback to 0
510 logger_warning(
511 "%(error)s : FloatObject (%(value)s) invalid; use 0.0 instead",
512 source=__name__,
513 error=exception,
514 value=value,
515 )
516 return float.__new__(cls, 0.0)
518 def clone(
519 self,
520 pdf_dest: Any,
521 force_duplicate: bool = False,
522 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
523 ) -> "FloatObject":
524 """Clone object into pdf_dest."""
525 return cast(
526 "FloatObject",
527 self._reference_clone(FloatObject(self), pdf_dest, force_duplicate),
528 )
530 def hash_bin(self) -> int:
531 """
532 Used to detect modified object.
534 Returns:
535 Hash considering type and value.
537 """
538 return hash((self.__class__, self.as_numeric()))
540 def myrepr(self) -> str:
541 if self == 0: # type: ignore[comparison-overlap]
542 return "0.0"
543 nb = FLOAT_WRITE_PRECISION - int(log10(abs(self)))
544 return f"{self:.{max(1, nb)}f}".rstrip("0").rstrip(".")
546 def __repr__(self) -> str:
547 return self.myrepr() # repr(float(self))
549 def as_numeric(self) -> float:
550 return float(self)
552 def write_to_stream(
553 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
554 ) -> None:
555 if encryption_key is not None: # deprecated
556 deprecation_no_replacement(
557 "the encryption_key parameter of write_to_stream", "5.0.0"
558 )
559 stream.write(self.myrepr().encode("utf8"))
562class NumberObject(int, PdfObject):
563 NumberPattern = re.compile(b"[^+-.0-9]")
564 _LENGTH_LIMIT = 64
566 def __new__(cls, value: Any) -> Self:
567 try:
568 return int.__new__(cls, int(value))
569 except ValueError:
570 logger_warning("NumberObject(%(value)s) invalid; use 0 instead", source=__name__, value=value)
571 return int.__new__(cls, 0)
573 def clone(
574 self,
575 pdf_dest: Any,
576 force_duplicate: bool = False,
577 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
578 ) -> "NumberObject":
579 """Clone object into pdf_dest."""
580 return cast(
581 "NumberObject",
582 self._reference_clone(NumberObject(self), pdf_dest, force_duplicate),
583 )
585 def hash_bin(self) -> int:
586 """
587 Used to detect modified object.
589 Returns:
590 Hash considering type and value.
592 """
593 return hash((self.__class__, self.as_numeric()))
595 def as_numeric(self) -> int:
596 return int(repr(self).encode("utf8"))
598 def write_to_stream(
599 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
600 ) -> None:
601 if encryption_key is not None: # deprecated
602 deprecation_no_replacement(
603 "the encryption_key parameter of write_to_stream", "5.0.0"
604 )
605 stream.write(repr(self).encode("utf8"))
607 @staticmethod
608 def read_from_stream(stream: StreamType) -> Union["NumberObject", "FloatObject"]:
609 num = read_until_regex(stream=stream, regex=NumberObject.NumberPattern, length=NumberObject._LENGTH_LIMIT)
610 if b"." in num:
611 return FloatObject(num)
612 return NumberObject(num)
615class ByteStringObject(bytes, PdfObject):
616 """
617 Represents a string object where the text encoding could not be determined.
619 This occurs quite often, as the PDF spec doesn't provide an alternate way to
620 represent strings -- for example, the encryption data stored in files (like
621 /O) is clearly not text, but is still stored in a "String" object.
622 """
624 def clone(
625 self,
626 pdf_dest: Any,
627 force_duplicate: bool = False,
628 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
629 ) -> "ByteStringObject":
630 """Clone object into pdf_dest."""
631 return cast(
632 "ByteStringObject",
633 self._reference_clone(
634 ByteStringObject(bytes(self)), pdf_dest, force_duplicate
635 ),
636 )
638 def hash_bin(self) -> int:
639 """
640 Used to detect modified object.
642 Returns:
643 Hash considering type and value.
645 """
646 return hash((self.__class__, bytes(self)))
648 @property
649 def original_bytes(self) -> bytes:
650 """For compatibility with TextStringObject.original_bytes."""
651 return self
653 def write_to_stream(
654 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
655 ) -> None:
656 if encryption_key is not None: # deprecated
657 deprecation_no_replacement(
658 "the encryption_key parameter of write_to_stream", "5.0.0"
659 )
660 stream.write(b"<")
661 stream.write(binascii.hexlify(self))
662 stream.write(b">")
664 def __str__(self) -> str:
665 charset_to_try = ["utf-16", *list(NameObject.CHARSETS)]
666 for enc in charset_to_try:
667 try:
668 return self.decode(enc)
669 except UnicodeDecodeError:
670 pass
671 raise PdfReadError("Cannot decode ByteStringObject.")
674class TextStringObject(str, PdfObject): # noqa: SLOT000
675 """
676 A string object that has been decoded into a real unicode string.
678 If read from a PDF document, this string appeared to match the
679 PDFDocEncoding, or contained a UTF-16BE BOM mark to cause UTF-16 decoding
680 to occur.
681 """
683 autodetect_pdfdocencoding: bool
684 autodetect_utf16: bool
685 utf16_bom: bytes
686 _original_bytes: Optional[bytes] = None
688 def __new__(cls, value: Any) -> Self:
689 original_bytes = None
690 if isinstance(value, bytes):
691 original_bytes = value
692 value = value.decode("charmap")
693 text_string_object = str.__new__(cls, value)
694 text_string_object._original_bytes = original_bytes
695 text_string_object.autodetect_utf16 = False
696 text_string_object.autodetect_pdfdocencoding = False
697 text_string_object.utf16_bom = b""
698 if original_bytes is not None and original_bytes[:2] in {codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE}:
699 # The value of `original_bytes` is only set for inputs being `bytes`.
700 # If this is UTF-16 data according to the BOM (first two characters),
701 # perform special handling. All other cases should not need any special conversion
702 # due to already being a string.
703 try:
704 text_string_object = str.__new__(cls, original_bytes.decode("utf-16"))
705 except UnicodeDecodeError as exception:
706 logger_warning(
707 "%(exception)s; initial string: %(initial_string)r",
708 source=__name__,
709 exception=exception,
710 initial_string=exception.object,
711 )
712 text_string_object = str.__new__(cls, exception.object[: exception.start].decode("utf-16"))
713 text_string_object._original_bytes = original_bytes
714 text_string_object.autodetect_utf16 = True
715 text_string_object.utf16_bom = original_bytes[:2]
716 else:
717 try:
718 encode_pdfdocencoding(text_string_object)
719 text_string_object.autodetect_pdfdocencoding = True
720 except UnicodeEncodeError:
721 text_string_object.autodetect_utf16 = True
722 text_string_object.utf16_bom = codecs.BOM_UTF16_BE
723 return text_string_object
725 def clone(
726 self,
727 pdf_dest: Any,
728 force_duplicate: bool = False,
729 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
730 ) -> "TextStringObject":
731 """Clone object into pdf_dest."""
732 obj = TextStringObject(self)
733 obj._original_bytes = self._original_bytes
734 obj.autodetect_pdfdocencoding = self.autodetect_pdfdocencoding
735 obj.autodetect_utf16 = self.autodetect_utf16
736 obj.utf16_bom = self.utf16_bom
737 return cast(
738 "TextStringObject", self._reference_clone(obj, pdf_dest, force_duplicate)
739 )
741 def hash_bin(self) -> int:
742 """
743 Used to detect modified object.
745 Returns:
746 Hash considering type and value.
748 """
749 return hash((self.__class__, self.original_bytes))
751 @property
752 def original_bytes(self) -> bytes:
753 """
754 It is occasionally possible that a text string object gets created where
755 a byte string object was expected due to the autodetection mechanism --
756 if that occurs, this "original_bytes" property can be used to
757 back-calculate what the original encoded bytes were.
758 """
759 if self._original_bytes is not None:
760 return self._original_bytes
761 return self.get_original_bytes()
763 def get_original_bytes(self) -> bytes:
764 # We're a text string object, but the library is trying to get our raw
765 # bytes. This can happen if we auto-detected this string as text, but
766 # we were wrong. It's pretty common. Return the original bytes that
767 # would have been used to create this object, based upon the autodetect
768 # method.
769 if self.autodetect_utf16:
770 if self.utf16_bom == codecs.BOM_UTF16_LE:
771 return codecs.BOM_UTF16_LE + self.encode("utf-16le")
772 if self.utf16_bom == codecs.BOM_UTF16_BE:
773 return codecs.BOM_UTF16_BE + self.encode("utf-16be")
774 return self.encode("utf-16be")
775 if self.autodetect_pdfdocencoding:
776 return encode_pdfdocencoding(self)
777 raise Exception("no information about original bytes") # pragma: no cover
779 def get_encoded_bytes(self) -> bytes:
780 # Try to write the string out as a PDFDocEncoding encoded string. It's
781 # nicer to look at in the PDF file. Sadly, we take a performance hit
782 # here for trying...
783 try:
784 if self._original_bytes is not None:
785 return self._original_bytes
786 if self.autodetect_utf16:
787 raise UnicodeEncodeError("", "forced", -1, -1, "")
788 bytearr = encode_pdfdocencoding(self)
789 except UnicodeEncodeError:
790 if self.utf16_bom == codecs.BOM_UTF16_LE:
791 bytearr = codecs.BOM_UTF16_LE + self.encode("utf-16le")
792 elif self.utf16_bom == codecs.BOM_UTF16_BE:
793 bytearr = codecs.BOM_UTF16_BE + self.encode("utf-16be")
794 else:
795 bytearr = self.encode("utf-16be")
796 return bytearr
798 def write_to_stream(
799 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
800 ) -> None:
801 if encryption_key is not None: # deprecated
802 deprecation_no_replacement(
803 "the encryption_key parameter of write_to_stream", "5.0.0"
804 )
805 bytearr = self.get_encoded_bytes()
806 stream.write(b"(")
807 for c_ in iter_unpack("c", bytearr):
808 c = cast(bytes, c_[0])
809 if not c.isalnum() and c != b" ":
810 # This:
811 # stream.write(rf"\{c:0>3o}".encode())
812 # gives
813 # https://github.com/davidhalter/parso/issues/207
814 stream.write(b"\\%03o" % ord(c))
815 else:
816 stream.write(c)
817 stream.write(b")")
820class NameObject(str, PdfObject): # noqa: SLOT000
821 delimiter_pattern = re.compile(rb"\s+|[\(\)<>\[\]{}/%]")
822 prefix = b"/"
823 renumber_table: ClassVar[dict[str, bytes]] = {
824 **{chr(i): f"#{i:02X}".encode() for i in b"#()<>[]{}/%"},
825 **{chr(i): f"#{i:02X}".encode() for i in range(33)},
826 }
827 _LENGTH_LIMIT = 4096
829 def clone(
830 self,
831 pdf_dest: Any,
832 force_duplicate: bool = False,
833 ignore_fields: Optional[Sequence[Union[str, int]]] = (),
834 ) -> "NameObject":
835 """Clone object into pdf_dest."""
836 return cast(
837 "NameObject",
838 self._reference_clone(NameObject(self), pdf_dest, force_duplicate),
839 )
841 def hash_bin(self) -> int:
842 """
843 Used to detect modified object.
845 Returns:
846 Hash considering type and value.
848 """
849 return hash((self.__class__, self))
851 def write_to_stream(
852 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None
853 ) -> None:
854 if encryption_key is not None: # deprecated
855 deprecation_no_replacement(
856 "the encryption_key parameter of write_to_stream", "5.0.0"
857 )
858 stream.write(self.renumber())
860 def renumber(self) -> bytes:
861 out = self[0].encode("utf-8")
862 if out != b"/":
863 deprecation_no_replacement(
864 f"Incorrect first char in NameObject, should start with '/': ({self})",
865 "5.0.0",
866 )
867 parts = [out]
868 for c in self[1:]:
869 if c > "~":
870 parts.extend(f"#{x:02X}".encode() for x in c.encode("utf-8"))
871 else:
872 try:
873 parts.append(self.renumber_table[c])
874 except KeyError:
875 parts.append(c.encode("utf-8"))
876 return b"".join(parts)
878 def _sanitize(self) -> "NameObject":
879 """
880 Sanitize the NameObject's name to be a valid PDF name part
881 (alphanumeric, underscore, hyphen). The _sanitize method replaces
882 spaces and any non-alphanumeric/non-underscore/non-hyphen with
883 underscores.
885 Returns:
886 NameObject with sanitized name.
887 """
888 name = str(self).removeprefix("/")
889 name = re.sub(r" ", "_", name)
890 name = re.sub(r"[^a-zA-Z0-9_-]", "_", name)
891 return NameObject("/" + name)
893 @classproperty
894 def surfix(cls) -> bytes: # noqa: N805
895 deprecation_with_replacement("surfix", "prefix", "5.0.0")
896 return b"/"
898 @staticmethod
899 def unnumber(sin: bytes) -> bytes:
900 if b"#" not in sin:
901 return bytes(sin)
902 result = bytearray()
903 i = 0
904 while i < len(sin):
905 if sin[i:i + 1] == b"#":
906 try:
907 result.append(int(sin[i + 1 : i + 3], 16))
908 i += 3
909 continue
910 except (ValueError, IndexError):
911 # if the 2 characters after # can not be converted to hex
912 # we change nothing and carry on
913 pass
914 result.append(sin[i])
915 i += 1
916 return bytes(result)
918 CHARSETS = ("utf-8", "gbk", "latin1")
920 @staticmethod
921 def read_from_stream(stream: StreamType, pdf: Any) -> "NameObject": # PdfReader
922 name = stream.read(1)
923 if name != NameObject.prefix:
924 raise PdfReadError("Name read error")
925 name += read_until_regex(stream=stream, regex=NameObject.delimiter_pattern, length=NameObject._LENGTH_LIMIT)
926 try:
927 # Name objects should represent irregular characters
928 # with a '#' followed by the symbol's hex number
929 name = NameObject.unnumber(name)
930 for enc in NameObject.CHARSETS:
931 try:
932 ret = name.decode(enc)
933 return NameObject(ret)
934 except Exception:
935 pass
936 raise UnicodeDecodeError("", name, 0, 0, "Code Not Found")
937 except (UnicodeEncodeError, UnicodeDecodeError) as exception:
938 if not pdf.strict:
939 logger_warning(
940 "Illegal character in NameObject (%(name)r), you may need to adjust NameObject.CHARSETS",
941 source=__name__,
942 name=name,
943 )
944 return NameObject(name.decode("charmap"))
945 raise PdfReadError(
946 f"Illegal character in NameObject ({name!r}). "
947 "You may need to adjust NameObject.CHARSETS.",
948 ) from exception
951def encode_pdfdocencoding(unicode_string: str) -> bytes:
952 try:
953 return bytes([_pdfdoc_encoding_rev[k] for k in unicode_string])
954 except KeyError as exception:
955 raise UnicodeEncodeError(
956 "pdfdocencoding",
957 unicode_string,
958 -1,
959 -1,
960 "does not exist in translation table",
961 ) from exception
964def is_null_or_none(x: Any) -> TypeGuard[Union[NullObject, IndirectObject, None]]:
965 """
966 Returns:
967 True if x is None or NullObject.
969 """
970 return x is None or (
971 isinstance(x, PdfObject)
972 and (x.get_object() is None or isinstance(x.get_object(), NullObject))
973 )