aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/pypdf/generic
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/pypdf/generic')
-rw-r--r--.venv/lib/python3.12/site-packages/pypdf/generic/__init__.py464
-rw-r--r--.venv/lib/python3.12/site-packages/pypdf/generic/_base.py721
-rw-r--r--.venv/lib/python3.12/site-packages/pypdf/generic/_data_structures.py1616
-rw-r--r--.venv/lib/python3.12/site-packages/pypdf/generic/_fit.py168
-rw-r--r--.venv/lib/python3.12/site-packages/pypdf/generic/_image_inline.py235
-rw-r--r--.venv/lib/python3.12/site-packages/pypdf/generic/_outline.py33
-rw-r--r--.venv/lib/python3.12/site-packages/pypdf/generic/_rectangle.py132
-rw-r--r--.venv/lib/python3.12/site-packages/pypdf/generic/_utils.py180
-rw-r--r--.venv/lib/python3.12/site-packages/pypdf/generic/_viewerpref.py164
9 files changed, 3713 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pypdf/generic/__init__.py b/.venv/lib/python3.12/site-packages/pypdf/generic/__init__.py
new file mode 100644
index 00000000..48045e0a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pypdf/generic/__init__.py
@@ -0,0 +1,464 @@
+# Copyright (c) 2006, Mathieu Fenniak
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""Implementation of generic PDF objects (dictionary, number, string, ...)."""
+__author__ = "Mathieu Fenniak"
+__author_email__ = "biziqe@mathieu.fenniak.net"
+
+from typing import Dict, List, Optional, Tuple, Union
+
+from .._utils import StreamType, deprecate_with_replacement
+from ..constants import OutlineFontFlag
+from ._base import (
+ BooleanObject,
+ ByteStringObject,
+ FloatObject,
+ IndirectObject,
+ NameObject,
+ NullObject,
+ NumberObject,
+ PdfObject,
+ TextStringObject,
+ encode_pdfdocencoding,
+)
+from ._data_structures import (
+ ArrayObject,
+ ContentStream,
+ DecodedStreamObject,
+ Destination,
+ DictionaryObject,
+ EncodedStreamObject,
+ Field,
+ StreamObject,
+ TreeObject,
+ read_object,
+)
+from ._fit import Fit
+from ._outline import OutlineItem
+from ._rectangle import RectangleObject
+from ._utils import (
+ create_string_object,
+ decode_pdfdocencoding,
+ hex_to_rgb,
+ read_hex_string_from_stream,
+ read_string_from_stream,
+)
+from ._viewerpref import ViewerPreferences
+
+
+def readHexStringFromStream(
+ stream: StreamType,
+) -> Union["TextStringObject", "ByteStringObject"]: # deprecated
+ """Deprecated, use read_hex_string_from_stream."""
+ deprecate_with_replacement(
+ "readHexStringFromStream", "read_hex_string_from_stream", "4.0.0"
+ )
+ return read_hex_string_from_stream(stream)
+
+
+def readStringFromStream(
+ stream: StreamType,
+ forced_encoding: Union[None, str, List[str], Dict[int, str]] = None,
+) -> Union["TextStringObject", "ByteStringObject"]: # deprecated
+ """Deprecated, use read_string_from_stream."""
+ deprecate_with_replacement(
+ "readStringFromStream", "read_string_from_stream", "4.0.0"
+ )
+ return read_string_from_stream(stream, forced_encoding)
+
+
+def createStringObject(
+ string: Union[str, bytes],
+ forced_encoding: Union[None, str, List[str], Dict[int, str]] = None,
+) -> Union[TextStringObject, ByteStringObject]: # deprecated
+ """Deprecated, use create_string_object."""
+ deprecate_with_replacement("createStringObject", "create_string_object", "4.0.0")
+ return create_string_object(string, forced_encoding)
+
+
+PAGE_FIT = Fit.fit()
+
+
+class AnnotationBuilder:
+ """
+ The AnnotationBuilder is deprecated.
+
+ Instead, use the annotation classes in pypdf.annotations.
+
+ See `adding PDF annotations <../user/adding-pdf-annotations.html>`_ for
+ its usage combined with PdfWriter.
+ """
+
+ from ..generic._rectangle import RectangleObject
+
+ @staticmethod
+ def text(
+ rect: Union[RectangleObject, Tuple[float, float, float, float]],
+ text: str,
+ open: bool = False,
+ flags: int = 0,
+ ) -> DictionaryObject:
+ """
+ Add text annotation.
+
+ Args:
+ rect: array of four integers ``[xLL, yLL, xUR, yUR]``
+ specifying the clickable rectangular area
+ text: The text that is added to the document
+ open:
+ flags:
+
+ Returns:
+ A dictionary object representing the annotation.
+ """
+ deprecate_with_replacement(
+ "AnnotationBuilder.text", "pypdf.annotations.Text", "4.0.0"
+ )
+ from ..annotations import Text
+
+ return Text(rect=rect, text=text, open=open, flags=flags)
+
+ @staticmethod
+ def free_text(
+ text: str,
+ rect: Union[RectangleObject, Tuple[float, float, float, float]],
+ font: str = "Helvetica",
+ bold: bool = False,
+ italic: bool = False,
+ font_size: str = "14pt",
+ font_color: str = "000000",
+ border_color: Optional[str] = "000000",
+ background_color: Optional[str] = "ffffff",
+ ) -> DictionaryObject:
+ """
+ Add text in a rectangle to a page.
+
+ Args:
+ text: Text to be added
+ rect: array of four integers ``[xLL, yLL, xUR, yUR]``
+ specifying the clickable rectangular area
+ font: Name of the Font, e.g. 'Helvetica'
+ bold: Print the text in bold
+ italic: Print the text in italic
+ font_size: How big the text will be, e.g. '14pt'
+ font_color: Hex-string for the color, e.g. cdcdcd
+ border_color: Hex-string for the border color, e.g. cdcdcd.
+ Use ``None`` for no border.
+ background_color: Hex-string for the background of the annotation,
+ e.g. cdcdcd. Use ``None`` for transparent background.
+
+ Returns:
+ A dictionary object representing the annotation.
+ """
+ deprecate_with_replacement(
+ "AnnotationBuilder.free_text", "pypdf.annotations.FreeText", "4.0.0"
+ )
+ from ..annotations import FreeText
+
+ return FreeText(
+ text=text,
+ rect=rect,
+ font=font,
+ bold=bold,
+ italic=italic,
+ font_size=font_size,
+ font_color=font_color,
+ background_color=background_color,
+ border_color=border_color,
+ )
+
+ @staticmethod
+ def popup(
+ *,
+ rect: Union[RectangleObject, Tuple[float, float, float, float]],
+ flags: int = 0,
+ parent: Optional[DictionaryObject] = None,
+ open: bool = False,
+ ) -> DictionaryObject:
+ """
+ Add a popup to the document.
+
+ Args:
+ rect:
+ Specifies the clickable rectangular area as `[xLL, yLL, xUR, yUR]`
+ flags:
+ 1 - invisible, 2 - hidden, 3 - print, 4 - no zoom,
+ 5 - no rotate, 6 - no view, 7 - read only, 8 - locked,
+ 9 - toggle no view, 10 - locked contents
+ open:
+ Whether the popup should be shown directly (default is False).
+ parent:
+ The contents of the popup. Create this via the AnnotationBuilder.
+
+ Returns:
+ A dictionary object representing the annotation.
+ """
+ deprecate_with_replacement(
+ "AnnotationBuilder.popup", "pypdf.annotations.Popup", "4.0.0"
+ )
+ from ..annotations import Popup
+
+ popup = Popup(rect=rect, open=open, parent=parent)
+ popup.flags = flags # type: ignore
+
+ return popup
+
+ @staticmethod
+ def line(
+ p1: Tuple[float, float],
+ p2: Tuple[float, float],
+ rect: Union[RectangleObject, Tuple[float, float, float, float]],
+ text: str = "",
+ title_bar: Optional[str] = None,
+ ) -> DictionaryObject:
+ """
+ Draw a line on the PDF.
+
+ Args:
+ p1: First point
+ p2: Second point
+ rect: array of four integers ``[xLL, yLL, xUR, yUR]``
+ specifying the clickable rectangular area
+ text: Text to be displayed as the line annotation
+ title_bar: Text to be displayed in the title bar of the
+ annotation; by convention this is the name of the author
+
+ Returns:
+ A dictionary object representing the annotation.
+ """
+ deprecate_with_replacement(
+ "AnnotationBuilder.line", "pypdf.annotations.Line", "4.0.0"
+ )
+ from ..annotations import Line
+
+ return Line(p1=p1, p2=p2, rect=rect, text=text, title_bar=title_bar)
+
+ @staticmethod
+ def polyline(
+ vertices: List[Tuple[float, float]],
+ ) -> DictionaryObject:
+ """
+ Draw a polyline on the PDF.
+
+ Args:
+ vertices: Array specifying the vertices (x, y) coordinates of the poly-line.
+
+ Returns:
+ A dictionary object representing the annotation.
+ """
+ deprecate_with_replacement(
+ "AnnotationBuilder.polyline", "pypdf.annotations.PolyLine", "4.0.0"
+ )
+ from ..annotations import PolyLine
+
+ return PolyLine(vertices=vertices)
+
+ @staticmethod
+ def rectangle(
+ rect: Union[RectangleObject, Tuple[float, float, float, float]],
+ interiour_color: Optional[str] = None,
+ ) -> DictionaryObject:
+ """
+ Draw a rectangle on the PDF.
+
+ This method uses the /Square annotation type of the PDF format.
+
+ Args:
+ rect: array of four integers ``[xLL, yLL, xUR, yUR]``
+ specifying the clickable rectangular area
+ interiour_color: None or hex-string for the color, e.g. cdcdcd
+ If None is used, the interiour is transparent.
+
+ Returns:
+ A dictionary object representing the annotation.
+ """
+ deprecate_with_replacement(
+ "AnnotationBuilder.rectangle", "pypdf.annotations.Rectangle", "4.0.0"
+ )
+ from ..annotations import Rectangle
+
+ return Rectangle(rect=rect, interiour_color=interiour_color)
+
+ @staticmethod
+ def highlight(
+ *,
+ rect: Union[RectangleObject, Tuple[float, float, float, float]],
+ quad_points: ArrayObject,
+ highlight_color: str = "ff0000",
+ printing: bool = False,
+ ) -> DictionaryObject:
+ """
+ Add a highlight annotation to the document.
+
+ Args:
+ rect: Array of four integers ``[xLL, yLL, xUR, yUR]``
+ specifying the highlighted area
+ quad_points: An ArrayObject of 8 FloatObjects. Must match a word or
+ a group of words, otherwise no highlight will be shown.
+ highlight_color: The color used for the highlight.
+ printing: Whether to print out the highlight annotation when the page
+ is printed.
+
+ Returns:
+ A dictionary object representing the annotation.
+ """
+ deprecate_with_replacement(
+ "AnnotationBuilder.highlight", "pypdf.annotations.Highlight", "4.0.0"
+ )
+ from ..annotations import Highlight
+
+ return Highlight(
+ rect=rect, quad_points=quad_points, highlight_color=highlight_color, printing=printing
+ )
+
+ @staticmethod
+ def ellipse(
+ rect: Union[RectangleObject, Tuple[float, float, float, float]],
+ interiour_color: Optional[str] = None,
+ ) -> DictionaryObject:
+ """
+ Draw an ellipse on the PDF.
+
+ This method uses the /Circle annotation type of the PDF format.
+
+ Args:
+ rect: array of four integers ``[xLL, yLL, xUR, yUR]`` specifying
+ the bounding box of the ellipse
+ interiour_color: None or hex-string for the color, e.g. cdcdcd
+ If None is used, the interiour is transparent.
+
+ Returns:
+ A dictionary object representing the annotation.
+ """
+ deprecate_with_replacement(
+ "AnnotationBuilder.ellipse", "pypdf.annotations.Ellipse", "4.0.0"
+ )
+ from ..annotations import Ellipse
+
+ return Ellipse(rect=rect, interiour_color=interiour_color)
+
+ @staticmethod
+ def polygon(vertices: List[Tuple[float, float]]) -> DictionaryObject:
+ deprecate_with_replacement(
+ "AnnotationBuilder.polygon", "pypdf.annotations.Polygon", "4.0.0"
+ )
+ from ..annotations import Polygon
+
+ return Polygon(vertices=vertices)
+
+ from ._fit import DEFAULT_FIT
+
+ @staticmethod
+ def link(
+ rect: Union[RectangleObject, Tuple[float, float, float, float]],
+ border: Optional[ArrayObject] = None,
+ url: Optional[str] = None,
+ target_page_index: Optional[int] = None,
+ fit: Fit = DEFAULT_FIT,
+ ) -> DictionaryObject:
+ """
+ Add a link to the document.
+
+ The link can either be an external link or an internal link.
+
+ An external link requires the URL parameter.
+ An internal link requires the target_page_index, fit, and fit args.
+
+ Args:
+ rect: array of four integers ``[xLL, yLL, xUR, yUR]``
+ specifying the clickable rectangular area
+ border: if provided, an array describing border-drawing
+ properties. See the PDF spec for details. No border will be
+ drawn if this argument is omitted.
+ - horizontal corner radius,
+ - vertical corner radius, and
+ - border width
+ - Optionally: Dash
+ url: Link to a website (if you want to make an external link)
+ target_page_index: index of the page to which the link should go
+ (if you want to make an internal link)
+ fit: Page fit or 'zoom' option.
+
+ Returns:
+ A dictionary object representing the annotation.
+ """
+ deprecate_with_replacement(
+ "AnnotationBuilder.link", "pypdf.annotations.Link", "4.0.0"
+ )
+ from ..annotations import Link
+
+ return Link(
+ rect=rect,
+ border=border,
+ url=url,
+ target_page_index=target_page_index,
+ fit=fit,
+ )
+
+
+__all__ = [
+ # Base types
+ "BooleanObject",
+ "FloatObject",
+ "NumberObject",
+ "NameObject",
+ "IndirectObject",
+ "NullObject",
+ "PdfObject",
+ "TextStringObject",
+ "ByteStringObject",
+ # Annotations
+ "AnnotationBuilder",
+ # Fit
+ "Fit",
+ "PAGE_FIT",
+ # Data structures
+ "ArrayObject",
+ "DictionaryObject",
+ "TreeObject",
+ "StreamObject",
+ "DecodedStreamObject",
+ "EncodedStreamObject",
+ "ContentStream",
+ "RectangleObject",
+ "Field",
+ "Destination",
+ "ViewerPreferences",
+ # --- More specific stuff
+ # Outline
+ "OutlineItem",
+ "OutlineFontFlag",
+ # Data structures core functions
+ "read_object",
+ # Utility functions
+ "create_string_object",
+ "encode_pdfdocencoding",
+ "decode_pdfdocencoding",
+ "hex_to_rgb",
+ "read_hex_string_from_stream",
+ "read_string_from_stream",
+]
diff --git a/.venv/lib/python3.12/site-packages/pypdf/generic/_base.py b/.venv/lib/python3.12/site-packages/pypdf/generic/_base.py
new file mode 100644
index 00000000..2d606b41
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pypdf/generic/_base.py
@@ -0,0 +1,721 @@
+# Copyright (c) 2006, Mathieu Fenniak
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+import binascii
+import codecs
+import hashlib
+import re
+from binascii import unhexlify
+from math import log10
+from typing import Any, Callable, ClassVar, Dict, Optional, Sequence, Union, cast
+
+from .._codecs import _pdfdoc_encoding_rev
+from .._protocols import PdfObjectProtocol, PdfWriterProtocol
+from .._utils import (
+ StreamType,
+ b_,
+ deprecate_no_replacement,
+ logger_warning,
+ read_non_whitespace,
+ read_until_regex,
+ str_,
+)
+from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError
+
+__author__ = "Mathieu Fenniak"
+__author_email__ = "biziqe@mathieu.fenniak.net"
+
+
+class PdfObject(PdfObjectProtocol):
+ # function for calculating a hash value
+ hash_func: Callable[..., "hashlib._Hash"] = hashlib.sha1
+ indirect_reference: Optional["IndirectObject"]
+
+ def hash_value_data(self) -> bytes:
+ return ("%s" % self).encode()
+
+ def hash_value(self) -> bytes:
+ return (
+ "%s:%s"
+ % (
+ self.__class__.__name__,
+ self.hash_func(self.hash_value_data()).hexdigest(),
+ )
+ ).encode()
+
+ def clone(
+ self,
+ pdf_dest: PdfWriterProtocol,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "PdfObject":
+ """
+ Clone object into pdf_dest (PdfWriterProtocol which is an interface for PdfWriter).
+
+ By default, this method will call ``_reference_clone`` (see ``_reference``).
+
+
+ Args:
+ pdf_dest: Target to clone to.
+ force_duplicate: By default, if the object has already been cloned and referenced,
+ the copy will be returned; when ``True``, a new copy will be created.
+ (Default value = ``False``)
+ ignore_fields: List/tuple of field names (for dictionaries) that will be ignored
+ during cloning (applies to children duplication as well). If fields are to be
+ considered for a limited number of levels, you have to add it as integer, for
+ example ``[1,"/B","/TOTO"]`` means that ``"/B"`` will be ignored at the first
+ level only but ``"/TOTO"`` on all levels.
+
+ Returns:
+ The cloned PdfObject
+ """
+ raise NotImplementedError(
+ f"{self.__class__.__name__} does not implement .clone so far"
+ )
+
+ def _reference_clone(
+ self, clone: Any, pdf_dest: PdfWriterProtocol, force_duplicate: bool = False
+ ) -> PdfObjectProtocol:
+ """
+ Reference the object within the _objects of pdf_dest only if
+ indirect_reference attribute exists (which means the objects was
+ already identified in xref/xobjstm) if object has been already
+ referenced do nothing.
+
+ Args:
+ clone:
+ pdf_dest:
+
+ Returns:
+ The clone
+ """
+ try:
+ if not force_duplicate and clone.indirect_reference.pdf == pdf_dest:
+ return clone
+ except Exception:
+ pass
+ # if hasattr(clone, "indirect_reference"):
+ try:
+ ind = self.indirect_reference
+ except AttributeError:
+ return clone
+ i = len(pdf_dest._objects) + 1
+ if ind is not None:
+ if id(ind.pdf) not in pdf_dest._id_translated:
+ pdf_dest._id_translated[id(ind.pdf)] = {}
+ pdf_dest._id_translated[id(ind.pdf)]["PreventGC"] = ind.pdf # type: ignore
+ if (
+ not force_duplicate
+ and ind.idnum in pdf_dest._id_translated[id(ind.pdf)]
+ ):
+ obj = pdf_dest.get_object(
+ pdf_dest._id_translated[id(ind.pdf)][ind.idnum]
+ )
+ assert obj is not None
+ return obj
+ pdf_dest._id_translated[id(ind.pdf)][ind.idnum] = i
+ pdf_dest._objects.append(clone)
+ clone.indirect_reference = IndirectObject(i, 0, pdf_dest)
+ return clone
+
+ def get_object(self) -> Optional["PdfObject"]:
+ """Resolve indirect references."""
+ return self
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ raise NotImplementedError
+
+
+class NullObject(PdfObject):
+ def clone(
+ self,
+ pdf_dest: PdfWriterProtocol,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "NullObject":
+ """Clone object into pdf_dest."""
+ return cast(
+ "NullObject", self._reference_clone(NullObject(), pdf_dest, force_duplicate)
+ )
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ stream.write(b"null")
+
+ @staticmethod
+ def read_from_stream(stream: StreamType) -> "NullObject":
+ nulltxt = stream.read(4)
+ if nulltxt != b"null":
+ raise PdfReadError("Could not read Null object")
+ return NullObject()
+
+ def __repr__(self) -> str:
+ return "NullObject"
+
+
+class BooleanObject(PdfObject):
+ def __init__(self, value: Any) -> None:
+ self.value = value
+
+ def clone(
+ self,
+ pdf_dest: PdfWriterProtocol,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "BooleanObject":
+ """Clone object into pdf_dest."""
+ return cast(
+ "BooleanObject",
+ self._reference_clone(BooleanObject(self.value), pdf_dest, force_duplicate),
+ )
+
+ def __eq__(self, __o: object) -> bool:
+ if isinstance(__o, BooleanObject):
+ return self.value == __o.value
+ elif isinstance(__o, bool):
+ return self.value == __o
+ else:
+ return False
+
+ def __repr__(self) -> str:
+ return "True" if self.value else "False"
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ if self.value:
+ stream.write(b"true")
+ else:
+ stream.write(b"false")
+
+ @staticmethod
+ def read_from_stream(stream: StreamType) -> "BooleanObject":
+ word = stream.read(4)
+ if word == b"true":
+ return BooleanObject(True)
+ elif word == b"fals":
+ stream.read(1)
+ return BooleanObject(False)
+ else:
+ raise PdfReadError("Could not read Boolean object")
+
+
+class IndirectObject(PdfObject):
+ def __init__(self, idnum: int, generation: int, pdf: Any) -> None: # PdfReader
+ self.idnum = idnum
+ self.generation = generation
+ self.pdf = pdf
+
+ def clone(
+ self,
+ pdf_dest: PdfWriterProtocol,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "IndirectObject":
+ """Clone object into pdf_dest."""
+ if self.pdf == pdf_dest and not force_duplicate:
+ # Already duplicated and no extra duplication required
+ return self
+ if id(self.pdf) not in pdf_dest._id_translated:
+ pdf_dest._id_translated[id(self.pdf)] = {}
+
+ if self.idnum in pdf_dest._id_translated[id(self.pdf)]:
+ dup = pdf_dest.get_object(pdf_dest._id_translated[id(self.pdf)][self.idnum])
+ if force_duplicate:
+ assert dup is not None
+ assert dup.indirect_reference is not None
+ idref = dup.indirect_reference
+ return IndirectObject(idref.idnum, idref.generation, idref.pdf)
+ else:
+ obj = self.get_object()
+ # case observed : a pointed object can not be found
+ if obj is None:
+ # this normally
+ obj = NullObject()
+ assert isinstance(self, (IndirectObject,))
+ obj.indirect_reference = self
+ dup = pdf_dest._add_object(
+ obj.clone(pdf_dest, force_duplicate, ignore_fields)
+ )
+ # asserts added to prevent errors in mypy
+ assert dup is not None
+ assert dup.indirect_reference is not None
+ return dup.indirect_reference
+
+ @property
+ def indirect_reference(self) -> "IndirectObject": # type: ignore[override]
+ return self
+
+ def get_object(self) -> Optional["PdfObject"]:
+ return self.pdf.get_object(self)
+
+ def __deepcopy__(self, memo: Any) -> "IndirectObject":
+ return IndirectObject(self.idnum, self.generation, self.pdf)
+
+ def _get_object_with_check(self) -> Optional["PdfObject"]:
+ o = self.get_object()
+ # the check is done here to not slow down get_object()
+ if isinstance(o, IndirectObject):
+ raise PdfStreamError(
+ f"{self.__repr__()} references an IndirectObject {o.__repr__()}"
+ )
+ return o
+
+ def __getattr__(self, name: str) -> Any:
+ # Attribute not found in object: look in pointed object
+ try:
+ return getattr(self._get_object_with_check(), name)
+ except AttributeError:
+ raise AttributeError(
+ f"No attribute {name} found in IndirectObject or pointed object"
+ )
+
+ def __getitem__(self, key: Any) -> Any:
+ # items should be extracted from pointed Object
+ return self._get_object_with_check()[key] # type: ignore
+
+ def __str__(self) -> str:
+ # in this case we are looking for the pointed data
+ return self.get_object().__str__()
+
+ def __repr__(self) -> str:
+ return f"IndirectObject({self.idnum!r}, {self.generation!r}, {id(self.pdf)})"
+
+ def __eq__(self, other: object) -> bool:
+ return (
+ other is not None
+ and isinstance(other, IndirectObject)
+ and self.idnum == other.idnum
+ and self.generation == other.generation
+ and self.pdf is other.pdf
+ )
+
+ def __ne__(self, other: object) -> bool:
+ return not self.__eq__(other)
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ stream.write(f"{self.idnum} {self.generation} R".encode())
+
+ @staticmethod
+ def read_from_stream(stream: StreamType, pdf: Any) -> "IndirectObject": # PdfReader
+ idnum = b""
+ while True:
+ tok = stream.read(1)
+ if not tok:
+ raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
+ if tok.isspace():
+ break
+ idnum += tok
+ generation = b""
+ while True:
+ tok = stream.read(1)
+ if not tok:
+ raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
+ if tok.isspace():
+ if not generation:
+ continue
+ break
+ generation += tok
+ r = read_non_whitespace(stream)
+ if r != b"R":
+ raise PdfReadError(
+ f"Error reading indirect object reference at byte {hex(stream.tell())}"
+ )
+ return IndirectObject(int(idnum), int(generation), pdf)
+
+
+FLOAT_WRITE_PRECISION = 8 # shall be min 5 digits max, allow user adj
+
+
+class FloatObject(float, PdfObject):
+ def __new__(
+ cls, value: Union[str, Any] = "0.0", context: Optional[Any] = None
+ ) -> "FloatObject":
+ try:
+ value = float(str_(value))
+ return float.__new__(cls, value)
+ except Exception as e:
+ # If this isn't a valid decimal (happens in malformed PDFs)
+ # fallback to 0
+ logger_warning(
+ f"{e} : FloatObject ({value}) invalid; use 0.0 instead", __name__
+ )
+ return float.__new__(cls, 0.0)
+
+ def clone(
+ self,
+ pdf_dest: Any,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "FloatObject":
+ """Clone object into pdf_dest."""
+ return cast(
+ "FloatObject",
+ self._reference_clone(FloatObject(self), pdf_dest, force_duplicate),
+ )
+
+ def myrepr(self) -> str:
+ if self == 0:
+ return "0.0"
+ nb = FLOAT_WRITE_PRECISION - int(log10(abs(self)))
+ s = f"{self:.{max(1,nb)}f}".rstrip("0").rstrip(".")
+ return s
+
+ def __repr__(self) -> str:
+ return self.myrepr() # repr(float(self))
+
+ def as_numeric(self) -> float:
+ return float(self)
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ stream.write(self.myrepr().encode("utf8"))
+
+
+class NumberObject(int, PdfObject):
+ NumberPattern = re.compile(b"[^+-.0-9]")
+
+ def __new__(cls, value: Any) -> "NumberObject":
+ try:
+ return int.__new__(cls, int(value))
+ except ValueError:
+ logger_warning(f"NumberObject({value}) invalid; use 0 instead", __name__)
+ return int.__new__(cls, 0)
+
+ def clone(
+ self,
+ pdf_dest: Any,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "NumberObject":
+ """Clone object into pdf_dest."""
+ return cast(
+ "NumberObject",
+ self._reference_clone(NumberObject(self), pdf_dest, force_duplicate),
+ )
+
+ def as_numeric(self) -> int:
+ return int(repr(self).encode("utf8"))
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ stream.write(repr(self).encode("utf8"))
+
+ @staticmethod
+ def read_from_stream(stream: StreamType) -> Union["NumberObject", "FloatObject"]:
+ num = read_until_regex(stream, NumberObject.NumberPattern)
+ if num.find(b".") != -1:
+ return FloatObject(num)
+ return NumberObject(num)
+
+
+class ByteStringObject(bytes, PdfObject):
+ """
+ Represents a string object where the text encoding could not be determined.
+
+ This occurs quite often, as the PDF spec doesn't provide an alternate way to
+ represent strings -- for example, the encryption data stored in files (like
+ /O) is clearly not text, but is still stored in a "String" object.
+ """
+
+ def clone(
+ self,
+ pdf_dest: Any,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "ByteStringObject":
+ """Clone object into pdf_dest."""
+ return cast(
+ "ByteStringObject",
+ self._reference_clone(
+ ByteStringObject(bytes(self)), pdf_dest, force_duplicate
+ ),
+ )
+
+ @property
+ def original_bytes(self) -> bytes:
+ """For compatibility with TextStringObject.original_bytes."""
+ return self
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ stream.write(b"<")
+ stream.write(binascii.hexlify(self))
+ stream.write(b">")
+
+
+class TextStringObject(str, PdfObject): # noqa: SLOT000
+ """
+ A string object that has been decoded into a real unicode string.
+
+ If read from a PDF document, this string appeared to match the
+ PDFDocEncoding, or contained a UTF-16BE BOM mark to cause UTF-16 decoding
+ to occur.
+ """
+
+ autodetect_pdfdocencoding: bool
+ autodetect_utf16: bool
+ utf16_bom: bytes
+
+ def __new__(cls, value: Any) -> "TextStringObject":
+ if isinstance(value, bytes):
+ value = value.decode("charmap")
+ o = str.__new__(cls, value)
+ o.autodetect_utf16 = False
+ o.autodetect_pdfdocencoding = False
+ o.utf16_bom = b""
+ if value.startswith(("\xfe\xff", "\xff\xfe")):
+ o.autodetect_utf16 = True
+ o.utf16_bom = value[:2].encode("charmap")
+ else:
+ try:
+ encode_pdfdocencoding(o)
+ o.autodetect_pdfdocencoding = True
+ except UnicodeEncodeError:
+ o.autodetect_utf16 = True
+ return o
+
+ def clone(
+ self,
+ pdf_dest: Any,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "TextStringObject":
+ """Clone object into pdf_dest."""
+ obj = TextStringObject(self)
+ obj.autodetect_pdfdocencoding = self.autodetect_pdfdocencoding
+ obj.autodetect_utf16 = self.autodetect_utf16
+ obj.utf16_bom = self.utf16_bom
+ return cast(
+ "TextStringObject", self._reference_clone(obj, pdf_dest, force_duplicate)
+ )
+
+ @property
+ def original_bytes(self) -> bytes:
+ """
+ It is occasionally possible that a text string object gets created where
+ a byte string object was expected due to the autodetection mechanism --
+ if that occurs, this "original_bytes" property can be used to
+ back-calculate what the original encoded bytes were.
+ """
+ return self.get_original_bytes()
+
+ def get_original_bytes(self) -> bytes:
+ # We're a text string object, but the library is trying to get our raw
+ # bytes. This can happen if we auto-detected this string as text, but
+ # we were wrong. It's pretty common. Return the original bytes that
+ # would have been used to create this object, based upon the autodetect
+ # method.
+ if self.autodetect_utf16:
+ if self.utf16_bom == codecs.BOM_UTF16_LE:
+ return codecs.BOM_UTF16_LE + self.encode("utf-16le")
+ elif self.utf16_bom == codecs.BOM_UTF16_BE:
+ return codecs.BOM_UTF16_BE + self.encode("utf-16be")
+ else:
+ return self.encode("utf-16be")
+ elif self.autodetect_pdfdocencoding:
+ return encode_pdfdocencoding(self)
+ else:
+ raise Exception("no information about original bytes") # pragma: no cover
+
+ def get_encoded_bytes(self) -> bytes:
+ # Try to write the string out as a PDFDocEncoding encoded string. It's
+ # nicer to look at in the PDF file. Sadly, we take a performance hit
+ # here for trying...
+ try:
+ if self.autodetect_utf16:
+ raise UnicodeEncodeError("", "forced", -1, -1, "")
+ bytearr = encode_pdfdocencoding(self)
+ except UnicodeEncodeError:
+ if self.utf16_bom == codecs.BOM_UTF16_LE:
+ bytearr = codecs.BOM_UTF16_LE + self.encode("utf-16le")
+ elif self.utf16_bom == codecs.BOM_UTF16_BE:
+ bytearr = codecs.BOM_UTF16_BE + self.encode("utf-16be")
+ else:
+ bytearr = self.encode("utf-16be")
+ return bytearr
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ bytearr = self.get_encoded_bytes()
+ stream.write(b"(")
+ for c in bytearr:
+ if not chr(c).isalnum() and c != b" ":
+ # This:
+ # stream.write(rf"\{c:0>3o}".encode())
+ # gives
+ # https://github.com/davidhalter/parso/issues/207
+ stream.write(("\\%03o" % c).encode())
+ else:
+ stream.write(b_(chr(c)))
+ stream.write(b")")
+
+
+class NameObject(str, PdfObject): # noqa: SLOT000
+ delimiter_pattern = re.compile(rb"\s+|[\(\)<>\[\]{}/%]")
+ surfix = b"/"
+ renumber_table: ClassVar[Dict[str, bytes]] = {
+ "#": b"#23",
+ "(": b"#28",
+ ")": b"#29",
+ "/": b"#2F",
+ "%": b"#25",
+ **{chr(i): f"#{i:02X}".encode() for i in range(33)},
+ }
+
+ def clone(
+ self,
+ pdf_dest: Any,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "NameObject":
+ """Clone object into pdf_dest."""
+ return cast(
+ "NameObject",
+ self._reference_clone(NameObject(self), pdf_dest, force_duplicate),
+ )
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ stream.write(self.renumber())
+
+ def renumber(self) -> bytes:
+ out = self[0].encode("utf-8")
+ if out != b"/":
+ deprecate_no_replacement(
+ f"Incorrect first char in NameObject, should start with '/': ({self})",
+ "6.0.0",
+ )
+ for c in self[1:]:
+ if c > "~":
+ for x in c.encode("utf-8"):
+ out += f"#{x:02X}".encode()
+ else:
+ try:
+ out += self.renumber_table[c]
+ except KeyError:
+ out += c.encode("utf-8")
+ return out
+
+ @staticmethod
+ def unnumber(sin: bytes) -> bytes:
+ i = sin.find(b"#", 0)
+ while i >= 0:
+ try:
+ sin = sin[:i] + unhexlify(sin[i + 1 : i + 3]) + sin[i + 3 :]
+ i = sin.find(b"#", i + 1)
+ except ValueError:
+ # if the 2 characters after # can not be converted to hex
+ # we change nothing and carry on
+ i = i + 1
+ return sin
+
+ CHARSETS = ("utf-8", "gbk", "latin1")
+
+ @staticmethod
+ def read_from_stream(stream: StreamType, pdf: Any) -> "NameObject": # PdfReader
+ name = stream.read(1)
+ if name != NameObject.surfix:
+ raise PdfReadError("name read error")
+ name += read_until_regex(stream, NameObject.delimiter_pattern)
+ try:
+ # Name objects should represent irregular characters
+ # with a '#' followed by the symbol's hex number
+ name = NameObject.unnumber(name)
+ for enc in NameObject.CHARSETS:
+ try:
+ ret = name.decode(enc)
+ return NameObject(ret)
+ except Exception:
+ pass
+ raise UnicodeDecodeError("", name, 0, 0, "Code Not Found")
+ except (UnicodeEncodeError, UnicodeDecodeError) as e:
+ if not pdf.strict:
+ logger_warning(
+ f"Illegal character in NameObject ({name!r}), "
+ "you may need to adjust NameObject.CHARSETS",
+ __name__,
+ )
+ return NameObject(name.decode("charmap"))
+ else:
+ raise PdfReadError(
+ f"Illegal character in NameObject ({name!r}). "
+ "You may need to adjust NameObject.CHARSETS.",
+ ) from e
+
+
+def encode_pdfdocencoding(unicode_string: str) -> bytes:
+ retval = bytearray()
+ for c in unicode_string:
+ try:
+ retval += b_(chr(_pdfdoc_encoding_rev[c]))
+ except KeyError:
+ raise UnicodeEncodeError(
+ "pdfdocencoding", c, -1, -1, "does not exist in translation table"
+ )
+ return bytes(retval)
diff --git a/.venv/lib/python3.12/site-packages/pypdf/generic/_data_structures.py b/.venv/lib/python3.12/site-packages/pypdf/generic/_data_structures.py
new file mode 100644
index 00000000..87d68867
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pypdf/generic/_data_structures.py
@@ -0,0 +1,1616 @@
+# Copyright (c) 2006, Mathieu Fenniak
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+
+__author__ = "Mathieu Fenniak"
+__author_email__ = "biziqe@mathieu.fenniak.net"
+
+import logging
+import re
+import sys
+from io import BytesIO
+from math import ceil
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ List,
+ Optional,
+ Sequence,
+ Set,
+ Tuple,
+ Union,
+ cast,
+)
+
+from .._protocols import PdfReaderProtocol, PdfWriterProtocol, XmpInformationProtocol
+from .._utils import (
+ WHITESPACES,
+ StreamType,
+ b_,
+ deprecate_no_replacement,
+ deprecate_with_replacement,
+ logger_warning,
+ read_non_whitespace,
+ read_until_regex,
+ skip_over_comment,
+)
+from ..constants import (
+ CheckboxRadioButtonAttributes,
+ FieldDictionaryAttributes,
+ OutlineFontFlag,
+)
+from ..constants import FilterTypes as FT
+from ..constants import StreamAttributes as SA
+from ..constants import TypArguments as TA
+from ..constants import TypFitArguments as TF
+from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError
+from ._base import (
+ BooleanObject,
+ ByteStringObject,
+ FloatObject,
+ IndirectObject,
+ NameObject,
+ NullObject,
+ NumberObject,
+ PdfObject,
+ TextStringObject,
+)
+from ._fit import Fit
+from ._image_inline import (
+ extract_inline_A85,
+ extract_inline_AHx,
+ extract_inline_DCT,
+ extract_inline_default,
+ extract_inline_RL,
+)
+from ._utils import read_hex_string_from_stream, read_string_from_stream
+
+if sys.version_info >= (3, 11):
+ from typing import Self
+else:
+ from typing_extensions import Self
+
+logger = logging.getLogger(__name__)
+NumberSigns = b"+-"
+IndirectPattern = re.compile(rb"[+-]?(\d+)\s+(\d+)\s+R[^a-zA-Z]")
+
+
+class ArrayObject(List[Any], PdfObject):
+ def clone(
+ self,
+ pdf_dest: PdfWriterProtocol,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "ArrayObject":
+ """Clone object into pdf_dest."""
+ try:
+ if self.indirect_reference.pdf == pdf_dest and not force_duplicate: # type: ignore
+ return self
+ except Exception:
+ pass
+ arr = cast(
+ "ArrayObject",
+ self._reference_clone(ArrayObject(), pdf_dest, force_duplicate),
+ )
+ for data in self:
+ if isinstance(data, StreamObject):
+ dup = data._reference_clone(
+ data.clone(pdf_dest, force_duplicate, ignore_fields),
+ pdf_dest,
+ force_duplicate,
+ )
+ arr.append(dup.indirect_reference)
+ elif hasattr(data, "clone"):
+ arr.append(data.clone(pdf_dest, force_duplicate, ignore_fields))
+ else:
+ arr.append(data)
+ return arr
+
+ def items(self) -> Iterable[Any]:
+ """Emulate DictionaryObject.items for a list (index, object)."""
+ return enumerate(self)
+
+ def _to_lst(self, lst: Any) -> List[Any]:
+ # Convert to list, internal
+ if isinstance(lst, (list, tuple, set)):
+ pass
+ elif isinstance(lst, PdfObject):
+ lst = [lst]
+ elif isinstance(lst, str):
+ if lst[0] == "/":
+ lst = [NameObject(lst)]
+ else:
+ lst = [TextStringObject(lst)]
+ elif isinstance(lst, bytes):
+ lst = [ByteStringObject(lst)]
+ else: # for numbers,...
+ lst = [lst]
+ return lst
+
+ def __add__(self, lst: Any) -> "ArrayObject":
+ """
+ Allow extension by adding list or add one element only
+
+ Args:
+ lst: any list, tuples are extended the list.
+ other types(numbers,...) will be appended.
+ if str is passed it will be converted into TextStringObject
+ or NameObject (if starting with "/")
+ if bytes is passed it will be converted into ByteStringObject
+
+ Returns:
+ ArrayObject with all elements
+ """
+ temp = ArrayObject(self)
+ temp.extend(self._to_lst(lst))
+ return temp
+
+ def __iadd__(self, lst: Any) -> Self:
+ """
+ Allow extension by adding list or add one element only
+
+ Args:
+ lst: any list, tuples are extended the list.
+ other types(numbers,...) will be appended.
+ if str is passed it will be converted into TextStringObject
+ or NameObject (if starting with "/")
+ if bytes is passed it will be converted into ByteStringObject
+ """
+ self.extend(self._to_lst(lst))
+ return self
+
+ def __isub__(self, lst: Any) -> Self:
+ """Allow to remove items"""
+ for x in self._to_lst(lst):
+ try:
+ x = self.index(x)
+ del self[x]
+ except ValueError:
+ pass
+ return self
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ stream.write(b"[")
+ for data in self:
+ stream.write(b" ")
+ data.write_to_stream(stream)
+ stream.write(b" ]")
+
+ @staticmethod
+ def read_from_stream(
+ stream: StreamType,
+ pdf: Optional[PdfReaderProtocol],
+ forced_encoding: Union[None, str, List[str], Dict[int, str]] = None,
+ ) -> "ArrayObject":
+ arr = ArrayObject()
+ tmp = stream.read(1)
+ if tmp != b"[":
+ raise PdfReadError("Could not read array")
+ while True:
+ # skip leading whitespace
+ tok = stream.read(1)
+ while tok.isspace():
+ tok = stream.read(1)
+ stream.seek(-1, 1)
+ # check for array ending
+ peek_ahead = stream.read(1)
+ if peek_ahead == b"]":
+ break
+ stream.seek(-1, 1)
+ # read and append obj
+ arr.append(read_object(stream, pdf, forced_encoding))
+ return arr
+
+
+class DictionaryObject(Dict[Any, Any], PdfObject):
+ def clone(
+ self,
+ pdf_dest: PdfWriterProtocol,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "DictionaryObject":
+ """Clone object into pdf_dest."""
+ try:
+ if self.indirect_reference.pdf == pdf_dest and not force_duplicate: # type: ignore
+ return self
+ except Exception:
+ pass
+
+ visited: Set[Tuple[int, int]] = set() # (idnum, generation)
+ d__ = cast(
+ "DictionaryObject",
+ self._reference_clone(self.__class__(), pdf_dest, force_duplicate),
+ )
+ if ignore_fields is None:
+ ignore_fields = []
+ if len(d__.keys()) == 0:
+ d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited)
+ return d__
+
+ def _clone(
+ self,
+ src: "DictionaryObject",
+ pdf_dest: PdfWriterProtocol,
+ force_duplicate: bool,
+ ignore_fields: Optional[Sequence[Union[str, int]]],
+ visited: Set[Tuple[int, int]], # (idnum, generation)
+ ) -> None:
+ """
+ Update the object from src.
+
+ Args:
+ src: "DictionaryObject":
+ pdf_dest:
+ force_duplicate:
+ ignore_fields:
+ """
+ # first we remove for the ignore_fields
+ # that are for a limited number of levels
+ x = 0
+ assert ignore_fields is not None
+ ignore_fields = list(ignore_fields)
+ while x < len(ignore_fields):
+ if isinstance(ignore_fields[x], int):
+ if cast(int, ignore_fields[x]) <= 0:
+ del ignore_fields[x]
+ del ignore_fields[x]
+ continue
+ else:
+ ignore_fields[x] -= 1 # type:ignore
+ x += 1
+ # First check if this is a chain list, we need to loop to prevent recur
+ if any(
+ field not in ignore_fields
+ and field in src
+ and isinstance(src.raw_get(field), IndirectObject)
+ and isinstance(src[field], DictionaryObject)
+ and (
+ src.get("/Type", None) is None
+ or cast(DictionaryObject, src[field]).get("/Type", None) is None
+ or src.get("/Type", None)
+ == cast(DictionaryObject, src[field]).get("/Type", None)
+ )
+ for field in ["/Next", "/Prev", "/N", "/V"]
+ ):
+ ignore_fields = list(ignore_fields)
+ for lst in (("/Next", "/Prev"), ("/N", "/V")):
+ for k in lst:
+ objs = []
+ if (
+ k in src
+ and k not in self
+ and isinstance(src.raw_get(k), IndirectObject)
+ and isinstance(src[k], DictionaryObject)
+ # IF need to go further the idea is to check
+ # that the types are the same:
+ and (
+ src.get("/Type", None) is None
+ or cast(DictionaryObject, src[k]).get("/Type", None) is None
+ or src.get("/Type", None)
+ == cast(DictionaryObject, src[k]).get("/Type", None)
+ )
+ ):
+ cur_obj: Optional[DictionaryObject] = cast(
+ "DictionaryObject", src[k]
+ )
+ prev_obj: Optional[DictionaryObject] = self
+ while cur_obj is not None:
+ clon = cast(
+ "DictionaryObject",
+ cur_obj._reference_clone(
+ cur_obj.__class__(), pdf_dest, force_duplicate
+ ),
+ )
+ # check to see if we've previously processed our item
+ if clon.indirect_reference is not None:
+ idnum = clon.indirect_reference.idnum
+ generation = clon.indirect_reference.generation
+ if (idnum, generation) in visited:
+ cur_obj = None
+ break
+ visited.add((idnum, generation))
+ objs.append((cur_obj, clon))
+ assert prev_obj is not None
+ prev_obj[NameObject(k)] = clon.indirect_reference
+ prev_obj = clon
+ try:
+ if cur_obj == src:
+ cur_obj = None
+ else:
+ cur_obj = cast("DictionaryObject", cur_obj[k])
+ except Exception:
+ cur_obj = None
+ for s, c in objs:
+ c._clone(
+ s, pdf_dest, force_duplicate, ignore_fields, visited
+ )
+
+ for k, v in src.items():
+ if k not in ignore_fields:
+ if isinstance(v, StreamObject):
+ if not hasattr(v, "indirect_reference"):
+ v.indirect_reference = None
+ vv = v.clone(pdf_dest, force_duplicate, ignore_fields)
+ assert vv.indirect_reference is not None
+ self[k.clone(pdf_dest)] = vv.indirect_reference # type: ignore[attr-defined]
+ elif k not in self:
+ self[NameObject(k)] = (
+ v.clone(pdf_dest, force_duplicate, ignore_fields)
+ if hasattr(v, "clone")
+ else v
+ )
+
+ def raw_get(self, key: Any) -> Any:
+ return dict.__getitem__(self, key)
+
+ def get_inherited(self, key: str, default: Any = None) -> Any:
+ """
+ Returns the value of a key or from the parent if not found.
+ If not found returns default.
+
+ Args:
+ key: string identifying the field to return
+
+ default: default value to return
+
+ Returns:
+ Current key or inherited one, otherwise default value.
+ """
+ if key in self:
+ return self[key]
+ try:
+ if "/Parent" not in self:
+ return default
+ raise KeyError("not present")
+ except KeyError:
+ return cast("DictionaryObject", self["/Parent"].get_object()).get_inherited(
+ key, default
+ )
+
+ def __setitem__(self, key: Any, value: Any) -> Any:
+ if not isinstance(key, PdfObject):
+ raise ValueError("key must be PdfObject")
+ if not isinstance(value, PdfObject):
+ raise ValueError("value must be PdfObject")
+ return dict.__setitem__(self, key, value)
+
+ def setdefault(self, key: Any, value: Optional[Any] = None) -> Any:
+ if not isinstance(key, PdfObject):
+ raise ValueError("key must be PdfObject")
+ if not isinstance(value, PdfObject):
+ raise ValueError("value must be PdfObject")
+ return dict.setdefault(self, key, value) # type: ignore
+
+ def __getitem__(self, key: Any) -> PdfObject:
+ return dict.__getitem__(self, key).get_object()
+
+ @property
+ def xmp_metadata(self) -> Optional[XmpInformationProtocol]:
+ """
+ Retrieve XMP (Extensible Metadata Platform) data relevant to the this
+ object, if available.
+
+ See Table 347 — Additional entries in a metadata stream dictionary.
+
+ Returns:
+ Returns a :class:`~pypdf.xmp.XmpInformation` instance
+ that can be used to access XMP metadata from the document. Can also
+ return None if no metadata was found on the document root.
+ """
+ from ..xmp import XmpInformation
+
+ metadata = self.get("/Metadata", None)
+ if metadata is None:
+ return None
+ metadata = metadata.get_object()
+
+ if not isinstance(metadata, XmpInformation):
+ metadata = XmpInformation(metadata)
+ self[NameObject("/Metadata")] = metadata
+ return metadata
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ stream.write(b"<<\n")
+ for key, value in list(self.items()):
+ if len(key) > 2 and key[1] == "%" and key[-1] == "%":
+ continue
+ key.write_to_stream(stream, encryption_key)
+ stream.write(b" ")
+ value.write_to_stream(stream)
+ stream.write(b"\n")
+ stream.write(b">>")
+
+ @staticmethod
+ def read_from_stream(
+ stream: StreamType,
+ pdf: Optional[PdfReaderProtocol],
+ forced_encoding: Union[None, str, List[str], Dict[int, str]] = None,
+ ) -> "DictionaryObject":
+ def get_next_obj_pos(
+ p: int, p1: int, rem_gens: List[int], pdf: PdfReaderProtocol
+ ) -> int:
+ out = p1
+ for gen in rem_gens:
+ loc = pdf.xref[gen]
+ try:
+ out = min(out, min([x for x in loc.values() if p < x <= p1]))
+ except ValueError:
+ pass
+ return out
+
+ def read_unsized_from_stream(
+ stream: StreamType, pdf: PdfReaderProtocol
+ ) -> bytes:
+ # we are just pointing at beginning of the stream
+ eon = get_next_obj_pos(stream.tell(), 2**32, list(pdf.xref), pdf) - 1
+ curr = stream.tell()
+ rw = stream.read(eon - stream.tell())
+ p = rw.find(b"endstream")
+ if p < 0:
+ raise PdfReadError(
+ f"Unable to find 'endstream' marker for obj starting at {curr}."
+ )
+ stream.seek(curr + p + 9)
+ return rw[: p - 1]
+
+ tmp = stream.read(2)
+ if tmp != b"<<":
+ raise PdfReadError(
+ f"Dictionary read error at byte {hex(stream.tell())}: "
+ "stream must begin with '<<'"
+ )
+ data: Dict[Any, Any] = {}
+ while True:
+ tok = read_non_whitespace(stream)
+ if tok == b"\x00":
+ continue
+ elif tok == b"%":
+ stream.seek(-1, 1)
+ skip_over_comment(stream)
+ continue
+ if not tok:
+ raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
+
+ if tok == b">":
+ stream.read(1)
+ break
+ stream.seek(-1, 1)
+ try:
+ key = read_object(stream, pdf)
+ tok = read_non_whitespace(stream)
+ stream.seek(-1, 1)
+ value = read_object(stream, pdf, forced_encoding)
+ except Exception as exc:
+ if pdf is not None and pdf.strict:
+ raise PdfReadError(exc.__repr__())
+ logger_warning(exc.__repr__(), __name__)
+ retval = DictionaryObject()
+ retval.update(data)
+ return retval # return partial data
+
+ if not data.get(key):
+ data[key] = value
+ else:
+ # multiple definitions of key not permitted
+ msg = (
+ f"Multiple definitions in dictionary at byte "
+ f"{hex(stream.tell())} for key {key}"
+ )
+ if pdf is not None and pdf.strict:
+ raise PdfReadError(msg)
+ logger_warning(msg, __name__)
+
+ pos = stream.tell()
+ s = read_non_whitespace(stream)
+ if s == b"s" and stream.read(5) == b"tream":
+ eol = stream.read(1)
+ # odd PDF file output has spaces after 'stream' keyword but before EOL.
+ # patch provided by Danial Sandler
+ while eol == b" ":
+ eol = stream.read(1)
+ if eol not in (b"\n", b"\r"):
+ raise PdfStreamError("Stream data must be followed by a newline")
+ if eol == b"\r" and stream.read(1) != b"\n":
+ stream.seek(-1, 1)
+ # this is a stream object, not a dictionary
+ if SA.LENGTH not in data:
+ if pdf is not None and pdf.strict:
+ raise PdfStreamError("Stream length not defined")
+ else:
+ logger_warning(
+ f"Stream length not defined @pos={stream.tell()}", __name__
+ )
+ data[NameObject(SA.LENGTH)] = NumberObject(-1)
+ length = data[SA.LENGTH]
+ if isinstance(length, IndirectObject):
+ t = stream.tell()
+ assert pdf is not None # hint for mypy
+ length = pdf.get_object(length)
+ stream.seek(t, 0)
+ if length is None: # if the PDF is damaged
+ length = -1
+ pstart = stream.tell()
+ if length > 0:
+ data["__streamdata__"] = stream.read(length)
+ else:
+ data["__streamdata__"] = read_until_regex(
+ stream, re.compile(b"endstream")
+ )
+ e = read_non_whitespace(stream)
+ ndstream = stream.read(8)
+ if (e + ndstream) != b"endstream":
+ # (sigh) - the odd PDF file has a length that is too long, so
+ # we need to read backwards to find the "endstream" ending.
+ # ReportLab (unknown version) generates files with this bug,
+ # and Python users into PDF files tend to be our audience.
+ # we need to do this to correct the streamdata and chop off
+ # an extra character.
+ pos = stream.tell()
+ stream.seek(-10, 1)
+ end = stream.read(9)
+ if end == b"endstream":
+ # we found it by looking back one character further.
+ data["__streamdata__"] = data["__streamdata__"][:-1]
+ elif pdf is not None and not pdf.strict:
+ stream.seek(pstart, 0)
+ data["__streamdata__"] = read_unsized_from_stream(stream, pdf)
+ pos = stream.tell()
+ else:
+ stream.seek(pos, 0)
+ raise PdfReadError(
+ "Unable to find 'endstream' marker after stream at byte "
+ f"{hex(stream.tell())} (nd='{ndstream!r}', end='{end!r}')."
+ )
+ else:
+ stream.seek(pos, 0)
+ if "__streamdata__" in data:
+ return StreamObject.initialize_from_dictionary(data)
+ else:
+ retval = DictionaryObject()
+ retval.update(data)
+ return retval
+
+
+class TreeObject(DictionaryObject):
+ def __init__(self, dct: Optional[DictionaryObject] = None) -> None:
+ DictionaryObject.__init__(self)
+ if dct:
+ self.update(dct)
+
+ def hasChildren(self) -> bool: # deprecated
+ deprecate_with_replacement("hasChildren", "has_children", "4.0.0")
+ return self.has_children()
+
+ def has_children(self) -> bool:
+ return "/First" in self
+
+ def __iter__(self) -> Any:
+ return self.children()
+
+ def children(self) -> Iterable[Any]:
+ if not self.has_children():
+ return
+
+ child_ref = self[NameObject("/First")]
+ child = child_ref.get_object()
+ while True:
+ yield child
+ if child == self[NameObject("/Last")]:
+ return
+ child_ref = child.get(NameObject("/Next")) # type: ignore
+ if child_ref is None:
+ return
+ child = child_ref.get_object()
+
+ def add_child(self, child: Any, pdf: PdfWriterProtocol) -> None:
+ self.insert_child(child, None, pdf)
+
+ def inc_parent_counter_default(
+ self, parent: Union[None, IndirectObject, "TreeObject"], n: int
+ ) -> None:
+ if parent is None:
+ return
+ parent = cast("TreeObject", parent.get_object())
+ if "/Count" in parent:
+ parent[NameObject("/Count")] = NumberObject(
+ max(0, cast(int, parent[NameObject("/Count")]) + n)
+ )
+ self.inc_parent_counter_default(parent.get("/Parent", None), n)
+
+ def inc_parent_counter_outline(
+ self, parent: Union[None, IndirectObject, "TreeObject"], n: int
+ ) -> None:
+ if parent is None:
+ return
+ parent = cast("TreeObject", parent.get_object())
+ # BooleanObject requires comparison with == not is
+ opn = parent.get("/%is_open%", True) == True # noqa
+ c = cast(int, parent.get("/Count", 0))
+ if c < 0:
+ c = abs(c)
+ parent[NameObject("/Count")] = NumberObject((c + n) * (1 if opn else -1))
+ if not opn:
+ return
+ self.inc_parent_counter_outline(parent.get("/Parent", None), n)
+
+ def insert_child(
+ self,
+ child: Any,
+ before: Any,
+ pdf: PdfWriterProtocol,
+ inc_parent_counter: Optional[Callable[..., Any]] = None,
+ ) -> IndirectObject:
+ if inc_parent_counter is None:
+ inc_parent_counter = self.inc_parent_counter_default
+ child_obj = child.get_object()
+ child = child.indirect_reference # get_reference(child_obj)
+
+ prev: Optional[DictionaryObject]
+ if "/First" not in self: # no child yet
+ self[NameObject("/First")] = child
+ self[NameObject("/Count")] = NumberObject(0)
+ self[NameObject("/Last")] = child
+ child_obj[NameObject("/Parent")] = self.indirect_reference
+ inc_parent_counter(self, child_obj.get("/Count", 1))
+ if "/Next" in child_obj:
+ del child_obj["/Next"]
+ if "/Prev" in child_obj:
+ del child_obj["/Prev"]
+ return child
+ else:
+ prev = cast("DictionaryObject", self["/Last"])
+
+ while prev.indirect_reference != before:
+ if "/Next" in prev:
+ prev = cast("TreeObject", prev["/Next"])
+ else: # append at the end
+ prev[NameObject("/Next")] = cast("TreeObject", child)
+ child_obj[NameObject("/Prev")] = prev.indirect_reference
+ child_obj[NameObject("/Parent")] = self.indirect_reference
+ if "/Next" in child_obj:
+ del child_obj["/Next"]
+ self[NameObject("/Last")] = child
+ inc_parent_counter(self, child_obj.get("/Count", 1))
+ return child
+ try: # insert as first or in the middle
+ assert isinstance(prev["/Prev"], DictionaryObject)
+ prev["/Prev"][NameObject("/Next")] = child
+ child_obj[NameObject("/Prev")] = prev["/Prev"]
+ except Exception: # it means we are inserting in first position
+ del child_obj["/Next"]
+ child_obj[NameObject("/Next")] = prev
+ prev[NameObject("/Prev")] = child
+ child_obj[NameObject("/Parent")] = self.indirect_reference
+ inc_parent_counter(self, child_obj.get("/Count", 1))
+ return child
+
+ def _remove_node_from_tree(
+ self, prev: Any, prev_ref: Any, cur: Any, last: Any
+ ) -> None:
+ """
+ Adjust the pointers of the linked list and tree node count.
+
+ Args:
+ prev:
+ prev_ref:
+ cur:
+ last:
+ """
+ next_ref = cur.get(NameObject("/Next"), None)
+ if prev is None:
+ if next_ref:
+ # Removing first tree node
+ next_obj = next_ref.get_object()
+ del next_obj[NameObject("/Prev")]
+ self[NameObject("/First")] = next_ref
+ self[NameObject("/Count")] = NumberObject(
+ self[NameObject("/Count")] - 1 # type: ignore
+ )
+
+ else:
+ # Removing only tree node
+ self[NameObject("/Count")] = NumberObject(0)
+ del self[NameObject("/First")]
+ if NameObject("/Last") in self:
+ del self[NameObject("/Last")]
+ else:
+ if next_ref:
+ # Removing middle tree node
+ next_obj = next_ref.get_object()
+ next_obj[NameObject("/Prev")] = prev_ref
+ prev[NameObject("/Next")] = next_ref
+ else:
+ # Removing last tree node
+ assert cur == last
+ del prev[NameObject("/Next")]
+ self[NameObject("/Last")] = prev_ref
+ self[NameObject("/Count")] = NumberObject(self[NameObject("/Count")] - 1) # type: ignore
+
+ def remove_child(self, child: Any) -> None:
+ child_obj = child.get_object()
+ child = child_obj.indirect_reference
+
+ if NameObject("/Parent") not in child_obj:
+ raise ValueError("Removed child does not appear to be a tree item")
+ elif child_obj[NameObject("/Parent")] != self:
+ raise ValueError("Removed child is not a member of this tree")
+
+ found = False
+ prev_ref = None
+ prev = None
+ cur_ref: Optional[Any] = self[NameObject("/First")]
+ cur: Optional[Dict[str, Any]] = cur_ref.get_object() # type: ignore
+ last_ref = self[NameObject("/Last")]
+ last = last_ref.get_object()
+ while cur is not None:
+ if cur == child_obj:
+ self._remove_node_from_tree(prev, prev_ref, cur, last)
+ found = True
+ break
+
+ # Go to the next node
+ prev_ref = cur_ref
+ prev = cur
+ if NameObject("/Next") in cur:
+ cur_ref = cur[NameObject("/Next")]
+ cur = cur_ref.get_object()
+ else:
+ cur_ref = None
+ cur = None
+
+ if not found:
+ raise ValueError("Removal couldn't find item in tree")
+
+ _reset_node_tree_relationship(child_obj)
+
+ def remove_from_tree(self) -> None:
+ """Remove the object from the tree it is in."""
+ if NameObject("/Parent") not in self:
+ raise ValueError("Removed child does not appear to be a tree item")
+ else:
+ cast("TreeObject", self["/Parent"]).remove_child(self)
+
+ def emptyTree(self) -> None: # deprecated
+ deprecate_with_replacement("emptyTree", "empty_tree", "4.0.0")
+ self.empty_tree()
+
+ def empty_tree(self) -> None:
+ for child in self:
+ child_obj = child.get_object()
+ _reset_node_tree_relationship(child_obj)
+
+ if NameObject("/Count") in self:
+ del self[NameObject("/Count")]
+ if NameObject("/First") in self:
+ del self[NameObject("/First")]
+ if NameObject("/Last") in self:
+ del self[NameObject("/Last")]
+
+
+def _reset_node_tree_relationship(child_obj: Any) -> None:
+ """
+ Call this after a node has been removed from a tree.
+
+ This resets the nodes attributes in respect to that tree.
+
+ Args:
+ child_obj:
+ """
+ del child_obj[NameObject("/Parent")]
+ if NameObject("/Next") in child_obj:
+ del child_obj[NameObject("/Next")]
+ if NameObject("/Prev") in child_obj:
+ del child_obj[NameObject("/Prev")]
+
+
+class StreamObject(DictionaryObject):
+ def __init__(self) -> None:
+ self._data: Union[bytes, str] = b""
+ self.decoded_self: Optional[DecodedStreamObject] = None
+
+ def _clone(
+ self,
+ src: DictionaryObject,
+ pdf_dest: PdfWriterProtocol,
+ force_duplicate: bool,
+ ignore_fields: Optional[Sequence[Union[str, int]]],
+ visited: Set[Tuple[int, int]],
+ ) -> None:
+ """
+ Update the object from src.
+
+ Args:
+ src:
+ pdf_dest:
+ force_duplicate:
+ ignore_fields:
+ """
+ self._data = cast("StreamObject", src)._data
+ try:
+ decoded_self = cast("StreamObject", src).decoded_self
+ if decoded_self is None:
+ self.decoded_self = None
+ else:
+ self.decoded_self = cast(
+ "DecodedStreamObject",
+ decoded_self.clone(pdf_dest, force_duplicate, ignore_fields),
+ )
+ except Exception:
+ pass
+ super()._clone(src, pdf_dest, force_duplicate, ignore_fields, visited)
+
+ def get_data(self) -> Union[bytes, str]:
+ return self._data
+
+ def set_data(self, data: bytes) -> None:
+ self._data = data
+
+ def hash_value_data(self) -> bytes:
+ data = super().hash_value_data()
+ data += b_(self._data)
+ return data
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ self[NameObject(SA.LENGTH)] = NumberObject(len(self._data))
+ DictionaryObject.write_to_stream(self, stream)
+ del self[SA.LENGTH]
+ stream.write(b"\nstream\n")
+ stream.write(self._data)
+ stream.write(b"\nendstream")
+
+ @staticmethod
+ def initializeFromDictionary(
+ data: Dict[str, Any]
+ ) -> Union["EncodedStreamObject", "DecodedStreamObject"]:
+ deprecate_with_replacement(
+ "initializeFromDictionary", "initialize_from_dictionary", "5.0.0"
+ ) # pragma: no cover
+ return StreamObject.initialize_from_dictionary(data) # pragma: no cover
+
+ @staticmethod
+ def initialize_from_dictionary(
+ data: Dict[str, Any]
+ ) -> Union["EncodedStreamObject", "DecodedStreamObject"]:
+ retval: Union[EncodedStreamObject, DecodedStreamObject]
+ if SA.FILTER in data:
+ retval = EncodedStreamObject()
+ else:
+ retval = DecodedStreamObject()
+ retval._data = data["__streamdata__"]
+ del data["__streamdata__"]
+ del data[SA.LENGTH]
+ retval.update(data)
+ return retval
+
+ def flate_encode(self, level: int = -1) -> "EncodedStreamObject":
+ from ..filters import FlateDecode
+
+ if SA.FILTER in self:
+ f = self[SA.FILTER]
+ if isinstance(f, ArrayObject):
+ f = ArrayObject([NameObject(FT.FLATE_DECODE), *f])
+ try:
+ params = ArrayObject(
+ [NullObject(), *self.get(SA.DECODE_PARMS, ArrayObject())]
+ )
+ except TypeError:
+ # case of error where the * operator is not working (not an array
+ params = ArrayObject(
+ [NullObject(), self.get(SA.DECODE_PARMS, ArrayObject())]
+ )
+ else:
+ f = ArrayObject([NameObject(FT.FLATE_DECODE), f])
+ params = ArrayObject(
+ [NullObject(), self.get(SA.DECODE_PARMS, NullObject())]
+ )
+ else:
+ f = NameObject(FT.FLATE_DECODE)
+ params = None
+ retval = EncodedStreamObject()
+ retval.update(self)
+ retval[NameObject(SA.FILTER)] = f
+ if params is not None:
+ retval[NameObject(SA.DECODE_PARMS)] = params
+ retval._data = FlateDecode.encode(b_(self._data), level)
+ return retval
+
+ def decode_as_image(self) -> Any:
+ """
+ Try to decode the stream object as an image
+
+ Returns:
+ a PIL image if proper decoding has been found
+ Raises:
+ Exception: (any)during decoding to to invalid object or
+ errors during decoding will be reported
+ It is recommended to catch exceptions to prevent
+ stops in your program.
+ """
+ from ..filters import _xobj_to_image
+
+ if self.get("/Subtype", "") != "/Image":
+ try:
+ msg = f"{self.indirect_reference} does not seem to be an Image" # pragma: no cover
+ except AttributeError:
+ msg = f"{self.__repr__()} object does not seem to be an Image" # pragma: no cover
+ logger_warning(msg, __name__)
+ extension, byte_stream, img = _xobj_to_image(self)
+ if extension is None:
+ return None # pragma: no cover
+ return img
+
+
+class DecodedStreamObject(StreamObject):
+ pass
+
+
+class EncodedStreamObject(StreamObject):
+ def __init__(self) -> None:
+ self.decoded_self: Optional[DecodedStreamObject] = None
+
+ # This overrides the parent method:
+ def get_data(self) -> Union[bytes, str]:
+ from ..filters import decode_stream_data
+
+ if self.decoded_self is not None:
+ # cached version of decoded object
+ return self.decoded_self.get_data()
+ else:
+ # create decoded object
+ decoded = DecodedStreamObject()
+
+ decoded.set_data(b_(decode_stream_data(self)))
+ for key, value in list(self.items()):
+ if key not in (SA.LENGTH, SA.FILTER, SA.DECODE_PARMS):
+ decoded[key] = value
+ self.decoded_self = decoded
+ return decoded.get_data()
+
+ # This overrides the parent method:
+ def set_data(self, data: bytes) -> None: # deprecated
+ from ..filters import FlateDecode
+
+ if self.get(SA.FILTER, "") == FT.FLATE_DECODE:
+ if not isinstance(data, bytes):
+ raise TypeError("data must be bytes")
+ assert self.decoded_self is not None
+ self.decoded_self.set_data(data)
+ super().set_data(FlateDecode.encode(data))
+ else:
+ raise PdfReadError(
+ "Streams encoded with different filter from only FlateDecode is not supported"
+ )
+
+
+class ContentStream(DecodedStreamObject):
+ """
+ In order to be fast, this data structure can contain either:
+
+ * raw data in ._data
+ * parsed stream operations in ._operations.
+
+ At any time, ContentStream object can either have both of those fields defined,
+ or one field defined and the other set to None.
+
+ These fields are "rebuilt" lazily, when accessed:
+
+ * when .get_data() is called, if ._data is None, it is rebuilt from ._operations.
+ * when .operations is called, if ._operations is None, it is rebuilt from ._data.
+
+ Conversely, these fields can be invalidated:
+
+ * when .set_data() is called, ._operations is set to None.
+ * when .operations is set, ._data is set to None.
+ """
+
+ def __init__(
+ self,
+ stream: Any,
+ pdf: Any,
+ forced_encoding: Union[None, str, List[str], Dict[int, str]] = None,
+ ) -> None:
+ self.pdf = pdf
+
+ # The inner list has two elements:
+ # Element 0: List
+ # Element 1: str
+ self._operations: List[Tuple[Any, Any]] = []
+
+ # stream may be a StreamObject or an ArrayObject containing
+ # multiple StreamObjects to be cat'd together.
+ if stream is None:
+ super().set_data(b"")
+ else:
+ stream = stream.get_object()
+ if isinstance(stream, ArrayObject):
+ data = b""
+ for s in stream:
+ data += b_(s.get_object().get_data())
+ if len(data) == 0 or data[-1] != b"\n":
+ data += b"\n"
+ super().set_data(bytes(data))
+ else:
+ stream_data = stream.get_data()
+ assert stream_data is not None
+ super().set_data(b_(stream_data))
+ self.forced_encoding = forced_encoding
+
+ def clone(
+ self,
+ pdf_dest: Any,
+ force_duplicate: bool = False,
+ ignore_fields: Optional[Sequence[Union[str, int]]] = (),
+ ) -> "ContentStream":
+ """
+ Clone object into pdf_dest.
+
+ Args:
+ pdf_dest:
+ force_duplicate:
+ ignore_fields:
+
+ Returns:
+ The cloned ContentStream
+ """
+ try:
+ if self.indirect_reference.pdf == pdf_dest and not force_duplicate: # type: ignore
+ return self
+ except Exception:
+ pass
+
+ visited: Set[Tuple[int, int]] = set()
+ d__ = cast(
+ "ContentStream",
+ self._reference_clone(
+ self.__class__(None, None), pdf_dest, force_duplicate
+ ),
+ )
+ if ignore_fields is None:
+ ignore_fields = []
+ d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited)
+ return d__
+
+ def _clone(
+ self,
+ src: DictionaryObject,
+ pdf_dest: PdfWriterProtocol,
+ force_duplicate: bool,
+ ignore_fields: Optional[Sequence[Union[str, int]]],
+ visited: Set[Tuple[int, int]],
+ ) -> None:
+ """
+ Update the object from src.
+
+ Args:
+ src:
+ pdf_dest:
+ force_duplicate:
+ ignore_fields:
+ """
+ src_cs = cast("ContentStream", src)
+ super().set_data(b_(src_cs._data))
+ self.pdf = pdf_dest
+ self._operations = list(src_cs._operations)
+ self.forced_encoding = src_cs.forced_encoding
+ # no need to call DictionaryObjection or anything
+ # like super(DictionaryObject,self)._clone(src, pdf_dest, force_duplicate, ignore_fields, visited)
+
+ def _parse_content_stream(self, stream: StreamType) -> None:
+ # 7.8.2 Content Streams
+ stream.seek(0, 0)
+ operands: List[Union[int, str, PdfObject]] = []
+ while True:
+ peek = read_non_whitespace(stream)
+ if peek == b"" or peek == 0:
+ break
+ stream.seek(-1, 1)
+ if peek.isalpha() or peek in (b"'", b'"'):
+ operator = read_until_regex(stream, NameObject.delimiter_pattern)
+ if operator == b"BI":
+ # begin inline image - a completely different parsing
+ # mechanism is required, of course... thanks buddy...
+ assert operands == []
+ ii = self._read_inline_image(stream)
+ self._operations.append((ii, b"INLINE IMAGE"))
+ else:
+ self._operations.append((operands, operator))
+ operands = []
+ elif peek == b"%":
+ # If we encounter a comment in the content stream, we have to
+ # handle it here. Typically, read_object will handle
+ # encountering a comment -- but read_object assumes that
+ # following the comment must be the object we're trying to
+ # read. In this case, it could be an operator instead.
+ while peek not in (b"\r", b"\n", b""):
+ peek = stream.read(1)
+ else:
+ operands.append(read_object(stream, None, self.forced_encoding))
+
+ def _read_inline_image(self, stream: StreamType) -> Dict[str, Any]:
+ # begin reading just after the "BI" - begin image
+ # first read the dictionary of settings.
+ settings = DictionaryObject()
+ while True:
+ tok = read_non_whitespace(stream)
+ stream.seek(-1, 1)
+ if tok == b"I":
+ # "ID" - begin of image data
+ break
+ key = read_object(stream, self.pdf)
+ tok = read_non_whitespace(stream)
+ stream.seek(-1, 1)
+ value = read_object(stream, self.pdf)
+ settings[key] = value
+ # left at beginning of ID
+ tmp = stream.read(3)
+ assert tmp[:2] == b"ID"
+ filtr = settings.get("/F", settings.get("/Filter", "not set"))
+ savpos = stream.tell()
+ if isinstance(filtr, list):
+ filtr = filtr[0] # used forencoding
+ if "AHx" in filtr or "ASCIIHexDecode" in filtr:
+ data = extract_inline_AHx(stream)
+ elif "A85" in filtr or "ASCII85Decode" in filtr:
+ data = extract_inline_A85(stream)
+ elif "RL" in filtr or "RunLengthDecode" in filtr:
+ data = extract_inline_RL(stream)
+ elif "DCT" in filtr or "DCTDecode" in filtr:
+ data = extract_inline_DCT(stream)
+ elif filtr == "not set":
+ cs = settings.get("/CS", "")
+ if "RGB" in cs:
+ lcs = 3
+ elif "CMYK" in cs:
+ lcs = 4
+ else:
+ bits = settings.get(
+ "/BPC",
+ 8 if cs in {"/I", "/G", "/Indexed", "/DeviceGray"} else -1,
+ )
+ if bits > 0:
+ lcs = bits / 8.0
+ else:
+ data = extract_inline_default(stream)
+ lcs = -1
+ if lcs > 0:
+ data = stream.read(
+ ceil(cast(int, settings["/W"]) * lcs) * cast(int, settings["/H"])
+ )
+ ei = read_non_whitespace(stream)
+ stream.seek(-1, 1)
+ else:
+ data = extract_inline_default(stream)
+
+ ei = stream.read(3)
+ stream.seek(-1, 1)
+ if ei[0:2] != b"EI" or ei[2:3] not in WHITESPACES:
+ stream.seek(savpos, 0)
+ data = extract_inline_default(stream)
+ return {"settings": settings, "data": data}
+
+ # This overrides the parent method:
+ def get_data(self) -> bytes:
+ if not self._data:
+ new_data = BytesIO()
+ for operands, operator in self._operations:
+ if operator == b"INLINE IMAGE":
+ new_data.write(b"BI")
+ dict_text = BytesIO()
+ operands["settings"].write_to_stream(dict_text)
+ new_data.write(dict_text.getvalue()[2:-2])
+ new_data.write(b"ID ")
+ new_data.write(operands["data"])
+ new_data.write(b"EI")
+ else:
+ for op in operands:
+ op.write_to_stream(new_data)
+ new_data.write(b" ")
+ new_data.write(b_(operator))
+ new_data.write(b"\n")
+ self._data = new_data.getvalue()
+ return b_(self._data)
+
+ # This overrides the parent method:
+ def set_data(self, data: bytes) -> None:
+ super().set_data(data)
+ self._operations = []
+
+ @property
+ def operations(self) -> List[Tuple[Any, Any]]:
+ if not self._operations and self._data:
+ self._parse_content_stream(BytesIO(b_(self._data)))
+ self._data = b""
+ return self._operations
+
+ @operations.setter
+ def operations(self, operations: List[Tuple[Any, Any]]) -> None:
+ self._operations = operations
+ self._data = b""
+
+ def isolate_graphics_state(self) -> None:
+ if self._operations:
+ self._operations.insert(0, ([], "q"))
+ self._operations.append(([], "Q"))
+ elif self._data:
+ self._data = b"q\n" + b_(self._data) + b"\nQ\n"
+
+ # This overrides the parent method:
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if not self._data and self._operations:
+ self.get_data() # this ensures ._data is rebuilt
+ super().write_to_stream(stream, encryption_key)
+
+
+def read_object(
+ stream: StreamType,
+ pdf: Optional[PdfReaderProtocol],
+ forced_encoding: Union[None, str, List[str], Dict[int, str]] = None,
+) -> Union[PdfObject, int, str, ContentStream]:
+ tok = stream.read(1)
+ stream.seek(-1, 1) # reset to start
+ if tok == b"/":
+ return NameObject.read_from_stream(stream, pdf)
+ elif tok == b"<":
+ # hexadecimal string OR dictionary
+ peek = stream.read(2)
+ stream.seek(-2, 1) # reset to start
+ if peek == b"<<":
+ return DictionaryObject.read_from_stream(stream, pdf, forced_encoding)
+ else:
+ return read_hex_string_from_stream(stream, forced_encoding)
+ elif tok == b"[":
+ return ArrayObject.read_from_stream(stream, pdf, forced_encoding)
+ elif tok == b"t" or tok == b"f":
+ return BooleanObject.read_from_stream(stream)
+ elif tok == b"(":
+ return read_string_from_stream(stream, forced_encoding)
+ elif tok == b"e" and stream.read(6) == b"endobj":
+ stream.seek(-6, 1)
+ return NullObject()
+ elif tok == b"n":
+ return NullObject.read_from_stream(stream)
+ elif tok == b"%":
+ # comment
+ while tok not in (b"\r", b"\n"):
+ tok = stream.read(1)
+ # Prevents an infinite loop by raising an error if the stream is at
+ # the EOF
+ if len(tok) <= 0:
+ raise PdfStreamError("File ended unexpectedly.")
+ tok = read_non_whitespace(stream)
+ stream.seek(-1, 1)
+ return read_object(stream, pdf, forced_encoding)
+ elif tok in b"0123456789+-.":
+ # number object OR indirect reference
+ peek = stream.read(20)
+ stream.seek(-len(peek), 1) # reset to start
+ if IndirectPattern.match(peek) is not None:
+ assert pdf is not None # hint for mypy
+ return IndirectObject.read_from_stream(stream, pdf)
+ else:
+ return NumberObject.read_from_stream(stream)
+ else:
+ stream.seek(-20, 1)
+ raise PdfReadError(
+ f"Invalid Elementary Object starting with {tok!r} @{stream.tell()}: {stream.read(80).__repr__()}"
+ )
+
+
+class Field(TreeObject):
+ """
+ A class representing a field dictionary.
+
+ This class is accessed through
+ :meth:`get_fields()<pypdf.PdfReader.get_fields>`
+ """
+
+ def __init__(self, data: DictionaryObject) -> None:
+ DictionaryObject.__init__(self)
+ field_attributes = (
+ FieldDictionaryAttributes.attributes()
+ + CheckboxRadioButtonAttributes.attributes()
+ )
+ self.indirect_reference = data.indirect_reference
+ for attr in field_attributes:
+ try:
+ self[NameObject(attr)] = data[attr]
+ except KeyError:
+ pass
+ if isinstance(self.get("/V"), EncodedStreamObject):
+ d = cast(EncodedStreamObject, self[NameObject("/V")]).get_data()
+ if isinstance(d, bytes):
+ d_str = d.decode()
+ elif d is None:
+ d_str = ""
+ else:
+ raise Exception("Should never happen")
+ self[NameObject("/V")] = TextStringObject(d_str)
+
+ # TABLE 8.69 Entries common to all field dictionaries
+ @property
+ def field_type(self) -> Optional[NameObject]:
+ """Read-only property accessing the type of this field."""
+ return self.get(FieldDictionaryAttributes.FT)
+
+ @property
+ def parent(self) -> Optional[DictionaryObject]:
+ """Read-only property accessing the parent of this field."""
+ return self.get(FieldDictionaryAttributes.Parent)
+
+ @property
+ def kids(self) -> Optional["ArrayObject"]:
+ """Read-only property accessing the kids of this field."""
+ return self.get(FieldDictionaryAttributes.Kids)
+
+ @property
+ def name(self) -> Optional[str]:
+ """Read-only property accessing the name of this field."""
+ return self.get(FieldDictionaryAttributes.T)
+
+ @property
+ def alternate_name(self) -> Optional[str]:
+ """Read-only property accessing the alternate name of this field."""
+ return self.get(FieldDictionaryAttributes.TU)
+
+ @property
+ def mapping_name(self) -> Optional[str]:
+ """
+ Read-only property accessing the mapping name of this field.
+
+ This name is used by pypdf as a key in the dictionary returned by
+ :meth:`get_fields()<pypdf.PdfReader.get_fields>`
+ """
+ return self.get(FieldDictionaryAttributes.TM)
+
+ @property
+ def flags(self) -> Optional[int]:
+ """
+ Read-only property accessing the field flags, specifying various
+ characteristics of the field (see Table 8.70 of the PDF 1.7 reference).
+ """
+ return self.get(FieldDictionaryAttributes.Ff)
+
+ @property
+ def value(self) -> Optional[Any]:
+ """
+ Read-only property accessing the value of this field.
+
+ Format varies based on field type.
+ """
+ return self.get(FieldDictionaryAttributes.V)
+
+ @property
+ def default_value(self) -> Optional[Any]:
+ """Read-only property accessing the default value of this field."""
+ return self.get(FieldDictionaryAttributes.DV)
+
+ @property
+ def additional_actions(self) -> Optional[DictionaryObject]:
+ """
+ Read-only property accessing the additional actions dictionary.
+
+ This dictionary defines the field's behavior in response to trigger
+ events. See Section 8.5.2 of the PDF 1.7 reference.
+ """
+ return self.get(FieldDictionaryAttributes.AA)
+
+
+class Destination(TreeObject):
+ """
+ A class representing a destination within a PDF file.
+
+ See section 12.3.2 of the PDF 2.0 reference.
+
+ Args:
+ title: Title of this destination.
+ page: Reference to the page of this destination. Should
+ be an instance of :class:`IndirectObject<pypdf.generic.IndirectObject>`.
+ fit: How the destination is displayed.
+
+ Raises:
+ PdfReadError: If destination type is invalid.
+ """
+
+ node: Optional[
+ DictionaryObject
+ ] = None # node provide access to the original Object
+
+ def __init__(
+ self,
+ title: str,
+ page: Union[NumberObject, IndirectObject, NullObject, DictionaryObject],
+ fit: Fit,
+ ) -> None:
+ self._filtered_children: List[Any] = [] # used in PdfWriter
+
+ typ = fit.fit_type
+ args = fit.fit_args
+
+ DictionaryObject.__init__(self)
+ self[NameObject("/Title")] = TextStringObject(title)
+ self[NameObject("/Page")] = page
+ self[NameObject("/Type")] = typ
+
+ # from table 8.2 of the PDF 1.7 reference.
+ if typ == "/XYZ":
+ if len(args) < 1: # left is missing : should never occur
+ args.append(NumberObject(0.0))
+ if len(args) < 2: # top is missing
+ args.append(NumberObject(0.0))
+ if len(args) < 3: # zoom is missing
+ args.append(NumberObject(0.0))
+ (
+ self[NameObject(TA.LEFT)],
+ self[NameObject(TA.TOP)],
+ self[NameObject("/Zoom")],
+ ) = args
+ elif len(args) == 0:
+ pass
+ elif typ == TF.FIT_R:
+ (
+ self[NameObject(TA.LEFT)],
+ self[NameObject(TA.BOTTOM)],
+ self[NameObject(TA.RIGHT)],
+ self[NameObject(TA.TOP)],
+ ) = args
+ elif typ in [TF.FIT_H, TF.FIT_BH]:
+ try: # Preferred to be more robust not only to null parameters
+ (self[NameObject(TA.TOP)],) = args
+ except Exception:
+ (self[NameObject(TA.TOP)],) = (NullObject(),)
+ elif typ in [TF.FIT_V, TF.FIT_BV]:
+ try: # Preferred to be more robust not only to null parameters
+ (self[NameObject(TA.LEFT)],) = args
+ except Exception:
+ (self[NameObject(TA.LEFT)],) = (NullObject(),)
+ elif typ in [TF.FIT, TF.FIT_B]:
+ pass
+ else:
+ raise PdfReadError(f"Unknown Destination Type: {typ!r}")
+
+ @property
+ def dest_array(self) -> "ArrayObject":
+ return ArrayObject(
+ [self.raw_get("/Page"), self["/Type"]]
+ + [
+ self[x]
+ for x in ["/Left", "/Bottom", "/Right", "/Top", "/Zoom"]
+ if x in self
+ ]
+ )
+
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ stream.write(b"<<\n")
+ key = NameObject("/D")
+ key.write_to_stream(stream)
+ stream.write(b" ")
+ value = self.dest_array
+ value.write_to_stream(stream)
+
+ key = NameObject("/S")
+ key.write_to_stream(stream)
+ stream.write(b" ")
+ value_s = NameObject("/GoTo")
+ value_s.write_to_stream(stream)
+
+ stream.write(b"\n")
+ stream.write(b">>")
+
+ @property
+ def title(self) -> Optional[str]:
+ """Read-only property accessing the destination title."""
+ return self.get("/Title")
+
+ @property
+ def page(self) -> Optional[int]:
+ """Read-only property accessing the destination page number."""
+ return self.get("/Page")
+
+ @property
+ def typ(self) -> Optional[str]:
+ """Read-only property accessing the destination type."""
+ return self.get("/Type")
+
+ @property
+ def zoom(self) -> Optional[int]:
+ """Read-only property accessing the zoom factor."""
+ return self.get("/Zoom", None)
+
+ @property
+ def left(self) -> Optional[FloatObject]:
+ """Read-only property accessing the left horizontal coordinate."""
+ return self.get("/Left", None)
+
+ @property
+ def right(self) -> Optional[FloatObject]:
+ """Read-only property accessing the right horizontal coordinate."""
+ return self.get("/Right", None)
+
+ @property
+ def top(self) -> Optional[FloatObject]:
+ """Read-only property accessing the top vertical coordinate."""
+ return self.get("/Top", None)
+
+ @property
+ def bottom(self) -> Optional[FloatObject]:
+ """Read-only property accessing the bottom vertical coordinate."""
+ return self.get("/Bottom", None)
+
+ @property
+ def color(self) -> Optional["ArrayObject"]:
+ """Read-only property accessing the color in (R, G, B) with values 0.0-1.0."""
+ return self.get(
+ "/C", ArrayObject([FloatObject(0), FloatObject(0), FloatObject(0)])
+ )
+
+ @property
+ def font_format(self) -> Optional[OutlineFontFlag]:
+ """
+ Read-only property accessing the font type.
+
+ 1=italic, 2=bold, 3=both
+ """
+ return self.get("/F", 0)
+
+ @property
+ def outline_count(self) -> Optional[int]:
+ """
+ Read-only property accessing the outline count.
+
+ positive = expanded
+ negative = collapsed
+ absolute value = number of visible descendants at all levels
+ """
+ return self.get("/Count", None)
diff --git a/.venv/lib/python3.12/site-packages/pypdf/generic/_fit.py b/.venv/lib/python3.12/site-packages/pypdf/generic/_fit.py
new file mode 100644
index 00000000..4132f4b7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pypdf/generic/_fit.py
@@ -0,0 +1,168 @@
+from typing import Any, Optional, Tuple, Union
+
+
+class Fit:
+ def __init__(
+ self, fit_type: str, fit_args: Tuple[Union[None, float, Any], ...] = ()
+ ):
+ from ._base import FloatObject, NameObject, NullObject
+
+ self.fit_type = NameObject(fit_type)
+ self.fit_args = [
+ NullObject() if a is None or isinstance(a, NullObject) else FloatObject(a)
+ for a in fit_args
+ ]
+
+ @classmethod
+ def xyz(
+ cls,
+ left: Optional[float] = None,
+ top: Optional[float] = None,
+ zoom: Optional[float] = None,
+ ) -> "Fit":
+ """
+ Display the page designated by page, with the coordinates (left, top)
+ positioned at the upper-left corner of the window and the contents
+ of the page magnified by the factor zoom.
+
+ A null value for any of the parameters left, top, or zoom specifies
+ that the current value of that parameter is to be retained unchanged.
+
+ A zoom value of 0 has the same meaning as a null value.
+
+ Args:
+ left:
+ top:
+ zoom:
+
+ Returns:
+ The created fit object.
+ """
+ return Fit(fit_type="/XYZ", fit_args=(left, top, zoom))
+
+ @classmethod
+ def fit(cls) -> "Fit":
+ """
+ Display the page designated by page, with its contents magnified just
+ enough to fit the entire page within the window both horizontally and
+ vertically.
+
+ If the required horizontal and vertical magnification factors are
+ different, use the smaller of the two, centering the page within the
+ window in the other dimension.
+ """
+ return Fit(fit_type="/Fit")
+
+ @classmethod
+ def fit_horizontally(cls, top: Optional[float] = None) -> "Fit":
+ """
+ Display the page designated by page, with the vertical coordinate top
+ positioned at the top edge of the window and the contents of the page
+ magnified just enough to fit the entire width of the page within the
+ window.
+
+ A null value for ``top`` specifies that the current value of that
+ parameter is to be retained unchanged.
+
+ Args:
+ top:
+
+ Returns:
+ The created fit object.
+ """
+ return Fit(fit_type="/FitH", fit_args=(top,))
+
+ @classmethod
+ def fit_vertically(cls, left: Optional[float] = None) -> "Fit":
+ return Fit(fit_type="/FitV", fit_args=(left,))
+
+ @classmethod
+ def fit_rectangle(
+ cls,
+ left: Optional[float] = None,
+ bottom: Optional[float] = None,
+ right: Optional[float] = None,
+ top: Optional[float] = None,
+ ) -> "Fit":
+ """
+ Display the page designated by page, with its contents magnified
+ just enough to fit the rectangle specified by the coordinates
+ left, bottom, right, and top entirely within the window
+ both horizontally and vertically.
+
+ If the required horizontal and vertical magnification factors are
+ different, use the smaller of the two, centering the rectangle within
+ the window in the other dimension.
+
+ A null value for any of the parameters may result in unpredictable
+ behavior.
+
+ Args:
+ left:
+ bottom:
+ right:
+ top:
+
+ Returns:
+ The created fit object.
+ """
+ return Fit(fit_type="/FitR", fit_args=(left, bottom, right, top))
+
+ @classmethod
+ def fit_box(cls) -> "Fit":
+ """
+ Display the page designated by page, with its contents magnified just
+ enough to fit its bounding box entirely within the window both
+ horizontally and vertically.
+
+ If the required horizontal and vertical magnification factors are
+ different, use the smaller of the two, centering the bounding box
+ within the window in the other dimension.
+ """
+ return Fit(fit_type="/FitB")
+
+ @classmethod
+ def fit_box_horizontally(cls, top: Optional[float] = None) -> "Fit":
+ """
+ Display the page designated by page, with the vertical coordinate top
+ positioned at the top edge of the window and the contents of the page
+ magnified just enough to fit the entire width of its bounding box
+ within the window.
+
+ A null value for top specifies that the current value of that parameter
+ is to be retained unchanged.
+
+ Args:
+ top:
+
+ Returns:
+ The created fit object.
+ """
+ return Fit(fit_type="/FitBH", fit_args=(top,))
+
+ @classmethod
+ def fit_box_vertically(cls, left: Optional[float] = None) -> "Fit":
+ """
+ Display the page designated by page, with the horizontal coordinate
+ left positioned at the left edge of the window and the contents of the
+ page magnified just enough to fit the entire height of its bounding box
+ within the window.
+
+ A null value for left specifies that the current value of that
+ parameter is to be retained unchanged.
+
+ Args:
+ left:
+
+ Returns:
+ The created fit object.
+ """
+ return Fit(fit_type="/FitBV", fit_args=(left,))
+
+ def __str__(self) -> str:
+ if not self.fit_args:
+ return f"Fit({self.fit_type})"
+ return f"Fit({self.fit_type}, {self.fit_args})"
+
+
+DEFAULT_FIT = Fit.fit()
diff --git a/.venv/lib/python3.12/site-packages/pypdf/generic/_image_inline.py b/.venv/lib/python3.12/site-packages/pypdf/generic/_image_inline.py
new file mode 100644
index 00000000..41826ac3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pypdf/generic/_image_inline.py
@@ -0,0 +1,235 @@
+# Copyright (c) 2024, pypdf contributors
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+from io import BytesIO
+
+from .._utils import (
+ WHITESPACES,
+ StreamType,
+ read_non_whitespace,
+)
+from ..errors import PdfReadError
+
+logger = logging.getLogger(__name__)
+
+BUFFER_SIZE = 8192
+
+
+def extract_inline_AHx(stream: StreamType) -> bytes:
+ """
+ Extract HexEncoded Stream from Inline Image.
+ the stream will be moved onto the EI
+ """
+ data_out: bytes = b""
+ # Read data until delimiter > and EI as backup
+ # ignoring backup.
+ while True:
+ data_buffered = read_non_whitespace(stream) + stream.read(BUFFER_SIZE)
+ if not data_buffered:
+ raise PdfReadError("Unexpected end of stream")
+ pos_tok = data_buffered.find(b">")
+ if pos_tok >= 0: # found >
+ data_out += data_buffered[: (pos_tok + 1)]
+ stream.seek(-len(data_buffered) + pos_tok + 1, 1)
+ break
+ pos_ei = data_buffered.find(b"EI")
+ if pos_ei >= 0: # found EI
+ stream.seek(-len(data_buffered) + pos_ei - 1, 1)
+ c = stream.read(1)
+ while c in WHITESPACES:
+ stream.seek(-2, 1)
+ c = stream.read(1)
+ pos_ei -= 1
+ data_out += data_buffered[:pos_ei]
+ break
+ elif len(data_buffered) == 2:
+ data_out += data_buffered
+ raise PdfReadError("Unexpected end of stream")
+ else: # > nor EI found
+ data_out += data_buffered[:-2]
+ stream.seek(-2, 1)
+
+ ei_tok = read_non_whitespace(stream)
+ ei_tok += stream.read(2)
+ stream.seek(-3, 1)
+ if ei_tok[0:2] != b"EI" or not (ei_tok[2:3] == b"" or ei_tok[2:3] in WHITESPACES):
+ raise PdfReadError("EI stream not found")
+ return data_out
+
+
+def extract_inline_A85(stream: StreamType) -> bytes:
+ """
+ Extract A85 Stream from Inline Image.
+ the stream will be moved onto the EI
+ """
+ data_out: bytes = b""
+ # Read data up to delimiter ~>
+ # see §3.3.2 from PDF ref 1.7
+ while True:
+ data_buffered = read_non_whitespace(stream) + stream.read(BUFFER_SIZE)
+ if not data_buffered:
+ raise PdfReadError("Unexpected end of stream")
+ pos_tok = data_buffered.find(b"~>")
+ if pos_tok >= 0: # found!
+ data_out += data_buffered[: pos_tok + 2]
+ stream.seek(-len(data_buffered) + pos_tok + 2, 1)
+ break
+ elif len(data_buffered) == 2: # end of buffer
+ data_out += data_buffered
+ raise PdfReadError("Unexpected end of stream")
+ data_out += data_buffered[
+ :-2
+ ] # back by one char in case of in the middle of ~>
+ stream.seek(-2, 1)
+
+ ei_tok = read_non_whitespace(stream)
+ ei_tok += stream.read(2)
+ stream.seek(-3, 1)
+ if ei_tok[0:2] != b"EI" or not (ei_tok[2:3] == b"" or ei_tok[2:3] in WHITESPACES):
+ raise PdfReadError("EI stream not found")
+ return data_out
+
+
+def extract_inline_RL(stream: StreamType) -> bytes:
+ """
+ Extract RL Stream from Inline Image.
+ the stream will be moved onto the EI
+ """
+ data_out: bytes = b""
+ # Read data up to delimiter ~>
+ # see §3.3.4 from PDF ref 1.7
+ while True:
+ data_buffered = stream.read(BUFFER_SIZE)
+ if not data_buffered:
+ raise PdfReadError("Unexpected end of stream")
+ pos_tok = data_buffered.find(b"\x80")
+ if pos_tok >= 0: # found
+ data_out += data_buffered[: pos_tok + 1]
+ stream.seek(-len(data_buffered) + pos_tok + 1, 1)
+ break
+ data_out += data_buffered
+
+ ei_tok = read_non_whitespace(stream)
+ ei_tok += stream.read(2)
+ stream.seek(-3, 1)
+ if ei_tok[0:2] != b"EI" or not (ei_tok[2:3] == b"" or ei_tok[2:3] in WHITESPACES):
+ raise PdfReadError("EI stream not found")
+ return data_out
+
+
+def extract_inline_DCT(stream: StreamType) -> bytes:
+ """
+ Extract DCT (JPEG) Stream from Inline Image.
+ the stream will be moved onto the EI
+ """
+ data_out: bytes = b""
+ # Read Blocks of data (ID/Size/data) up to ID=FF/D9
+ # see https://www.digicamsoft.com/itu/itu-t81-36.html
+ notfirst = False
+ while True:
+ c = stream.read(1)
+ if notfirst or (c == b"\xff"):
+ data_out += c
+ if c != b"\xff":
+ continue
+ else:
+ notfirst = True
+ c = stream.read(1)
+ data_out += c
+ if c == b"\xff":
+ stream.seek(-1, 1) # pragma: no cover
+ elif c == b"\x00": # stuffing
+ pass
+ elif c == b"\xd9": # end
+ break
+ elif c in (
+ b"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc9\xca\xcb\xcc\xcd\xce\xcf"
+ b"\xda\xdb\xdc\xdd\xde\xdf"
+ b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xfe"
+ ):
+ c = stream.read(2)
+ data_out += c
+ sz = c[0] * 256 + c[1]
+ data_out += stream.read(sz - 2)
+ # else: pass
+
+ ei_tok = read_non_whitespace(stream)
+ ei_tok += stream.read(2)
+ stream.seek(-3, 1)
+ if ei_tok[0:2] != b"EI" or not (ei_tok[2:3] == b"" or ei_tok[2:3] in WHITESPACES):
+ raise PdfReadError("EI stream not found")
+ return data_out
+
+
+def extract_inline_default(stream: StreamType) -> bytes:
+ """
+ Legacy method
+ used by default
+ """
+ stream_out = BytesIO()
+ # Read the inline image, while checking for EI (End Image) operator.
+ while True:
+ data_buffered = stream.read(BUFFER_SIZE)
+ if not data_buffered:
+ raise PdfReadError("Unexpected end of stream")
+ pos_ei = data_buffered.find(
+ b"E"
+ ) # we can not look straight for "EI" because it may not have been loaded in the buffer
+
+ if pos_ei == -1:
+ stream_out.write(data_buffered)
+ else:
+ # Write out everything including E (the one from EI to be removed).
+ stream_out.write(data_buffered[0 : pos_ei + 1])
+ sav_pos_ei = stream_out.tell() - 1
+ # Seek back in the stream to read the E next.
+ stream.seek(pos_ei + 1 - len(data_buffered), 1)
+ saved_pos = stream.tell()
+ # Check for End Image
+ tok2 = stream.read(1) # I of "EI"
+ if tok2 != b"I":
+ stream.seek(saved_pos, 0)
+ continue
+ tok3 = stream.read(1) # possible space after "EI"
+ if tok3 not in WHITESPACES:
+ stream.seek(saved_pos, 0)
+ continue
+ while tok3 in WHITESPACES:
+ tok3 = stream.read(1)
+ if data_buffered[pos_ei - 1 : pos_ei] not in WHITESPACES and tok3 not in {
+ b"Q",
+ b"E",
+ }: # for Q ou EMC
+ stream.seek(saved_pos, 0)
+ continue
+ # Data contains [\s]EI[\s](Q|EMC): 4 chars are sufficients
+ # remove E(I) wrongly inserted earlier
+ stream_out.truncate(sav_pos_ei)
+ break
+
+ return stream_out.getvalue()
diff --git a/.venv/lib/python3.12/site-packages/pypdf/generic/_outline.py b/.venv/lib/python3.12/site-packages/pypdf/generic/_outline.py
new file mode 100644
index 00000000..4d6a47da
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pypdf/generic/_outline.py
@@ -0,0 +1,33 @@
+from typing import Union
+
+from .._utils import StreamType, deprecate_no_replacement
+from ._base import NameObject
+from ._data_structures import Destination
+
+
+class OutlineItem(Destination):
+ def write_to_stream(
+ self, stream: StreamType, encryption_key: Union[None, str, bytes] = None
+ ) -> None:
+ if encryption_key is not None: # deprecated
+ deprecate_no_replacement(
+ "the encryption_key parameter of write_to_stream", "5.0.0"
+ )
+ stream.write(b"<<\n")
+ for key in [
+ NameObject(x)
+ for x in ["/Title", "/Parent", "/First", "/Last", "/Next", "/Prev"]
+ if x in self
+ ]:
+ key.write_to_stream(stream)
+ stream.write(b" ")
+ value = self.raw_get(key)
+ value.write_to_stream(stream)
+ stream.write(b"\n")
+ key = NameObject("/Dest")
+ key.write_to_stream(stream)
+ stream.write(b" ")
+ value = self.dest_array
+ value.write_to_stream(stream)
+ stream.write(b"\n")
+ stream.write(b">>")
diff --git a/.venv/lib/python3.12/site-packages/pypdf/generic/_rectangle.py b/.venv/lib/python3.12/site-packages/pypdf/generic/_rectangle.py
new file mode 100644
index 00000000..690b5217
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pypdf/generic/_rectangle.py
@@ -0,0 +1,132 @@
+from typing import Any, Tuple, Union
+
+from ._base import FloatObject, NumberObject
+from ._data_structures import ArrayObject
+
+
+class RectangleObject(ArrayObject):
+ """
+ This class is used to represent *page boxes* in pypdf.
+
+ These boxes include:
+
+ * :attr:`artbox <pypdf._page.PageObject.artbox>`
+ * :attr:`bleedbox <pypdf._page.PageObject.bleedbox>`
+ * :attr:`cropbox <pypdf._page.PageObject.cropbox>`
+ * :attr:`mediabox <pypdf._page.PageObject.mediabox>`
+ * :attr:`trimbox <pypdf._page.PageObject.trimbox>`
+ """
+
+ def __init__(
+ self, arr: Union["RectangleObject", Tuple[float, float, float, float]]
+ ) -> None:
+ # must have four points
+ assert len(arr) == 4
+ # automatically convert arr[x] into NumberObject(arr[x]) if necessary
+ ArrayObject.__init__(self, [self._ensure_is_number(x) for x in arr]) # type: ignore
+
+ def _ensure_is_number(self, value: Any) -> Union[FloatObject, NumberObject]:
+ if not isinstance(value, (NumberObject, FloatObject)):
+ value = FloatObject(value)
+ return value
+
+ def scale(self, sx: float, sy: float) -> "RectangleObject":
+ return RectangleObject(
+ (
+ float(self.left) * sx,
+ float(self.bottom) * sy,
+ float(self.right) * sx,
+ float(self.top) * sy,
+ )
+ )
+
+ def __repr__(self) -> str:
+ return f"RectangleObject({list(self)!r})"
+
+ @property
+ def left(self) -> FloatObject:
+ return self[0]
+
+ @left.setter
+ def left(self, f: float) -> None:
+ self[0] = FloatObject(f)
+
+ @property
+ def bottom(self) -> FloatObject:
+ return self[1]
+
+ @bottom.setter
+ def bottom(self, f: float) -> None:
+ self[1] = FloatObject(f)
+
+ @property
+ def right(self) -> FloatObject:
+ return self[2]
+
+ @right.setter
+ def right(self, f: float) -> None:
+ self[2] = FloatObject(f)
+
+ @property
+ def top(self) -> FloatObject:
+ return self[3]
+
+ @top.setter
+ def top(self, f: float) -> None:
+ self[3] = FloatObject(f)
+
+ @property
+ def lower_left(self) -> Tuple[float, float]:
+ """
+ Property to read and modify the lower left coordinate of this box
+ in (x,y) form.
+ """
+ return self.left, self.bottom
+
+ @lower_left.setter
+ def lower_left(self, value: Tuple[float, float]) -> None:
+ self[0], self[1] = (self._ensure_is_number(x) for x in value)
+
+ @property
+ def lower_right(self) -> Tuple[float, float]:
+ """
+ Property to read and modify the lower right coordinate of this box
+ in (x,y) form.
+ """
+ return self.right, self.bottom
+
+ @lower_right.setter
+ def lower_right(self, value: Tuple[float, float]) -> None:
+ self[2], self[1] = (self._ensure_is_number(x) for x in value)
+
+ @property
+ def upper_left(self) -> Tuple[float, float]:
+ """
+ Property to read and modify the upper left coordinate of this box
+ in (x,y) form.
+ """
+ return self.left, self.top
+
+ @upper_left.setter
+ def upper_left(self, value: Tuple[float, float]) -> None:
+ self[0], self[3] = (self._ensure_is_number(x) for x in value)
+
+ @property
+ def upper_right(self) -> Tuple[float, float]:
+ """
+ Property to read and modify the upper right coordinate of this box
+ in (x,y) form.
+ """
+ return self.right, self.top
+
+ @upper_right.setter
+ def upper_right(self, value: Tuple[float, float]) -> None:
+ self[2], self[3] = (self._ensure_is_number(x) for x in value)
+
+ @property
+ def width(self) -> float:
+ return self.right - self.left
+
+ @property
+ def height(self) -> float:
+ return self.top - self.bottom
diff --git a/.venv/lib/python3.12/site-packages/pypdf/generic/_utils.py b/.venv/lib/python3.12/site-packages/pypdf/generic/_utils.py
new file mode 100644
index 00000000..fdcdc333
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pypdf/generic/_utils.py
@@ -0,0 +1,180 @@
+import codecs
+from typing import Dict, List, Tuple, Union
+
+from .._codecs import _pdfdoc_encoding
+from .._utils import StreamType, b_, logger_warning, read_non_whitespace
+from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfStreamError
+from ._base import ByteStringObject, TextStringObject
+
+
+def hex_to_rgb(value: str) -> Tuple[float, float, float]:
+ return tuple(int(value.lstrip("#")[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore
+
+
+def read_hex_string_from_stream(
+ stream: StreamType,
+ forced_encoding: Union[None, str, List[str], Dict[int, str]] = None,
+) -> Union["TextStringObject", "ByteStringObject"]:
+ stream.read(1)
+ txt = ""
+ x = b""
+ while True:
+ tok = read_non_whitespace(stream)
+ if not tok:
+ raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
+ if tok == b">":
+ break
+ x += tok
+ if len(x) == 2:
+ txt += chr(int(x, base=16))
+ x = b""
+ if len(x) == 1:
+ x += b"0"
+ if len(x) == 2:
+ txt += chr(int(x, base=16))
+ return create_string_object(b_(txt), forced_encoding)
+
+
+def read_string_from_stream(
+ stream: StreamType,
+ forced_encoding: Union[None, str, List[str], Dict[int, str]] = None,
+) -> Union["TextStringObject", "ByteStringObject"]:
+ tok = stream.read(1)
+ parens = 1
+ txt = []
+ while True:
+ tok = stream.read(1)
+ if not tok:
+ raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
+ if tok == b"(":
+ parens += 1
+ elif tok == b")":
+ parens -= 1
+ if parens == 0:
+ break
+ elif tok == b"\\":
+ tok = stream.read(1)
+ escape_dict = {
+ b"n": b"\n",
+ b"r": b"\r",
+ b"t": b"\t",
+ b"b": b"\b",
+ b"f": b"\f",
+ b"c": rb"\c",
+ b"(": b"(",
+ b")": b")",
+ b"/": b"/",
+ b"\\": b"\\",
+ b" ": b" ",
+ b"%": b"%",
+ b"<": b"<",
+ b">": b">",
+ b"[": b"[",
+ b"]": b"]",
+ b"#": b"#",
+ b"_": b"_",
+ b"&": b"&",
+ b"$": b"$",
+ }
+ try:
+ tok = escape_dict[tok]
+ except KeyError:
+ if b"0" <= tok <= b"7":
+ # "The number ddd may consist of one, two, or three
+ # octal digits; high-order overflow shall be ignored.
+ # Three octal digits shall be used, with leading zeros
+ # as needed, if the next character of the string is also
+ # a digit." (PDF reference 7.3.4.2, p 16)
+ for _ in range(2):
+ ntok = stream.read(1)
+ if b"0" <= ntok <= b"7":
+ tok += ntok
+ else:
+ stream.seek(-1, 1) # ntok has to be analyzed
+ break
+ tok = b_(chr(int(tok, base=8)))
+ elif tok in b"\n\r":
+ # This case is hit when a backslash followed by a line
+ # break occurs. If it's a multi-char EOL, consume the
+ # second character:
+ tok = stream.read(1)
+ if tok not in b"\n\r":
+ stream.seek(-1, 1)
+ # Then don't add anything to the actual string, since this
+ # line break was escaped:
+ tok = b""
+ else:
+ msg = f"Unexpected escaped string: {tok.decode('utf-8','ignore')}"
+ logger_warning(msg, __name__)
+ txt.append(tok)
+ return create_string_object(b"".join(txt), forced_encoding)
+
+
+def create_string_object(
+ string: Union[str, bytes],
+ forced_encoding: Union[None, str, List[str], Dict[int, str]] = None,
+) -> Union[TextStringObject, ByteStringObject]:
+ """
+ Create a ByteStringObject or a TextStringObject from a string to represent the string.
+
+ Args:
+ string: The data being used
+ forced_encoding: Typically None, or an encoding string
+
+ Returns:
+ A ByteStringObject
+
+ Raises:
+ TypeError: If string is not of type str or bytes.
+ """
+ if isinstance(string, str):
+ return TextStringObject(string)
+ elif isinstance(string, bytes):
+ if isinstance(forced_encoding, (list, dict)):
+ out = ""
+ for x in string:
+ try:
+ out += forced_encoding[x]
+ except Exception:
+ out += bytes((x,)).decode("charmap")
+ return TextStringObject(out)
+ elif isinstance(forced_encoding, str):
+ if forced_encoding == "bytes":
+ return ByteStringObject(string)
+ return TextStringObject(string.decode(forced_encoding))
+ else:
+ try:
+ if string.startswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):
+ retval = TextStringObject(string.decode("utf-16"))
+ retval.autodetect_utf16 = True
+ retval.utf16_bom = string[:2]
+ return retval
+ else:
+ # This is probably a big performance hit here, but we need
+ # to convert string objects into the text/unicode-aware
+ # version if possible... and the only way to check if that's
+ # possible is to try.
+ # Some strings are strings, some are just byte arrays.
+ retval = TextStringObject(decode_pdfdocencoding(string))
+ retval.autodetect_pdfdocencoding = True
+ return retval
+ except UnicodeDecodeError:
+ return ByteStringObject(string)
+ else:
+ raise TypeError("create_string_object should have str or unicode arg")
+
+
+def decode_pdfdocencoding(byte_array: bytes) -> str:
+ retval = ""
+ for b in byte_array:
+ c = _pdfdoc_encoding[b]
+ if c == "\u0000":
+ raise UnicodeDecodeError(
+ "pdfdocencoding",
+ bytearray(b),
+ -1,
+ -1,
+ "does not exist in translation table",
+ )
+ retval += c
+ return retval
diff --git a/.venv/lib/python3.12/site-packages/pypdf/generic/_viewerpref.py b/.venv/lib/python3.12/site-packages/pypdf/generic/_viewerpref.py
new file mode 100644
index 00000000..a12f2d44
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/pypdf/generic/_viewerpref.py
@@ -0,0 +1,164 @@
+# Copyright (c) 2023, Pubpub-ZZ
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from typing import (
+ Any,
+ List,
+ Optional,
+)
+
+from ._base import BooleanObject, NameObject, NumberObject
+from ._data_structures import ArrayObject, DictionaryObject
+
+f_obj = BooleanObject(False)
+
+
+class ViewerPreferences(DictionaryObject):
+ def _get_bool(self, key: str, deft: Optional[BooleanObject]) -> BooleanObject:
+ return self.get(key, deft)
+
+ def _set_bool(self, key: str, v: bool) -> None:
+ self[NameObject(key)] = BooleanObject(v is True)
+
+ def _get_name(self, key: str, deft: Optional[NameObject]) -> Optional[NameObject]:
+ return self.get(key, deft)
+
+ def _set_name(self, key: str, lst: List[str], v: NameObject) -> None:
+ if v[0] != "/":
+ raise ValueError(f"{v} is not starting with '/'")
+ if lst != [] and v not in lst:
+ raise ValueError(f"{v} is not par of acceptable values")
+ self[NameObject(key)] = NameObject(v)
+
+ def _get_arr(self, key: str, deft: Optional[List[Any]]) -> NumberObject:
+ return self.get(key, None if deft is None else ArrayObject(deft))
+
+ def _set_arr(self, key: str, v: Optional[ArrayObject]) -> None:
+ if v is None:
+ try:
+ del self[NameObject(key)]
+ except KeyError:
+ pass
+ return
+ if not isinstance(v, ArrayObject):
+ raise ValueError("ArrayObject is expected")
+ self[NameObject(key)] = v
+
+ def _get_int(self, key: str, deft: Optional[NumberObject]) -> NumberObject:
+ return self.get(key, deft)
+
+ def _set_int(self, key: str, v: int) -> None:
+ self[NameObject(key)] = NumberObject(v)
+
+ @property
+ def PRINT_SCALING(self) -> NameObject:
+ return NameObject("/PrintScaling")
+
+ def __new__(cls: Any, value: Any = None) -> "ViewerPreferences":
+ def _add_prop_bool(key: str, deft: Optional[BooleanObject]) -> property:
+ return property(
+ lambda self: self._get_bool(key, deft),
+ lambda self, v: self._set_bool(key, v),
+ None,
+ f"""
+ Returns/Modify the status of {key}, Returns {deft} if not defined
+ """,
+ )
+
+ def _add_prop_name(
+ key: str, lst: List[str], deft: Optional[NameObject]
+ ) -> property:
+ return property(
+ lambda self: self._get_name(key, deft),
+ lambda self, v: self._set_name(key, lst, v),
+ None,
+ f"""
+ Returns/Modify the status of {key}, Returns {deft} if not defined.
+ Acceptable values: {lst}
+ """,
+ )
+
+ def _add_prop_arr(key: str, deft: Optional[ArrayObject]) -> property:
+ return property(
+ lambda self: self._get_arr(key, deft),
+ lambda self, v: self._set_arr(key, v),
+ None,
+ f"""
+ Returns/Modify the status of {key}, Returns {deft} if not defined
+ """,
+ )
+
+ def _add_prop_int(key: str, deft: Optional[int]) -> property:
+ return property(
+ lambda self: self._get_int(key, deft),
+ lambda self, v: self._set_int(key, v),
+ None,
+ f"""
+ Returns/Modify the status of {key}, Returns {deft} if not defined
+ """,
+ )
+
+ cls.hide_toolbar = _add_prop_bool("/HideToolbar", f_obj)
+ cls.hide_menubar = _add_prop_bool("/HideMenubar", f_obj)
+ cls.hide_windowui = _add_prop_bool("/HideWindowUI", f_obj)
+ cls.fit_window = _add_prop_bool("/FitWindow", f_obj)
+ cls.center_window = _add_prop_bool("/CenterWindow", f_obj)
+ cls.display_doctitle = _add_prop_bool("/DisplayDocTitle", f_obj)
+
+ cls.non_fullscreen_pagemode = _add_prop_name(
+ "/NonFullScreenPageMode",
+ ["/UseNone", "/UseOutlines", "/UseThumbs", "/UseOC"],
+ NameObject("/UseNone"),
+ )
+ cls.direction = _add_prop_name(
+ "/Direction", ["/L2R", "/R2L"], NameObject("/L2R")
+ )
+ cls.view_area = _add_prop_name("/ViewArea", [], None)
+ cls.view_clip = _add_prop_name("/ViewClip", [], None)
+ cls.print_area = _add_prop_name("/PrintArea", [], None)
+ cls.print_clip = _add_prop_name("/PrintClip", [], None)
+ cls.print_scaling = _add_prop_name("/PrintScaling", [], None)
+ cls.duplex = _add_prop_name(
+ "/Duplex", ["/Simplex", "/DuplexFlipShortEdge", "/DuplexFlipLongEdge"], None
+ )
+ cls.pick_tray_by_pdfsize = _add_prop_bool("/PickTrayByPDFSize", None)
+ cls.print_pagerange = _add_prop_arr("/PrintPageRange", None)
+ cls.num_copies = _add_prop_int("/NumCopies", None)
+
+ cls.enforce = _add_prop_arr("/Enforce", ArrayObject())
+
+ return DictionaryObject.__new__(cls)
+
+ def __init__(self, obj: Optional[DictionaryObject] = None) -> None:
+ super().__init__(self)
+ if obj is not None:
+ self.update(obj.items())
+ try:
+ self.indirect_reference = obj.indirect_reference # type: ignore
+ except AttributeError:
+ pass