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