pdfminer.six/pdfminer/psparser.py

674 lines
19 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import re
from typing import (
Any,
BinaryIO,
Dict,
Generic,
Iterator,
List,
Optional,
Tuple,
Type,
TypeVar,
Union,
)
from . import settings
from .utils import choplist
log = logging.getLogger(__name__)
class PSException(Exception):
pass
class PSEOF(PSException):
pass
class PSSyntaxError(PSException):
pass
class PSTypeError(PSException):
pass
class PSValueError(PSException):
pass
class PSObject:
"""Base class for all PS or PDF-related data types."""
pass
class PSLiteral(PSObject):
"""A class that represents a PostScript literal.
Postscript literals are used as identifiers, such as
variable names, property names and dictionary keys.
Literals are case sensitive and denoted by a preceding
slash sign (e.g. "/Name")
Note: Do not create an instance of PSLiteral directly.
Always use PSLiteralTable.intern().
"""
NameType = Union[str, bytes]
def __init__(self, name: NameType) -> None:
self.name = name
def __repr__(self) -> str:
name = self.name
return "/%r" % name
class PSKeyword(PSObject):
"""A class that represents a PostScript keyword.
PostScript keywords are a dozen of predefined words.
Commands and directives in PostScript are expressed by keywords.
They are also used to denote the content boundaries.
Note: Do not create an instance of PSKeyword directly.
Always use PSKeywordTable.intern().
"""
def __init__(self, name: bytes) -> None:
self.name = name
def __repr__(self) -> str:
name = self.name
return "/%r" % name
_SymbolT = TypeVar("_SymbolT", PSLiteral, PSKeyword)
class PSSymbolTable(Generic[_SymbolT]):
"""A utility class for storing PSLiteral/PSKeyword objects.
Interned objects can be checked its identity with "is" operator.
"""
def __init__(self, klass: Type[_SymbolT]) -> None:
self.dict: Dict[PSLiteral.NameType, _SymbolT] = {}
self.klass: Type[_SymbolT] = klass
def intern(self, name: PSLiteral.NameType) -> _SymbolT:
if name in self.dict:
lit = self.dict[name]
else:
# Type confusion issue: PSKeyword always takes bytes as name
# PSLiteral uses either str or bytes
lit = self.klass(name) # type: ignore[arg-type]
self.dict[name] = lit
return lit
PSLiteralTable = PSSymbolTable(PSLiteral)
PSKeywordTable = PSSymbolTable(PSKeyword)
LIT = PSLiteralTable.intern
KWD = PSKeywordTable.intern
KEYWORD_PROC_BEGIN = KWD(b"{")
KEYWORD_PROC_END = KWD(b"}")
KEYWORD_ARRAY_BEGIN = KWD(b"[")
KEYWORD_ARRAY_END = KWD(b"]")
KEYWORD_DICT_BEGIN = KWD(b"<<")
KEYWORD_DICT_END = KWD(b">>")
def literal_name(x: object) -> Any:
if not isinstance(x, PSLiteral):
if settings.STRICT:
raise PSTypeError("Literal required: {!r}".format(x))
else:
name = x
else:
name = x.name
if not isinstance(name, str):
try:
name = str(name, "utf-8")
except Exception:
pass
return name
def keyword_name(x: object) -> Any:
if not isinstance(x, PSKeyword):
if settings.STRICT:
raise PSTypeError("Keyword required: %r" % x)
else:
name = x
else:
name = str(x.name, "utf-8", "ignore")
return name
EOL = re.compile(rb"[\r\n]")
SPC = re.compile(rb"\s")
NONSPC = re.compile(rb"\S")
HEX = re.compile(rb"[0-9a-fA-F]")
END_LITERAL = re.compile(rb"[#/%\[\]()<>{}\s]")
END_HEX_STRING = re.compile(rb"[^\s0-9a-fA-F]")
HEX_PAIR = re.compile(rb"[0-9a-fA-F]{2}|.")
END_NUMBER = re.compile(rb"[^0-9]")
END_KEYWORD = re.compile(rb"[#/%\[\]()<>{}\s]")
END_STRING = re.compile(rb"[()\134]")
OCT_STRING = re.compile(rb"[0-7]")
ESC_STRING = {
b"b": 8,
b"t": 9,
b"n": 10,
b"f": 12,
b"r": 13,
b"(": 40,
b")": 41,
b"\\": 92,
}
PSBaseParserToken = Union[float, bool, PSLiteral, PSKeyword, bytes]
class PSBaseParser:
"""Most basic PostScript parser that performs only tokenization."""
BUFSIZ = 4096
def __init__(self, fp: BinaryIO) -> None:
self.fp = fp
self.seek(0)
def __repr__(self) -> str:
return "<%s: %r, bufpos=%d>" % (self.__class__.__name__, self.fp, self.bufpos)
def flush(self) -> None:
return
def close(self) -> None:
self.flush()
return
def tell(self) -> int:
return self.bufpos + self.charpos
def poll(self, pos: Optional[int] = None, n: int = 80) -> None:
pos0 = self.fp.tell()
if not pos:
pos = self.bufpos + self.charpos
self.fp.seek(pos)
log.debug("poll(%d): %r", pos, self.fp.read(n))
self.fp.seek(pos0)
return
def seek(self, pos: int) -> None:
"""Seeks the parser to the given position."""
log.debug("seek: %r", pos)
self.fp.seek(pos)
# reset the status for nextline()
self.bufpos = pos
self.buf = b""
self.charpos = 0
# reset the status for nexttoken()
self._parse1 = self._parse_main
self._curtoken = b""
self._curtokenpos = 0
self._tokens: List[Tuple[int, PSBaseParserToken]] = []
return
def fillbuf(self) -> None:
if self.charpos < len(self.buf):
return
# fetch next chunk.
self.bufpos = self.fp.tell()
self.buf = self.fp.read(self.BUFSIZ)
if not self.buf:
raise PSEOF("Unexpected EOF")
self.charpos = 0
return
def nextline(self) -> Tuple[int, bytes]:
"""Fetches a next line that ends either with \\r or \\n."""
linebuf = b""
linepos = self.bufpos + self.charpos
eol = False
while 1:
self.fillbuf()
if eol:
c = self.buf[self.charpos : self.charpos + 1]
# handle b'\r\n'
if c == b"\n":
linebuf += c
self.charpos += 1
break
m = EOL.search(self.buf, self.charpos)
if m:
linebuf += self.buf[self.charpos : m.end(0)]
self.charpos = m.end(0)
if linebuf[-1:] == b"\r":
eol = True
else:
break
else:
linebuf += self.buf[self.charpos :]
self.charpos = len(self.buf)
log.debug("nextline: %r, %r", linepos, linebuf)
return (linepos, linebuf)
def revreadlines(self) -> Iterator[bytes]:
"""Fetches a next line backword.
This is used to locate the trailers at the end of a file.
"""
self.fp.seek(0, 2)
pos = self.fp.tell()
buf = b""
while 0 < pos:
prevpos = pos
pos = max(0, pos - self.BUFSIZ)
self.fp.seek(pos)
s = self.fp.read(prevpos - pos)
if not s:
break
while 1:
n = max(s.rfind(b"\r"), s.rfind(b"\n"))
if n == -1:
buf = s + buf
break
yield s[n:] + buf
s = s[:n]
buf = b""
return
def _parse_main(self, s: bytes, i: int) -> int:
m = NONSPC.search(s, i)
if not m:
return len(s)
j = m.start(0)
c = s[j : j + 1]
self._curtokenpos = self.bufpos + j
if c == b"%":
self._curtoken = b"%"
self._parse1 = self._parse_comment
return j + 1
elif c == b"/":
self._curtoken = b""
self._parse1 = self._parse_literal
return j + 1
elif c in b"-+" or c.isdigit():
self._curtoken = c
self._parse1 = self._parse_number
return j + 1
elif c == b".":
self._curtoken = c
self._parse1 = self._parse_float
return j + 1
elif c.isalpha():
self._curtoken = c
self._parse1 = self._parse_keyword
return j + 1
elif c == b"(":
self._curtoken = b""
self.paren = 1
self._parse1 = self._parse_string
return j + 1
elif c == b"<":
self._curtoken = b""
self._parse1 = self._parse_wopen
return j + 1
elif c == b">":
self._curtoken = b""
self._parse1 = self._parse_wclose
return j + 1
else:
self._add_token(KWD(c))
return j + 1
def _add_token(self, obj: PSBaseParserToken) -> None:
self._tokens.append((self._curtokenpos, obj))
return
def _parse_comment(self, s: bytes, i: int) -> int:
m = EOL.search(s, i)
if not m:
self._curtoken += s[i:]
return len(s)
j = m.start(0)
self._curtoken += s[i:j]
self._parse1 = self._parse_main
# We ignore comments.
# self._tokens.append(self._curtoken)
return j
def _parse_literal(self, s: bytes, i: int) -> int:
m = END_LITERAL.search(s, i)
if not m:
self._curtoken += s[i:]
return len(s)
j = m.start(0)
self._curtoken += s[i:j]
c = s[j : j + 1]
if c == b"#":
self.hex = b""
self._parse1 = self._parse_literal_hex
return j + 1
try:
name: Union[str, bytes] = str(self._curtoken, "utf-8")
except Exception:
name = self._curtoken
self._add_token(LIT(name))
self._parse1 = self._parse_main
return j
def _parse_literal_hex(self, s: bytes, i: int) -> int:
c = s[i : i + 1]
if HEX.match(c) and len(self.hex) < 2:
self.hex += c
return i + 1
if self.hex:
self._curtoken += bytes((int(self.hex, 16),))
self._parse1 = self._parse_literal
return i
def _parse_number(self, s: bytes, i: int) -> int:
m = END_NUMBER.search(s, i)
if not m:
self._curtoken += s[i:]
return len(s)
j = m.start(0)
self._curtoken += s[i:j]
c = s[j : j + 1]
if c == b".":
self._curtoken += c
self._parse1 = self._parse_float
return j + 1
try:
self._add_token(int(self._curtoken))
except ValueError:
pass
self._parse1 = self._parse_main
return j
def _parse_float(self, s: bytes, i: int) -> int:
m = END_NUMBER.search(s, i)
if not m:
self._curtoken += s[i:]
return len(s)
j = m.start(0)
self._curtoken += s[i:j]
try:
self._add_token(float(self._curtoken))
except ValueError:
pass
self._parse1 = self._parse_main
return j
def _parse_keyword(self, s: bytes, i: int) -> int:
m = END_KEYWORD.search(s, i)
if not m:
self._curtoken += s[i:]
return len(s)
j = m.start(0)
self._curtoken += s[i:j]
if self._curtoken == b"true":
token: Union[bool, PSKeyword] = True
elif self._curtoken == b"false":
token = False
else:
token = KWD(self._curtoken)
self._add_token(token)
self._parse1 = self._parse_main
return j
def _parse_string(self, s: bytes, i: int) -> int:
m = END_STRING.search(s, i)
if not m:
self._curtoken += s[i:]
return len(s)
j = m.start(0)
self._curtoken += s[i:j]
c = s[j : j + 1]
if c == b"\\":
self.oct = b""
self._parse1 = self._parse_string_1
return j + 1
if c == b"(":
self.paren += 1
self._curtoken += c
return j + 1
if c == b")":
self.paren -= 1
if self.paren:
# WTF, they said balanced parens need no special treatment.
self._curtoken += c
return j + 1
self._add_token(self._curtoken)
self._parse1 = self._parse_main
return j + 1
def _parse_string_1(self, s: bytes, i: int) -> int:
"""Parse literal strings
PDF Reference 3.2.3
"""
c = s[i : i + 1]
if OCT_STRING.match(c) and len(self.oct) < 3:
self.oct += c
return i + 1
elif self.oct:
self._curtoken += bytes((int(self.oct, 8),))
self._parse1 = self._parse_string
return i
elif c in ESC_STRING:
self._curtoken += bytes((ESC_STRING[c],))
elif c == b"\r" and len(s) > i + 1 and s[i + 1 : i + 2] == b"\n":
# If current and next character is \r\n skip both because enters
# after a \ are ignored
i += 1
# default action
self._parse1 = self._parse_string
return i + 1
def _parse_wopen(self, s: bytes, i: int) -> int:
c = s[i : i + 1]
if c == b"<":
self._add_token(KEYWORD_DICT_BEGIN)
self._parse1 = self._parse_main
i += 1
else:
self._parse1 = self._parse_hexstring
return i
def _parse_wclose(self, s: bytes, i: int) -> int:
c = s[i : i + 1]
if c == b">":
self._add_token(KEYWORD_DICT_END)
i += 1
self._parse1 = self._parse_main
return i
def _parse_hexstring(self, s: bytes, i: int) -> int:
m = END_HEX_STRING.search(s, i)
if not m:
self._curtoken += s[i:]
return len(s)
j = m.start(0)
self._curtoken += s[i:j]
token = HEX_PAIR.sub(
lambda m: bytes((int(m.group(0), 16),)), SPC.sub(b"", self._curtoken)
)
self._add_token(token)
self._parse1 = self._parse_main
return j
def nexttoken(self) -> Tuple[int, PSBaseParserToken]:
while not self._tokens:
self.fillbuf()
self.charpos = self._parse1(self.buf, self.charpos)
token = self._tokens.pop(0)
log.debug("nexttoken: %r", token)
return token
# Stack slots may by occupied by any of:
# * the PSBaseParserToken types
# * list (via KEYWORD_ARRAY)
# * dict (via KEYWORD_DICT)
# * subclass-specific extensions (e.g. PDFStream, PDFObjRef) via ExtraT
ExtraT = TypeVar("ExtraT")
PSStackType = Union[float, bool, PSLiteral, bytes, List, Dict, ExtraT]
PSStackEntry = Tuple[int, PSStackType[ExtraT]]
class PSStackParser(PSBaseParser, Generic[ExtraT]):
def __init__(self, fp: BinaryIO) -> None:
PSBaseParser.__init__(self, fp)
self.reset()
return
def reset(self) -> None:
self.context: List[Tuple[int, Optional[str], List[PSStackEntry[ExtraT]]]] = []
self.curtype: Optional[str] = None
self.curstack: List[PSStackEntry[ExtraT]] = []
self.results: List[PSStackEntry[ExtraT]] = []
return
def seek(self, pos: int) -> None:
PSBaseParser.seek(self, pos)
self.reset()
return
def push(self, *objs: PSStackEntry[ExtraT]) -> None:
self.curstack.extend(objs)
return
def pop(self, n: int) -> List[PSStackEntry[ExtraT]]:
objs = self.curstack[-n:]
self.curstack[-n:] = []
return objs
def popall(self) -> List[PSStackEntry[ExtraT]]:
objs = self.curstack
self.curstack = []
return objs
def add_results(self, *objs: PSStackEntry[ExtraT]) -> None:
try:
log.debug("add_results: %r", objs)
except Exception:
log.debug("add_results: (unprintable object)")
self.results.extend(objs)
return
def start_type(self, pos: int, type: str) -> None:
self.context.append((pos, self.curtype, self.curstack))
(self.curtype, self.curstack) = (type, [])
log.debug("start_type: pos=%r, type=%r", pos, type)
return
def end_type(self, type: str) -> Tuple[int, List[PSStackType[ExtraT]]]:
if self.curtype != type:
raise PSTypeError("Type mismatch: {!r} != {!r}".format(self.curtype, type))
objs = [obj for (_, obj) in self.curstack]
(pos, self.curtype, self.curstack) = self.context.pop()
log.debug("end_type: pos=%r, type=%r, objs=%r", pos, type, objs)
return (pos, objs)
def do_keyword(self, pos: int, token: PSKeyword) -> None:
return
def nextobject(self) -> PSStackEntry[ExtraT]:
"""Yields a list of objects.
Arrays and dictionaries are represented as Python lists and
dictionaries.
:return: keywords, literals, strings, numbers, arrays and dictionaries.
"""
while not self.results:
(pos, token) = self.nexttoken()
if isinstance(token, (int, float, bool, str, bytes, PSLiteral)):
# normal token
self.push((pos, token))
elif token == KEYWORD_ARRAY_BEGIN:
# begin array
self.start_type(pos, "a")
elif token == KEYWORD_ARRAY_END:
# end array
try:
self.push(self.end_type("a"))
except PSTypeError:
if settings.STRICT:
raise
elif token == KEYWORD_DICT_BEGIN:
# begin dictionary
self.start_type(pos, "d")
elif token == KEYWORD_DICT_END:
# end dictionary
try:
(pos, objs) = self.end_type("d")
if len(objs) % 2 != 0:
error_msg = "Invalid dictionary construct: %r" % objs
raise PSSyntaxError(error_msg)
d = {
literal_name(k): v
for (k, v) in choplist(2, objs)
if v is not None
}
self.push((pos, d))
except PSTypeError:
if settings.STRICT:
raise
elif token == KEYWORD_PROC_BEGIN:
# begin proc
self.start_type(pos, "p")
elif token == KEYWORD_PROC_END:
# end proc
try:
self.push(self.end_type("p"))
except PSTypeError:
if settings.STRICT:
raise
elif isinstance(token, PSKeyword):
log.debug(
"do_keyword: pos=%r, token=%r, stack=%r", pos, token, self.curstack
)
self.do_keyword(pos, token)
else:
log.error(
"unknown token: pos=%r, token=%r, stack=%r",
pos,
token,
self.curstack,
)
self.do_keyword(pos, token)
raise
if self.context:
continue
else:
self.flush()
obj = self.results.pop(0)
try:
log.debug("nextobject: %r", obj)
except Exception:
log.debug("nextobject: (unprintable object)")
return obj