updates
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import codecs
|
||||
import collections
|
||||
@@ -6,24 +8,33 @@ import os
|
||||
import re
|
||||
import time
|
||||
import zlib
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from typing import IO
|
||||
|
||||
_DictBase = collections.UserDict[str | bytes, Any]
|
||||
else:
|
||||
_DictBase = collections.UserDict
|
||||
|
||||
|
||||
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
|
||||
# on page 656
|
||||
def encode_text(s):
|
||||
def encode_text(s: str) -> bytes:
|
||||
return codecs.BOM_UTF16_BE + s.encode("utf_16_be")
|
||||
|
||||
|
||||
PDFDocEncoding = {
|
||||
0x16: "\u0017",
|
||||
0x18: "\u02D8",
|
||||
0x19: "\u02C7",
|
||||
0x1A: "\u02C6",
|
||||
0x1B: "\u02D9",
|
||||
0x1C: "\u02DD",
|
||||
0x1D: "\u02DB",
|
||||
0x1E: "\u02DA",
|
||||
0x1F: "\u02DC",
|
||||
0x18: "\u02d8",
|
||||
0x19: "\u02c7",
|
||||
0x1A: "\u02c6",
|
||||
0x1B: "\u02d9",
|
||||
0x1C: "\u02dd",
|
||||
0x1D: "\u02db",
|
||||
0x1E: "\u02da",
|
||||
0x1F: "\u02dc",
|
||||
0x80: "\u2022",
|
||||
0x81: "\u2020",
|
||||
0x82: "\u2021",
|
||||
@@ -33,33 +44,33 @@ PDFDocEncoding = {
|
||||
0x86: "\u0192",
|
||||
0x87: "\u2044",
|
||||
0x88: "\u2039",
|
||||
0x89: "\u203A",
|
||||
0x89: "\u203a",
|
||||
0x8A: "\u2212",
|
||||
0x8B: "\u2030",
|
||||
0x8C: "\u201E",
|
||||
0x8D: "\u201C",
|
||||
0x8E: "\u201D",
|
||||
0x8C: "\u201e",
|
||||
0x8D: "\u201c",
|
||||
0x8E: "\u201d",
|
||||
0x8F: "\u2018",
|
||||
0x90: "\u2019",
|
||||
0x91: "\u201A",
|
||||
0x91: "\u201a",
|
||||
0x92: "\u2122",
|
||||
0x93: "\uFB01",
|
||||
0x94: "\uFB02",
|
||||
0x93: "\ufb01",
|
||||
0x94: "\ufb02",
|
||||
0x95: "\u0141",
|
||||
0x96: "\u0152",
|
||||
0x97: "\u0160",
|
||||
0x98: "\u0178",
|
||||
0x99: "\u017D",
|
||||
0x99: "\u017d",
|
||||
0x9A: "\u0131",
|
||||
0x9B: "\u0142",
|
||||
0x9C: "\u0153",
|
||||
0x9D: "\u0161",
|
||||
0x9E: "\u017E",
|
||||
0xA0: "\u20AC",
|
||||
0x9E: "\u017e",
|
||||
0xA0: "\u20ac",
|
||||
}
|
||||
|
||||
|
||||
def decode_text(b):
|
||||
def decode_text(b: bytes) -> str:
|
||||
if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE:
|
||||
return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be")
|
||||
else:
|
||||
@@ -73,47 +84,53 @@ class PdfFormatError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def check_format_condition(condition, error_message):
|
||||
def check_format_condition(condition: bool, error_message: str) -> None:
|
||||
if not condition:
|
||||
raise PdfFormatError(error_message)
|
||||
|
||||
|
||||
class IndirectReference(
|
||||
collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"])
|
||||
):
|
||||
def __str__(self):
|
||||
return "%s %s R" % self
|
||||
class IndirectReferenceTuple(NamedTuple):
|
||||
object_id: int
|
||||
generation: int
|
||||
|
||||
def __bytes__(self):
|
||||
|
||||
class IndirectReference(IndirectReferenceTuple):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.object_id} {self.generation} R"
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return self.__str__().encode("us-ascii")
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
other.__class__ is self.__class__
|
||||
and other.object_id == self.object_id
|
||||
and other.generation == self.generation
|
||||
)
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if self.__class__ is not other.__class__:
|
||||
return False
|
||||
assert isinstance(other, IndirectReference)
|
||||
return other.object_id == self.object_id and other.generation == self.generation
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not (self == other)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.object_id, self.generation))
|
||||
|
||||
|
||||
class IndirectObjectDef(IndirectReference):
|
||||
def __str__(self):
|
||||
return "%s %s obj" % self
|
||||
def __str__(self) -> str:
|
||||
return f"{self.object_id} {self.generation} obj"
|
||||
|
||||
|
||||
class XrefTable:
|
||||
def __init__(self):
|
||||
self.existing_entries = {} # object ID => (offset, generation)
|
||||
self.new_entries = {} # object ID => (offset, generation)
|
||||
def __init__(self) -> None:
|
||||
self.existing_entries: dict[int, tuple[int, int]] = (
|
||||
{}
|
||||
) # object ID => (offset, generation)
|
||||
self.new_entries: dict[int, tuple[int, int]] = (
|
||||
{}
|
||||
) # object ID => (offset, generation)
|
||||
self.deleted_entries = {0: 65536} # object ID => generation
|
||||
self.reading_finished = False
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
def __setitem__(self, key: int, value: tuple[int, int]) -> None:
|
||||
if self.reading_finished:
|
||||
self.new_entries[key] = value
|
||||
else:
|
||||
@@ -121,13 +138,13 @@ class XrefTable:
|
||||
if key in self.deleted_entries:
|
||||
del self.deleted_entries[key]
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: int) -> tuple[int, int]:
|
||||
try:
|
||||
return self.new_entries[key]
|
||||
except KeyError:
|
||||
return self.existing_entries[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: int) -> None:
|
||||
if key in self.new_entries:
|
||||
generation = self.new_entries[key][1] + 1
|
||||
del self.new_entries[key]
|
||||
@@ -138,34 +155,32 @@ class XrefTable:
|
||||
elif key in self.deleted_entries:
|
||||
generation = self.deleted_entries[key]
|
||||
else:
|
||||
msg = (
|
||||
"object ID " + str(key) + " cannot be deleted because it doesn't exist"
|
||||
)
|
||||
msg = f"object ID {key} cannot be deleted because it doesn't exist"
|
||||
raise IndexError(msg)
|
||||
|
||||
def __contains__(self, key):
|
||||
def __contains__(self, key: int) -> bool:
|
||||
return key in self.existing_entries or key in self.new_entries
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(
|
||||
set(self.existing_entries.keys())
|
||||
| set(self.new_entries.keys())
|
||||
| set(self.deleted_entries.keys())
|
||||
)
|
||||
|
||||
def keys(self):
|
||||
def keys(self) -> set[int]:
|
||||
return (
|
||||
set(self.existing_entries.keys()) - set(self.deleted_entries.keys())
|
||||
) | set(self.new_entries.keys())
|
||||
|
||||
def write(self, f):
|
||||
def write(self, f: IO[bytes]) -> int:
|
||||
keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys()))
|
||||
deleted_keys = sorted(set(self.deleted_entries.keys()))
|
||||
startxref = f.tell()
|
||||
f.write(b"xref\n")
|
||||
while keys:
|
||||
# find a contiguous sequence of object IDs
|
||||
prev = None
|
||||
prev: int | None = None
|
||||
for index, key in enumerate(keys):
|
||||
if prev is None or prev + 1 == key:
|
||||
prev = key
|
||||
@@ -175,7 +190,7 @@ class XrefTable:
|
||||
break
|
||||
else:
|
||||
contiguous_keys = keys
|
||||
keys = None
|
||||
keys = []
|
||||
f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys)))
|
||||
for object_id in contiguous_keys:
|
||||
if object_id in self.new_entries:
|
||||
@@ -199,7 +214,9 @@ class XrefTable:
|
||||
|
||||
|
||||
class PdfName:
|
||||
def __init__(self, name):
|
||||
name: bytes
|
||||
|
||||
def __init__(self, name: PdfName | bytes | str) -> None:
|
||||
if isinstance(name, PdfName):
|
||||
self.name = name.name
|
||||
elif isinstance(name, bytes):
|
||||
@@ -207,27 +224,27 @@ class PdfName:
|
||||
else:
|
||||
self.name = name.encode("us-ascii")
|
||||
|
||||
def name_as_str(self):
|
||||
def name_as_str(self) -> str:
|
||||
return self.name.decode("us-ascii")
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, PdfName) and other.name == self.name
|
||||
) or other == self.name
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return f"PdfName({repr(self.name)})"
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({repr(self.name)})"
|
||||
|
||||
@classmethod
|
||||
def from_pdf_stream(cls, data):
|
||||
def from_pdf_stream(cls, data: bytes) -> PdfName:
|
||||
return cls(PdfParser.interpret_name(data))
|
||||
|
||||
allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"}
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
result = bytearray(b"/")
|
||||
for b in self.name:
|
||||
if b in self.allowed_chars:
|
||||
@@ -237,19 +254,19 @@ class PdfName:
|
||||
return bytes(result)
|
||||
|
||||
|
||||
class PdfArray(list):
|
||||
def __bytes__(self):
|
||||
class PdfArray(list[Any]):
|
||||
def __bytes__(self) -> bytes:
|
||||
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
|
||||
|
||||
|
||||
class PdfDict(collections.UserDict):
|
||||
def __setattr__(self, key, value):
|
||||
class PdfDict(_DictBase):
|
||||
def __setattr__(self, key: str, value: Any) -> None:
|
||||
if key == "data":
|
||||
collections.UserDict.__setattr__(self, key, value)
|
||||
else:
|
||||
self[key.encode("us-ascii")] = value
|
||||
|
||||
def __getattr__(self, key):
|
||||
def __getattr__(self, key: str) -> str | time.struct_time:
|
||||
try:
|
||||
value = self[key.encode("us-ascii")]
|
||||
except KeyError as e:
|
||||
@@ -276,7 +293,7 @@ class PdfDict(collections.UserDict):
|
||||
value = time.gmtime(calendar.timegm(value) + offset)
|
||||
return value
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
out = bytearray(b"<<")
|
||||
for key, value in self.items():
|
||||
if value is None:
|
||||
@@ -291,35 +308,35 @@ class PdfDict(collections.UserDict):
|
||||
|
||||
|
||||
class PdfBinary:
|
||||
def __init__(self, data):
|
||||
def __init__(self, data: list[int] | bytes) -> None:
|
||||
self.data = data
|
||||
|
||||
def __bytes__(self):
|
||||
def __bytes__(self) -> bytes:
|
||||
return b"<%s>" % b"".join(b"%02X" % b for b in self.data)
|
||||
|
||||
|
||||
class PdfStream:
|
||||
def __init__(self, dictionary, buf):
|
||||
def __init__(self, dictionary: PdfDict, buf: bytes) -> None:
|
||||
self.dictionary = dictionary
|
||||
self.buf = buf
|
||||
|
||||
def decode(self):
|
||||
def decode(self) -> bytes:
|
||||
try:
|
||||
filter = self.dictionary.Filter
|
||||
except AttributeError:
|
||||
filter = self.dictionary[b"Filter"]
|
||||
except KeyError:
|
||||
return self.buf
|
||||
if filter == b"FlateDecode":
|
||||
try:
|
||||
expected_length = self.dictionary.DL
|
||||
except AttributeError:
|
||||
expected_length = self.dictionary.Length
|
||||
expected_length = self.dictionary[b"DL"]
|
||||
except KeyError:
|
||||
expected_length = self.dictionary[b"Length"]
|
||||
return zlib.decompress(self.buf, bufsize=int(expected_length))
|
||||
else:
|
||||
msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported"
|
||||
msg = f"stream filter {repr(filter)} unknown/unsupported"
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
|
||||
def pdf_repr(x):
|
||||
def pdf_repr(x: Any) -> bytes:
|
||||
if x is True:
|
||||
return b"true"
|
||||
elif x is False:
|
||||
@@ -354,12 +371,19 @@ class PdfParser:
|
||||
Supports PDF up to 1.4
|
||||
"""
|
||||
|
||||
def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"):
|
||||
def __init__(
|
||||
self,
|
||||
filename: str | None = None,
|
||||
f: IO[bytes] | None = None,
|
||||
buf: bytes | bytearray | None = None,
|
||||
start_offset: int = 0,
|
||||
mode: str = "rb",
|
||||
) -> None:
|
||||
if buf and f:
|
||||
msg = "specify buf or f or filename, but not both buf and f"
|
||||
raise RuntimeError(msg)
|
||||
self.filename = filename
|
||||
self.buf = buf
|
||||
self.buf: bytes | bytearray | mmap.mmap | None = buf
|
||||
self.f = f
|
||||
self.start_offset = start_offset
|
||||
self.should_close_buf = False
|
||||
@@ -368,12 +392,16 @@ class PdfParser:
|
||||
self.f = f = open(filename, mode)
|
||||
self.should_close_file = True
|
||||
if f is not None:
|
||||
self.buf = buf = self.get_buf_from_file(f)
|
||||
self.buf = self.get_buf_from_file(f)
|
||||
self.should_close_buf = True
|
||||
if not filename and hasattr(f, "name"):
|
||||
self.filename = f.name
|
||||
self.cached_objects = {}
|
||||
if buf:
|
||||
self.cached_objects: dict[IndirectReference, Any] = {}
|
||||
self.root_ref: IndirectReference | None
|
||||
self.info_ref: IndirectReference | None
|
||||
self.pages_ref: IndirectReference | None
|
||||
self.last_xref_section_offset: int | None
|
||||
if self.buf:
|
||||
self.read_pdf_info()
|
||||
else:
|
||||
self.file_size_total = self.file_size_this = 0
|
||||
@@ -381,52 +409,53 @@ class PdfParser:
|
||||
self.root_ref = None
|
||||
self.info = PdfDict()
|
||||
self.info_ref = None
|
||||
self.page_tree_root = {}
|
||||
self.pages = []
|
||||
self.orig_pages = []
|
||||
self.page_tree_root = PdfDict()
|
||||
self.pages: list[IndirectReference] = []
|
||||
self.orig_pages: list[IndirectReference] = []
|
||||
self.pages_ref = None
|
||||
self.last_xref_section_offset = None
|
||||
self.trailer_dict = {}
|
||||
self.trailer_dict: dict[bytes, Any] = {}
|
||||
self.xref_table = XrefTable()
|
||||
self.xref_table.reading_finished = True
|
||||
if f:
|
||||
self.seek_end()
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> PdfParser:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
def __exit__(self, *args: object) -> None:
|
||||
self.close()
|
||||
return False # do not suppress exceptions
|
||||
|
||||
def start_writing(self):
|
||||
def start_writing(self) -> None:
|
||||
self.close_buf()
|
||||
self.seek_end()
|
||||
|
||||
def close_buf(self):
|
||||
try:
|
||||
def close_buf(self) -> None:
|
||||
if isinstance(self.buf, mmap.mmap):
|
||||
self.buf.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
self.buf = None
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
if self.should_close_buf:
|
||||
self.close_buf()
|
||||
if self.f is not None and self.should_close_file:
|
||||
self.f.close()
|
||||
self.f = None
|
||||
|
||||
def seek_end(self):
|
||||
def seek_end(self) -> None:
|
||||
assert self.f is not None
|
||||
self.f.seek(0, os.SEEK_END)
|
||||
|
||||
def write_header(self):
|
||||
def write_header(self) -> None:
|
||||
assert self.f is not None
|
||||
self.f.write(b"%PDF-1.4\n")
|
||||
|
||||
def write_comment(self, s):
|
||||
def write_comment(self, s: str) -> None:
|
||||
assert self.f is not None
|
||||
self.f.write(f"% {s}\n".encode())
|
||||
|
||||
def write_catalog(self):
|
||||
def write_catalog(self) -> IndirectReference:
|
||||
assert self.f is not None
|
||||
self.del_root()
|
||||
self.root_ref = self.next_object_id(self.f.tell())
|
||||
self.pages_ref = self.next_object_id(0)
|
||||
@@ -440,7 +469,7 @@ class PdfParser:
|
||||
)
|
||||
return self.root_ref
|
||||
|
||||
def rewrite_pages(self):
|
||||
def rewrite_pages(self) -> None:
|
||||
pages_tree_nodes_to_delete = []
|
||||
for i, page_ref in enumerate(self.orig_pages):
|
||||
page_info = self.cached_objects[page_ref]
|
||||
@@ -469,7 +498,10 @@ class PdfParser:
|
||||
pages_tree_node_ref = pages_tree_node.get(b"Parent", None)
|
||||
self.orig_pages = []
|
||||
|
||||
def write_xref_and_trailer(self, new_root_ref=None):
|
||||
def write_xref_and_trailer(
|
||||
self, new_root_ref: IndirectReference | None = None
|
||||
) -> None:
|
||||
assert self.f is not None
|
||||
if new_root_ref:
|
||||
self.del_root()
|
||||
self.root_ref = new_root_ref
|
||||
@@ -477,7 +509,10 @@ class PdfParser:
|
||||
self.info_ref = self.write_obj(None, self.info)
|
||||
start_xref = self.xref_table.write(self.f)
|
||||
num_entries = len(self.xref_table)
|
||||
trailer_dict = {b"Root": self.root_ref, b"Size": num_entries}
|
||||
trailer_dict: dict[str | bytes, Any] = {
|
||||
b"Root": self.root_ref,
|
||||
b"Size": num_entries,
|
||||
}
|
||||
if self.last_xref_section_offset is not None:
|
||||
trailer_dict[b"Prev"] = self.last_xref_section_offset
|
||||
if self.info:
|
||||
@@ -489,16 +524,20 @@ class PdfParser:
|
||||
+ b"\nstartxref\n%d\n%%%%EOF" % start_xref
|
||||
)
|
||||
|
||||
def write_page(self, ref, *objs, **dict_obj):
|
||||
if isinstance(ref, int):
|
||||
ref = self.pages[ref]
|
||||
def write_page(
|
||||
self, ref: int | IndirectReference | None, *objs: Any, **dict_obj: Any
|
||||
) -> IndirectReference:
|
||||
obj_ref = self.pages[ref] if isinstance(ref, int) else ref
|
||||
if "Type" not in dict_obj:
|
||||
dict_obj["Type"] = PdfName(b"Page")
|
||||
if "Parent" not in dict_obj:
|
||||
dict_obj["Parent"] = self.pages_ref
|
||||
return self.write_obj(ref, *objs, **dict_obj)
|
||||
return self.write_obj(obj_ref, *objs, **dict_obj)
|
||||
|
||||
def write_obj(self, ref, *objs, **dict_obj):
|
||||
def write_obj(
|
||||
self, ref: IndirectReference | None, *objs: Any, **dict_obj: Any
|
||||
) -> IndirectReference:
|
||||
assert self.f is not None
|
||||
f = self.f
|
||||
if ref is None:
|
||||
ref = self.next_object_id(f.tell())
|
||||
@@ -519,14 +558,14 @@ class PdfParser:
|
||||
f.write(b"endobj\n")
|
||||
return ref
|
||||
|
||||
def del_root(self):
|
||||
def del_root(self) -> None:
|
||||
if self.root_ref is None:
|
||||
return
|
||||
del self.xref_table[self.root_ref.object_id]
|
||||
del self.xref_table[self.root[b"Pages"].object_id]
|
||||
|
||||
@staticmethod
|
||||
def get_buf_from_file(f):
|
||||
def get_buf_from_file(f: IO[bytes]) -> bytes | mmap.mmap:
|
||||
if hasattr(f, "getbuffer"):
|
||||
return f.getbuffer()
|
||||
elif hasattr(f, "getvalue"):
|
||||
@@ -537,11 +576,16 @@ class PdfParser:
|
||||
except ValueError: # cannot mmap an empty file
|
||||
return b""
|
||||
|
||||
def read_pdf_info(self):
|
||||
def read_pdf_info(self) -> None:
|
||||
assert self.buf is not None
|
||||
self.file_size_total = len(self.buf)
|
||||
self.file_size_this = self.file_size_total - self.start_offset
|
||||
self.read_trailer()
|
||||
check_format_condition(
|
||||
self.trailer_dict.get(b"Root") is not None, "Root is missing"
|
||||
)
|
||||
self.root_ref = self.trailer_dict[b"Root"]
|
||||
assert self.root_ref is not None
|
||||
self.info_ref = self.trailer_dict.get(b"Info", None)
|
||||
self.root = PdfDict(self.read_indirect(self.root_ref))
|
||||
if self.info_ref is None:
|
||||
@@ -552,12 +596,15 @@ class PdfParser:
|
||||
check_format_condition(
|
||||
self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog"
|
||||
)
|
||||
check_format_condition(b"Pages" in self.root, "/Pages missing in Root")
|
||||
check_format_condition(
|
||||
self.root.get(b"Pages") is not None, "/Pages missing in Root"
|
||||
)
|
||||
check_format_condition(
|
||||
isinstance(self.root[b"Pages"], IndirectReference),
|
||||
"/Pages in Root is not an indirect reference",
|
||||
)
|
||||
self.pages_ref = self.root[b"Pages"]
|
||||
assert self.pages_ref is not None
|
||||
self.page_tree_root = self.read_indirect(self.pages_ref)
|
||||
self.pages = self.linearize_page_tree(self.page_tree_root)
|
||||
# save the original list of page references
|
||||
@@ -565,7 +612,7 @@ class PdfParser:
|
||||
# and we need to rewrite the pages and their list
|
||||
self.orig_pages = self.pages[:]
|
||||
|
||||
def next_object_id(self, offset=None):
|
||||
def next_object_id(self, offset: int | None = None) -> IndirectReference:
|
||||
try:
|
||||
# TODO: support reuse of deleted objects
|
||||
reference = IndirectReference(max(self.xref_table.keys()) + 1, 0)
|
||||
@@ -615,12 +662,13 @@ class PdfParser:
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
def read_trailer(self):
|
||||
def read_trailer(self) -> None:
|
||||
assert self.buf is not None
|
||||
search_start_offset = len(self.buf) - 16384
|
||||
if search_start_offset < self.start_offset:
|
||||
search_start_offset = self.start_offset
|
||||
m = self.re_trailer_end.search(self.buf, search_start_offset)
|
||||
check_format_condition(m, "trailer end not found")
|
||||
check_format_condition(m is not None, "trailer end not found")
|
||||
# make sure we found the LAST trailer
|
||||
last_match = m
|
||||
while m:
|
||||
@@ -628,6 +676,7 @@ class PdfParser:
|
||||
m = self.re_trailer_end.search(self.buf, m.start() + 16)
|
||||
if not m:
|
||||
m = last_match
|
||||
assert m is not None
|
||||
trailer_data = m.group(1)
|
||||
self.last_xref_section_offset = int(m.group(2))
|
||||
self.trailer_dict = self.interpret_trailer(trailer_data)
|
||||
@@ -636,12 +685,14 @@ class PdfParser:
|
||||
if b"Prev" in self.trailer_dict:
|
||||
self.read_prev_trailer(self.trailer_dict[b"Prev"])
|
||||
|
||||
def read_prev_trailer(self, xref_section_offset):
|
||||
def read_prev_trailer(self, xref_section_offset: int) -> None:
|
||||
assert self.buf is not None
|
||||
trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset)
|
||||
m = self.re_trailer_prev.search(
|
||||
self.buf[trailer_offset : trailer_offset + 16384]
|
||||
)
|
||||
check_format_condition(m, "previous trailer not found")
|
||||
check_format_condition(m is not None, "previous trailer not found")
|
||||
assert m is not None
|
||||
trailer_data = m.group(1)
|
||||
check_format_condition(
|
||||
int(m.group(2)) == xref_section_offset,
|
||||
@@ -662,7 +713,7 @@ class PdfParser:
|
||||
re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional)
|
||||
|
||||
@classmethod
|
||||
def interpret_trailer(cls, trailer_data):
|
||||
def interpret_trailer(cls, trailer_data: bytes) -> dict[bytes, Any]:
|
||||
trailer = {}
|
||||
offset = 0
|
||||
while True:
|
||||
@@ -670,14 +721,18 @@ class PdfParser:
|
||||
if not m:
|
||||
m = cls.re_dict_end.match(trailer_data, offset)
|
||||
check_format_condition(
|
||||
m and m.end() == len(trailer_data),
|
||||
m is not None and m.end() == len(trailer_data),
|
||||
"name not found in trailer, remaining data: "
|
||||
+ repr(trailer_data[offset:]),
|
||||
)
|
||||
break
|
||||
key = cls.interpret_name(m.group(1))
|
||||
value, offset = cls.get_value(trailer_data, m.end())
|
||||
assert isinstance(key, bytes)
|
||||
value, value_offset = cls.get_value(trailer_data, m.end())
|
||||
trailer[key] = value
|
||||
if value_offset is None:
|
||||
break
|
||||
offset = value_offset
|
||||
check_format_condition(
|
||||
b"Size" in trailer and isinstance(trailer[b"Size"], int),
|
||||
"/Size not in trailer or not an integer",
|
||||
@@ -691,7 +746,7 @@ class PdfParser:
|
||||
re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?")
|
||||
|
||||
@classmethod
|
||||
def interpret_name(cls, raw, as_text=False):
|
||||
def interpret_name(cls, raw: bytes, as_text: bool = False) -> str | bytes:
|
||||
name = b""
|
||||
for m in cls.re_hashes_in_name.finditer(raw):
|
||||
if m.group(3):
|
||||
@@ -753,7 +808,13 @@ class PdfParser:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1):
|
||||
def get_value(
|
||||
cls,
|
||||
data: bytes | bytearray | mmap.mmap,
|
||||
offset: int,
|
||||
expect_indirect: IndirectReference | None = None,
|
||||
max_nesting: int = -1,
|
||||
) -> tuple[Any, int | None]:
|
||||
if max_nesting == 0:
|
||||
return None, None
|
||||
m = cls.re_comment.match(data, offset)
|
||||
@@ -775,11 +836,16 @@ class PdfParser:
|
||||
== IndirectReference(int(m.group(1)), int(m.group(2))),
|
||||
"indirect object definition different than expected",
|
||||
)
|
||||
object, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1)
|
||||
if offset is None:
|
||||
object, object_offset = cls.get_value(
|
||||
data, m.end(), max_nesting=max_nesting - 1
|
||||
)
|
||||
if object_offset is None:
|
||||
return object, None
|
||||
m = cls.re_indirect_def_end.match(data, offset)
|
||||
check_format_condition(m, "indirect object definition end not found")
|
||||
m = cls.re_indirect_def_end.match(data, object_offset)
|
||||
check_format_condition(
|
||||
m is not None, "indirect object definition end not found"
|
||||
)
|
||||
assert m is not None
|
||||
return object, m.end()
|
||||
check_format_condition(
|
||||
not expect_indirect, "indirect object definition not found"
|
||||
@@ -798,47 +864,53 @@ class PdfParser:
|
||||
m = cls.re_dict_start.match(data, offset)
|
||||
if m:
|
||||
offset = m.end()
|
||||
result = {}
|
||||
result: dict[Any, Any] = {}
|
||||
m = cls.re_dict_end.match(data, offset)
|
||||
current_offset: int | None = offset
|
||||
while not m:
|
||||
key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
|
||||
if offset is None:
|
||||
assert current_offset is not None
|
||||
key, current_offset = cls.get_value(
|
||||
data, current_offset, max_nesting=max_nesting - 1
|
||||
)
|
||||
if current_offset is None:
|
||||
return result, None
|
||||
value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
|
||||
value, current_offset = cls.get_value(
|
||||
data, current_offset, max_nesting=max_nesting - 1
|
||||
)
|
||||
result[key] = value
|
||||
if offset is None:
|
||||
if current_offset is None:
|
||||
return result, None
|
||||
m = cls.re_dict_end.match(data, offset)
|
||||
offset = m.end()
|
||||
m = cls.re_stream_start.match(data, offset)
|
||||
m = cls.re_dict_end.match(data, current_offset)
|
||||
current_offset = m.end()
|
||||
m = cls.re_stream_start.match(data, current_offset)
|
||||
if m:
|
||||
try:
|
||||
stream_len = int(result[b"Length"])
|
||||
except (TypeError, KeyError, ValueError) as e:
|
||||
msg = "bad or missing Length in stream dict (%r)" % result.get(
|
||||
b"Length", None
|
||||
)
|
||||
raise PdfFormatError(msg) from e
|
||||
stream_len = result.get(b"Length")
|
||||
if stream_len is None or not isinstance(stream_len, int):
|
||||
msg = f"bad or missing Length in stream dict ({stream_len})"
|
||||
raise PdfFormatError(msg)
|
||||
stream_data = data[m.end() : m.end() + stream_len]
|
||||
m = cls.re_stream_end.match(data, m.end() + stream_len)
|
||||
check_format_condition(m, "stream end not found")
|
||||
offset = m.end()
|
||||
result = PdfStream(PdfDict(result), stream_data)
|
||||
else:
|
||||
result = PdfDict(result)
|
||||
return result, offset
|
||||
check_format_condition(m is not None, "stream end not found")
|
||||
assert m is not None
|
||||
current_offset = m.end()
|
||||
return PdfStream(PdfDict(result), stream_data), current_offset
|
||||
return PdfDict(result), current_offset
|
||||
m = cls.re_array_start.match(data, offset)
|
||||
if m:
|
||||
offset = m.end()
|
||||
result = []
|
||||
results = []
|
||||
m = cls.re_array_end.match(data, offset)
|
||||
current_offset = offset
|
||||
while not m:
|
||||
value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
|
||||
result.append(value)
|
||||
if offset is None:
|
||||
return result, None
|
||||
m = cls.re_array_end.match(data, offset)
|
||||
return result, m.end()
|
||||
assert current_offset is not None
|
||||
value, current_offset = cls.get_value(
|
||||
data, current_offset, max_nesting=max_nesting - 1
|
||||
)
|
||||
results.append(value)
|
||||
if current_offset is None:
|
||||
return results, None
|
||||
m = cls.re_array_end.match(data, current_offset)
|
||||
return results, m.end()
|
||||
m = cls.re_null.match(data, offset)
|
||||
if m:
|
||||
return None, m.end()
|
||||
@@ -872,7 +944,7 @@ class PdfParser:
|
||||
if m:
|
||||
return cls.get_literal_string(data, m.end())
|
||||
# return None, offset # fallback (only for debugging)
|
||||
msg = "unrecognized object: " + repr(data[offset : offset + 32])
|
||||
msg = f"unrecognized object: {repr(data[offset : offset + 32])}"
|
||||
raise PdfFormatError(msg)
|
||||
|
||||
re_lit_str_token = re.compile(
|
||||
@@ -898,7 +970,9 @@ class PdfParser:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_literal_string(cls, data, offset):
|
||||
def get_literal_string(
|
||||
cls, data: bytes | bytearray | mmap.mmap, offset: int
|
||||
) -> tuple[bytes, int]:
|
||||
nesting_depth = 0
|
||||
result = bytearray()
|
||||
for m in cls.re_lit_str_token.finditer(data, offset):
|
||||
@@ -934,12 +1008,14 @@ class PdfParser:
|
||||
)
|
||||
re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
|
||||
|
||||
def read_xref_table(self, xref_section_offset):
|
||||
def read_xref_table(self, xref_section_offset: int) -> int:
|
||||
assert self.buf is not None
|
||||
subsection_found = False
|
||||
m = self.re_xref_section_start.match(
|
||||
self.buf, xref_section_offset + self.start_offset
|
||||
)
|
||||
check_format_condition(m, "xref section start not found")
|
||||
check_format_condition(m is not None, "xref section start not found")
|
||||
assert m is not None
|
||||
offset = m.end()
|
||||
while True:
|
||||
m = self.re_xref_subsection_start.match(self.buf, offset)
|
||||
@@ -954,7 +1030,8 @@ class PdfParser:
|
||||
num_objects = int(m.group(2))
|
||||
for i in range(first_object, first_object + num_objects):
|
||||
m = self.re_xref_entry.match(self.buf, offset)
|
||||
check_format_condition(m, "xref entry not found")
|
||||
check_format_condition(m is not None, "xref entry not found")
|
||||
assert m is not None
|
||||
offset = m.end()
|
||||
is_free = m.group(3) == b"f"
|
||||
if not is_free:
|
||||
@@ -964,13 +1041,14 @@ class PdfParser:
|
||||
self.xref_table[i] = new_entry
|
||||
return offset
|
||||
|
||||
def read_indirect(self, ref, max_nesting=-1):
|
||||
def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any:
|
||||
offset, generation = self.xref_table[ref[0]]
|
||||
check_format_condition(
|
||||
generation == ref[1],
|
||||
f"expected to find generation {ref[1]} for object ID {ref[0]} in xref "
|
||||
f"table, instead found generation {generation} at offset {offset}",
|
||||
)
|
||||
assert self.buf is not None
|
||||
value = self.get_value(
|
||||
self.buf,
|
||||
offset + self.start_offset,
|
||||
@@ -980,14 +1058,15 @@ class PdfParser:
|
||||
self.cached_objects[ref] = value
|
||||
return value
|
||||
|
||||
def linearize_page_tree(self, node=None):
|
||||
if node is None:
|
||||
node = self.page_tree_root
|
||||
def linearize_page_tree(
|
||||
self, node: PdfDict | None = None
|
||||
) -> list[IndirectReference]:
|
||||
page_node = node if node is not None else self.page_tree_root
|
||||
check_format_condition(
|
||||
node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
|
||||
page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
|
||||
)
|
||||
pages = []
|
||||
for kid in node[b"Kids"]:
|
||||
for kid in page_node[b"Kids"]:
|
||||
kid_object = self.read_indirect(kid)
|
||||
if kid_object[b"Type"] == b"Page":
|
||||
pages.append(kid)
|
||||
|
||||
Reference in New Issue
Block a user