import logging from io import BytesIO from typing import BinaryIO, TYPE_CHECKING, Optional, Union from . import settings from .pdftypes import PDFException from .pdftypes import PDFObjRef from .pdftypes import PDFStream from .pdftypes import dict_value from .pdftypes import int_value from .psparser import KWD from .psparser import PSEOF from .psparser import PSKeyword from .psparser import PSStackParser from .psparser import PSSyntaxError if TYPE_CHECKING: from .pdfdocument import PDFDocument log = logging.getLogger(__name__) class PDFSyntaxError(PDFException): pass # PDFParser stack holds all the base types plus PDFStream, PDFObjRef, and None class PDFParser(PSStackParser[Union[PSKeyword, PDFStream, PDFObjRef, None]]): """ PDFParser fetch PDF objects from a file stream. It can handle indirect references by referring to a PDF document set by set_document method. It also reads XRefs at the end of every PDF file. Typical usage: parser = PDFParser(fp) parser.read_xref() parser.read_xref(fallback=True) # optional parser.set_document(doc) parser.seek(offset) parser.nextobject() """ def __init__(self, fp: BinaryIO) -> None: PSStackParser.__init__(self, fp) self.doc: Optional["PDFDocument"] = None self.fallback = False def set_document(self, doc: "PDFDocument") -> None: """Associates the parser with a PDFDocument object.""" self.doc = doc KEYWORD_R = KWD(b"R") KEYWORD_NULL = KWD(b"null") KEYWORD_ENDOBJ = KWD(b"endobj") KEYWORD_STREAM = KWD(b"stream") KEYWORD_XREF = KWD(b"xref") KEYWORD_STARTXREF = KWD(b"startxref") def do_keyword(self, pos: int, token: PSKeyword) -> None: """Handles PDF-related keywords.""" if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF): self.add_results(*self.pop(1)) elif token is self.KEYWORD_ENDOBJ: self.add_results(*self.pop(4)) elif token is self.KEYWORD_NULL: # null object self.push((pos, None)) elif token is self.KEYWORD_R: # reference to indirect object if len(self.curstack) >= 2: try: ((_, objid), (_, genno)) = self.pop(2) (objid, genno) = (int(objid), int(genno)) # type: ignore[arg-type] assert self.doc is not None obj = PDFObjRef(self.doc, objid, genno) self.push((pos, obj)) except PSSyntaxError: pass elif token is self.KEYWORD_STREAM: # stream object ((_, dic),) = self.pop(1) dic = dict_value(dic) objlen = 0 if not self.fallback: try: objlen = int_value(dic["Length"]) except KeyError: if settings.STRICT: raise PDFSyntaxError("/Length is undefined: %r" % dic) self.seek(pos) try: (_, line) = self.nextline() # 'stream' except PSEOF: if settings.STRICT: raise PDFSyntaxError("Unexpected EOF") return pos += len(line) self.fp.seek(pos) data = bytearray(self.fp.read(objlen)) self.seek(pos + objlen) while 1: try: (linepos, line) = self.nextline() except PSEOF: if settings.STRICT: raise PDFSyntaxError("Unexpected EOF") break if b"endstream" in line: i = line.index(b"endstream") objlen += i if self.fallback: data += line[:i] break objlen += len(line) if self.fallback: data += line self.seek(pos + objlen) # XXX limit objlen not to exceed object boundary log.debug( "Stream: pos=%d, objlen=%d, dic=%r, data=%r...", pos, objlen, dic, data[:10], ) assert self.doc is not None stream = PDFStream(dic, bytes(data), self.doc.decipher) self.push((pos, stream)) else: # others self.push((pos, token)) class PDFStreamParser(PDFParser): """ PDFStreamParser is used to parse PDF content streams that is contained in each page and has instructions for rendering the page. A reference to a PDF document is needed because a PDF content stream can also have indirect references to other objects in the same document. """ def __init__(self, data: bytes) -> None: PDFParser.__init__(self, BytesIO(data)) def flush(self) -> None: self.add_results(*self.popall()) KEYWORD_OBJ = KWD(b"obj") def do_keyword(self, pos: int, token: PSKeyword) -> None: if token is self.KEYWORD_R: # reference to indirect object try: ((_, objid), (_, genno)) = self.pop(2) (objid, genno) = (int(objid), int(genno)) # type: ignore[arg-type] obj = PDFObjRef(self.doc, objid, genno) self.push((pos, obj)) except PSSyntaxError: pass return elif token in (self.KEYWORD_OBJ, self.KEYWORD_ENDOBJ): if settings.STRICT: # See PDF Spec 3.4.6: Only the object values are stored in the # stream; the obj and endobj keywords are not used. raise PDFSyntaxError("Keyword endobj found in stream") return # others self.push((pos, token))