Warning: session_start(): Session cannot be started after headers have already been sent in /home/tvrreohg/public_html/manga.php on line 13
# 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 from io import BytesIO from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, cast from .._utils import ( WHITESPACES, StreamType, b_, deprecate_with_replacement, hex_str, logger_warning, read_non_whitespace, read_until_regex, skip_over_comment, ) from ..constants import ( CheckboxRadioButtonAttributes, FieldDictionaryAttributes, ) from ..constants import FilterTypes as FT from ..constants import OutlineFontFlag 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, FloatObject, IndirectObject, NameObject, NullObject, NumberObject, PdfObject, TextStringObject, ) from ._utils import read_hex_string_from_stream, read_string_from_stream logger = logging.getLogger(__name__) NumberSigns = b"+-" IndirectPattern = re.compile(rb"[+-]?(\d+)\s+(\d+)\s+R[^a-zA-Z]") class ArrayObject(list, PdfObject): def items(self) -> Iterable[Any]: """ Emulate DictionaryObject.items for a list (index, object) """ return enumerate(self) def write_to_stream( self, stream: StreamType, encryption_key: Union[None, str, bytes] ) -> None: stream.write(b"[") for data in self: stream.write(b" ") data.write_to_stream(stream, encryption_key) stream.write(b" ]") def writeToStream( self, stream: StreamType, encryption_key: Union[None, str, bytes] ) -> None: # pragma: no cover deprecate_with_replacement("writeToStream", "write_to_stream") self.write_to_stream(stream, encryption_key) @staticmethod def read_from_stream( stream: StreamType, pdf: Any, forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, ) -> "ArrayObject": # PdfReader 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 peekahead = stream.read(1) if peekahead == b"]": break stream.seek(-1, 1) # read and append obj arr.append(read_object(stream, pdf, forced_encoding)) return arr @staticmethod def readFromStream( stream: StreamType, pdf: Any # PdfReader ) -> "ArrayObject": # pragma: no cover deprecate_with_replacement("readFromStream", "read_from_stream") return ArrayObject.read_from_stream(stream, pdf) class DictionaryObject(dict, PdfObject): def raw_get(self, key: Any) -> Any: return dict.__getitem__(self, key) 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[PdfObject]: """ Retrieve XMP (Extensible Metadata Platform) data relevant to the this object, if available. Stability: Added in v1.12, will exist for all future v1.x releases. @return Returns a {@link #xmp.XmpInformation XmlInformation} 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 getXmpMetadata( self, ) -> Optional[PdfObject]: # pragma: no cover """ .. deprecated:: 1.28.3 Use :meth:`xmp_metadata` instead. """ deprecate_with_replacement("getXmpMetadata", "xmp_metadata") return self.xmp_metadata @property def xmpMetadata(self) -> Optional[PdfObject]: # pragma: no cover """ .. deprecated:: 1.28.3 Use :meth:`xmp_metadata` instead. """ deprecate_with_replacement("xmpMetadata", "xmp_metadata") return self.xmp_metadata def write_to_stream( self, stream: StreamType, encryption_key: Union[None, str, bytes] ) -> None: stream.write(b"<<\n") for key, value in list(self.items()): key.write_to_stream(stream, encryption_key) stream.write(b" ") value.write_to_stream(stream, encryption_key) stream.write(b"\n") stream.write(b">>") def writeToStream( self, stream: StreamType, encryption_key: Union[None, str, bytes] ) -> None: # pragma: no cover deprecate_with_replacement("writeToStream", "write_to_stream") self.write_to_stream(stream, encryption_key) @staticmethod def read_from_stream( stream: StreamType, pdf: Any, # PdfReader 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: Any ) -> int: # PdfReader l = pdf.xref[rem_gens[0]] for o in l: if p1 > l[o] and p < l[o]: p1 = l[o] if len(rem_gens) == 1: return p1 else: return get_next_obj_pos(p, p1, rem_gens[1:], pdf) def read_unsized_from_steam(stream: StreamType, pdf: Any) -> bytes: # PdfReader # 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_str(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_str(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": # read \n after if stream.read(1) != b"\n": stream.seek(-1, 1) # this is a stream object, not a dictionary if SA.LENGTH not in data: raise PdfStreamError("Stream length not defined") length = data[SA.LENGTH] if isinstance(length, IndirectObject): t = stream.tell() length = pdf.get_object(length) stream.seek(t, 0) pstart = stream.tell() data["__streamdata__"] = stream.read(length) 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 not pdf.strict: stream.seek(pstart, 0) data["__streamdata__"] = read_unsized_from_steam(stream, pdf) pos = stream.tell() else: stream.seek(pos, 0) raise PdfReadError( "Unable to find 'endstream' marker after stream at byte " f"{hex_str(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 @staticmethod def readFromStream( stream: StreamType, pdf: Any # PdfReader ) -> "DictionaryObject": # pragma: no cover deprecate_with_replacement("readFromStream", "read_from_stream") return DictionaryObject.read_from_stream(stream, pdf) class TreeObject(DictionaryObject): def __init__(self) -> None: DictionaryObject.__init__(self) def hasChildren(self) -> bool: # pragma: no cover 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 addChild(self, child: Any, pdf: Any) -> None: # pragma: no cover deprecate_with_replacement("addChild", "add_child") self.add_child(child, pdf) def add_child(self, child: Any, pdf: Any) -> None: # PdfWriter child_obj = child.get_object() child = pdf.get_reference(child_obj) assert isinstance(child, IndirectObject) prev: Optional[DictionaryObject] if "/First" not in self: self[NameObject("/First")] = child self[NameObject("/Count")] = NumberObject(0) prev = None else: prev = cast( DictionaryObject, self["/Last"] ) # TABLE 8.3 Entries in the outline dictionary self[NameObject("/Last")] = child self[NameObject("/Count")] = NumberObject(self[NameObject("/Count")] + 1) # type: ignore if prev: prev_ref = pdf.get_reference(prev) assert isinstance(prev_ref, IndirectObject) child_obj[NameObject("/Prev")] = prev_ref prev[NameObject("/Next")] = child parent_ref = pdf.get_reference(self) assert isinstance(parent_ref, IndirectObject) child_obj[NameObject("/Parent")] = parent_ref def removeChild(self, child: Any) -> None: # pragma: no cover deprecate_with_replacement("removeChild", "remove_child") self.remove_child(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.""" 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 assert self[NameObject("/Count")] == 1 del self[NameObject("/Count")] 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() 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: # pragma: no cover 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. """ 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: Optional[str] = None self.decoded_self: Optional[DecodedStreamObject] = None def hash_value_data(self) -> bytes: data = super().hash_value_data() data += b_(self._data) return data @property def decodedSelf(self) -> Optional["DecodedStreamObject"]: # pragma: no cover deprecate_with_replacement("decodedSelf", "decoded_self") return self.decoded_self @decodedSelf.setter def decodedSelf(self, value: "DecodedStreamObject") -> None: # pragma: no cover deprecate_with_replacement("decodedSelf", "decoded_self") self.decoded_self = value @property def _data(self) -> Any: return self.__data @_data.setter def _data(self, value: Any) -> None: self.__data = value def write_to_stream( self, stream: StreamType, encryption_key: Union[None, str, bytes] ) -> None: self[NameObject(SA.LENGTH)] = NumberObject(len(self._data)) DictionaryObject.write_to_stream(self, stream, encryption_key) del self[SA.LENGTH] stream.write(b"\nstream\n") data = self._data if encryption_key: from .._security import RC4_encrypt data = RC4_encrypt(encryption_key, data) stream.write(data) stream.write(b"\nendstream") @staticmethod def initializeFromDictionary( data: Dict[str, Any] ) -> Union["EncodedStreamObject", "DecodedStreamObject"]: # pragma: no cover return StreamObject.initialize_from_dictionary(data) @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 flateEncode(self) -> "EncodedStreamObject": # pragma: no cover deprecate_with_replacement("flateEncode", "flate_encode") return self.flate_encode() def flate_encode(self) -> "EncodedStreamObject": from ..filters import FlateDecode if SA.FILTER in self: f = self[SA.FILTER] if isinstance(f, ArrayObject): f.insert(0, NameObject(FT.FLATE_DECODE)) else: newf = ArrayObject() newf.append(NameObject("/FlateDecode")) newf.append(f) f = newf else: f = NameObject("/FlateDecode") retval = EncodedStreamObject() retval[NameObject(SA.FILTER)] = f retval._data = FlateDecode.encode(self._data) return retval class DecodedStreamObject(StreamObject): def get_data(self) -> Any: return self._data def set_data(self, data: Any) -> Any: self._data = data def getData(self) -> Any: # pragma: no cover deprecate_with_replacement("getData", "get_data") return self._data def setData(self, data: Any) -> None: # pragma: no cover deprecate_with_replacement("setData", "set_data") self.set_data(data) class EncodedStreamObject(StreamObject): def __init__(self) -> None: self.decoded_self: Optional[DecodedStreamObject] = None @property def decodedSelf(self) -> Optional["DecodedStreamObject"]: # pragma: no cover deprecate_with_replacement("decodedSelf", "decoded_self") return self.decoded_self @decodedSelf.setter def decodedSelf(self, value: DecodedStreamObject) -> None: # pragma: no cover deprecate_with_replacement("decodedSelf", "decoded_self") self.decoded_self = value def get_data(self) -> Union[None, str, bytes]: 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._data = 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._data def getData(self) -> Union[None, str, bytes]: # pragma: no cover deprecate_with_replacement("getData", "get_data") return self.get_data() def set_data(self, data: Any) -> None: # pragma: no cover raise PdfReadError("Creating EncodedStreamObject is not currently supported") def setData(self, data: Any) -> None: # pragma: no cover deprecate_with_replacement("setData", "set_data") return self.set_data(data) class ContentStream(DecodedStreamObject): 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: # [0] : List # [1] : str self.operations: List[Tuple[Any, Any]] = [] # stream may be a StreamObject or an ArrayObject containing # multiple StreamObjects to be cat'd together. 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" stream_bytes = BytesIO(data) else: stream_data = stream.get_data() assert stream_data is not None stream_data_bytes = b_(stream_data) stream_bytes = BytesIO(stream_data_bytes) self.forced_encoding = forced_encoding self.__parse_content_stream(stream_bytes) def __parse_content_stream(self, stream: StreamType) -> None: 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, True) 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"): 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" data = BytesIO() # Read the inline image, while checking for EI (End Image) operator. while True: # Read 8 kB at a time and check if the chunk contains the E operator. buf = stream.read(8192) # We have reached the end of the stream, but haven't found the EI operator. if not buf: raise PdfReadError("Unexpected end of stream") loc = buf.find(b"E") if loc == -1: data.write(buf) else: # Write out everything before the E. data.write(buf[0:loc]) # Seek back in the stream to read the E next. stream.seek(loc - len(buf), 1) tok = stream.read(1) # Check for End Image tok2 = stream.read(1) if tok2 == b"I" and buf[loc - 1 : loc] in WHITESPACES: # Data can contain [\s]EI, so check for the separator \s; 4 chars suffisent Q operator not required. tok3 = stream.read(1) info = tok + tok2 # We need to find at least one whitespace after. has_q_whitespace = False while tok3 in WHITESPACES: has_q_whitespace = True info += tok3 tok3 = stream.read(1) if has_q_whitespace: stream.seek(-1, 1) break else: stream.seek(-1, 1) data.write(info) else: stream.seek(-1, 1) data.write(tok) return {"settings": settings, "data": data.getvalue()} @property def _data(self) -> bytes: newdata = BytesIO() for operands, operator in self.operations: if operator == b"INLINE IMAGE": newdata.write(b"BI") dicttext = BytesIO() operands["settings"].write_to_stream(dicttext, None) newdata.write(dicttext.getvalue()[2:-2]) newdata.write(b"ID ") newdata.write(operands["data"]) newdata.write(b"EI") else: for op in operands: op.write_to_stream(newdata, None) newdata.write(b" ") newdata.write(b_(operator)) newdata.write(b"\n") return newdata.getvalue() @_data.setter def _data(self, value: Union[str, bytes]) -> None: self.__parse_content_stream(BytesIO(b_(value))) def read_object( stream: StreamType, pdf: Any, # PdfReader 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: 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()` """ def __init__(self, data: Dict[str, Any]) -> None: DictionaryObject.__init__(self) field_attributes = ( FieldDictionaryAttributes.attributes() + CheckboxRadioButtonAttributes.attributes() ) for attr in field_attributes: try: self[NameObject(attr)] = data[attr] except KeyError: pass # 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 fieldType(self) -> Optional[NameObject]: # pragma: no cover """ .. deprecated:: 1.28.3 Use :py:attr:`field_type` instead. """ deprecate_with_replacement("fieldType", "field_type") return self.field_type @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 altName(self) -> Optional[str]: # pragma: no cover """ .. deprecated:: 1.28.3 Use :py:attr:`alternate_name` instead. """ deprecate_with_replacement("altName", "alternate_name") return self.alternate_name @property def mapping_name(self) -> Optional[str]: """ Read-only property accessing the mapping name of this field. This name is used by PyPDF2 as a key in the dictionary returned by :meth:`get_fields()` """ return self.get(FieldDictionaryAttributes.TM) @property def mappingName(self) -> Optional[str]: # pragma: no cover """ .. deprecated:: 1.28.3 Use :py:attr:`mapping_name` instead. """ deprecate_with_replacement("mappingName", "mapping_name") return self.mapping_name @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 defaultValue(self) -> Optional[Any]: # pragma: no cover """ .. deprecated:: 1.28.3 Use :py:attr:`default_value` instead. """ deprecate_with_replacement("defaultValue", "default_value") return self.default_value @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) @property def additionalActions(self) -> Optional[DictionaryObject]: # pragma: no cover """ .. deprecated:: 1.28.3 Use :py:attr:`additional_actions` instead. """ deprecate_with_replacement("additionalActions", "additional_actions") return self.additional_actions class Destination(TreeObject): """ A class representing a destination within a PDF file. See section 8.2.1 of the PDF 1.6 reference. :param str title: Title of this destination. :param IndirectObject page: Reference to the page of this destination. Should be an instance of :class:`IndirectObject`. :param str typ: How the destination is displayed. :param args: Additional arguments may be necessary depending on the type. :raises PdfReadError: If destination type is invalid. .. list-table:: Valid ``typ`` arguments (see PDF spec for details) :widths: 50 50 * - /Fit - No additional arguments * - /XYZ - [left] [top] [zoomFactor] * - /FitH - [top] * - /FitV - [left] * - /FitR - [left] [bottom] [right] [top] * - /FitB - No additional arguments * - /FitBH - [top] * - /FitBV - [left] """ def __init__( self, title: str, page: Union[NumberObject, IndirectObject, NullObject, DictionaryObject], typ: Union[str, NumberObject], *args: Any, # ZoomArgType ) -> None: 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": ( self[NameObject(TA.LEFT)], self[NameObject(TA.TOP)], self[NameObject("/Zoom")], ) = args 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: # Prefered 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: # Prefered 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 getDestArray(self) -> ArrayObject: # pragma: no cover """ .. deprecated:: 1.28.3 Use :py:attr:`dest_array` instead. """ deprecate_with_replacement("getDestArray", "dest_array") return self.dest_array def write_to_stream( self, stream: StreamType, encryption_key: Union[None, str, bytes] ) -> None: stream.write(b"<<\n") key = NameObject("/D") key.write_to_stream(stream, encryption_key) stream.write(b" ") value = self.dest_array value.write_to_stream(stream, encryption_key) key = NameObject("/S") key.write_to_stream(stream, encryption_key) stream.write(b" ") value_s = NameObject("/GoTo") value_s.write_to_stream(stream, encryption_key) 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 descendents at all levels """ return self.get("/Count", None)