Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PdfParser.py 34KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999
  1. import calendar
  2. import codecs
  3. import collections
  4. import mmap
  5. import os
  6. import re
  7. import time
  8. import zlib
  9. # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
  10. # on page 656
  11. def encode_text(s):
  12. return codecs.BOM_UTF16_BE + s.encode("utf_16_be")
  13. PDFDocEncoding = {
  14. 0x16: "\u0017",
  15. 0x18: "\u02D8",
  16. 0x19: "\u02C7",
  17. 0x1A: "\u02C6",
  18. 0x1B: "\u02D9",
  19. 0x1C: "\u02DD",
  20. 0x1D: "\u02DB",
  21. 0x1E: "\u02DA",
  22. 0x1F: "\u02DC",
  23. 0x80: "\u2022",
  24. 0x81: "\u2020",
  25. 0x82: "\u2021",
  26. 0x83: "\u2026",
  27. 0x84: "\u2014",
  28. 0x85: "\u2013",
  29. 0x86: "\u0192",
  30. 0x87: "\u2044",
  31. 0x88: "\u2039",
  32. 0x89: "\u203A",
  33. 0x8A: "\u2212",
  34. 0x8B: "\u2030",
  35. 0x8C: "\u201E",
  36. 0x8D: "\u201C",
  37. 0x8E: "\u201D",
  38. 0x8F: "\u2018",
  39. 0x90: "\u2019",
  40. 0x91: "\u201A",
  41. 0x92: "\u2122",
  42. 0x93: "\uFB01",
  43. 0x94: "\uFB02",
  44. 0x95: "\u0141",
  45. 0x96: "\u0152",
  46. 0x97: "\u0160",
  47. 0x98: "\u0178",
  48. 0x99: "\u017D",
  49. 0x9A: "\u0131",
  50. 0x9B: "\u0142",
  51. 0x9C: "\u0153",
  52. 0x9D: "\u0161",
  53. 0x9E: "\u017E",
  54. 0xA0: "\u20AC",
  55. }
  56. def decode_text(b):
  57. if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE:
  58. return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be")
  59. else:
  60. return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b)
  61. class PdfFormatError(RuntimeError):
  62. """An error that probably indicates a syntactic or semantic error in the
  63. PDF file structure"""
  64. pass
  65. def check_format_condition(condition, error_message):
  66. if not condition:
  67. raise PdfFormatError(error_message)
  68. class IndirectReference(
  69. collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"])
  70. ):
  71. def __str__(self):
  72. return "%s %s R" % self
  73. def __bytes__(self):
  74. return self.__str__().encode("us-ascii")
  75. def __eq__(self, other):
  76. return (
  77. other.__class__ is self.__class__
  78. and other.object_id == self.object_id
  79. and other.generation == self.generation
  80. )
  81. def __ne__(self, other):
  82. return not (self == other)
  83. def __hash__(self):
  84. return hash((self.object_id, self.generation))
  85. class IndirectObjectDef(IndirectReference):
  86. def __str__(self):
  87. return "%s %s obj" % self
  88. class XrefTable:
  89. def __init__(self):
  90. self.existing_entries = {} # object ID => (offset, generation)
  91. self.new_entries = {} # object ID => (offset, generation)
  92. self.deleted_entries = {0: 65536} # object ID => generation
  93. self.reading_finished = False
  94. def __setitem__(self, key, value):
  95. if self.reading_finished:
  96. self.new_entries[key] = value
  97. else:
  98. self.existing_entries[key] = value
  99. if key in self.deleted_entries:
  100. del self.deleted_entries[key]
  101. def __getitem__(self, key):
  102. try:
  103. return self.new_entries[key]
  104. except KeyError:
  105. return self.existing_entries[key]
  106. def __delitem__(self, key):
  107. if key in self.new_entries:
  108. generation = self.new_entries[key][1] + 1
  109. del self.new_entries[key]
  110. self.deleted_entries[key] = generation
  111. elif key in self.existing_entries:
  112. generation = self.existing_entries[key][1] + 1
  113. self.deleted_entries[key] = generation
  114. elif key in self.deleted_entries:
  115. generation = self.deleted_entries[key]
  116. else:
  117. msg = (
  118. "object ID " + str(key) + " cannot be deleted because it doesn't exist"
  119. )
  120. raise IndexError(msg)
  121. def __contains__(self, key):
  122. return key in self.existing_entries or key in self.new_entries
  123. def __len__(self):
  124. return len(
  125. set(self.existing_entries.keys())
  126. | set(self.new_entries.keys())
  127. | set(self.deleted_entries.keys())
  128. )
  129. def keys(self):
  130. return (
  131. set(self.existing_entries.keys()) - set(self.deleted_entries.keys())
  132. ) | set(self.new_entries.keys())
  133. def write(self, f):
  134. keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys()))
  135. deleted_keys = sorted(set(self.deleted_entries.keys()))
  136. startxref = f.tell()
  137. f.write(b"xref\n")
  138. while keys:
  139. # find a contiguous sequence of object IDs
  140. prev = None
  141. for index, key in enumerate(keys):
  142. if prev is None or prev + 1 == key:
  143. prev = key
  144. else:
  145. contiguous_keys = keys[:index]
  146. keys = keys[index:]
  147. break
  148. else:
  149. contiguous_keys = keys
  150. keys = None
  151. f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys)))
  152. for object_id in contiguous_keys:
  153. if object_id in self.new_entries:
  154. f.write(b"%010d %05d n \n" % self.new_entries[object_id])
  155. else:
  156. this_deleted_object_id = deleted_keys.pop(0)
  157. check_format_condition(
  158. object_id == this_deleted_object_id,
  159. f"expected the next deleted object ID to be {object_id}, "
  160. f"instead found {this_deleted_object_id}",
  161. )
  162. try:
  163. next_in_linked_list = deleted_keys[0]
  164. except IndexError:
  165. next_in_linked_list = 0
  166. f.write(
  167. b"%010d %05d f \n"
  168. % (next_in_linked_list, self.deleted_entries[object_id])
  169. )
  170. return startxref
  171. class PdfName:
  172. def __init__(self, name):
  173. if isinstance(name, PdfName):
  174. self.name = name.name
  175. elif isinstance(name, bytes):
  176. self.name = name
  177. else:
  178. self.name = name.encode("us-ascii")
  179. def name_as_str(self):
  180. return self.name.decode("us-ascii")
  181. def __eq__(self, other):
  182. return (
  183. isinstance(other, PdfName) and other.name == self.name
  184. ) or other == self.name
  185. def __hash__(self):
  186. return hash(self.name)
  187. def __repr__(self):
  188. return f"PdfName({repr(self.name)})"
  189. @classmethod
  190. def from_pdf_stream(cls, data):
  191. return cls(PdfParser.interpret_name(data))
  192. allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"}
  193. def __bytes__(self):
  194. result = bytearray(b"/")
  195. for b in self.name:
  196. if b in self.allowed_chars:
  197. result.append(b)
  198. else:
  199. result.extend(b"#%02X" % b)
  200. return bytes(result)
  201. class PdfArray(list):
  202. def __bytes__(self):
  203. return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
  204. class PdfDict(collections.UserDict):
  205. def __setattr__(self, key, value):
  206. if key == "data":
  207. collections.UserDict.__setattr__(self, key, value)
  208. else:
  209. self[key.encode("us-ascii")] = value
  210. def __getattr__(self, key):
  211. try:
  212. value = self[key.encode("us-ascii")]
  213. except KeyError as e:
  214. raise AttributeError(key) from e
  215. if isinstance(value, bytes):
  216. value = decode_text(value)
  217. if key.endswith("Date"):
  218. if value.startswith("D:"):
  219. value = value[2:]
  220. relationship = "Z"
  221. if len(value) > 17:
  222. relationship = value[14]
  223. offset = int(value[15:17]) * 60
  224. if len(value) > 20:
  225. offset += int(value[18:20])
  226. format = "%Y%m%d%H%M%S"[: len(value) - 2]
  227. value = time.strptime(value[: len(format) + 2], format)
  228. if relationship in ["+", "-"]:
  229. offset *= 60
  230. if relationship == "+":
  231. offset *= -1
  232. value = time.gmtime(calendar.timegm(value) + offset)
  233. return value
  234. def __bytes__(self):
  235. out = bytearray(b"<<")
  236. for key, value in self.items():
  237. if value is None:
  238. continue
  239. value = pdf_repr(value)
  240. out.extend(b"\n")
  241. out.extend(bytes(PdfName(key)))
  242. out.extend(b" ")
  243. out.extend(value)
  244. out.extend(b"\n>>")
  245. return bytes(out)
  246. class PdfBinary:
  247. def __init__(self, data):
  248. self.data = data
  249. def __bytes__(self):
  250. return b"<%s>" % b"".join(b"%02X" % b for b in self.data)
  251. class PdfStream:
  252. def __init__(self, dictionary, buf):
  253. self.dictionary = dictionary
  254. self.buf = buf
  255. def decode(self):
  256. try:
  257. filter = self.dictionary.Filter
  258. except AttributeError:
  259. return self.buf
  260. if filter == b"FlateDecode":
  261. try:
  262. expected_length = self.dictionary.DL
  263. except AttributeError:
  264. expected_length = self.dictionary.Length
  265. return zlib.decompress(self.buf, bufsize=int(expected_length))
  266. else:
  267. msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported"
  268. raise NotImplementedError(msg)
  269. def pdf_repr(x):
  270. if x is True:
  271. return b"true"
  272. elif x is False:
  273. return b"false"
  274. elif x is None:
  275. return b"null"
  276. elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)):
  277. return bytes(x)
  278. elif isinstance(x, (int, float)):
  279. return str(x).encode("us-ascii")
  280. elif isinstance(x, time.struct_time):
  281. return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")"
  282. elif isinstance(x, dict):
  283. return bytes(PdfDict(x))
  284. elif isinstance(x, list):
  285. return bytes(PdfArray(x))
  286. elif isinstance(x, str):
  287. return pdf_repr(encode_text(x))
  288. elif isinstance(x, bytes):
  289. # XXX escape more chars? handle binary garbage
  290. x = x.replace(b"\\", b"\\\\")
  291. x = x.replace(b"(", b"\\(")
  292. x = x.replace(b")", b"\\)")
  293. return b"(" + x + b")"
  294. else:
  295. return bytes(x)
  296. class PdfParser:
  297. """Based on
  298. https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf
  299. Supports PDF up to 1.4
  300. """
  301. def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"):
  302. if buf and f:
  303. msg = "specify buf or f or filename, but not both buf and f"
  304. raise RuntimeError(msg)
  305. self.filename = filename
  306. self.buf = buf
  307. self.f = f
  308. self.start_offset = start_offset
  309. self.should_close_buf = False
  310. self.should_close_file = False
  311. if filename is not None and f is None:
  312. self.f = f = open(filename, mode)
  313. self.should_close_file = True
  314. if f is not None:
  315. self.buf = buf = self.get_buf_from_file(f)
  316. self.should_close_buf = True
  317. if not filename and hasattr(f, "name"):
  318. self.filename = f.name
  319. self.cached_objects = {}
  320. if buf:
  321. self.read_pdf_info()
  322. else:
  323. self.file_size_total = self.file_size_this = 0
  324. self.root = PdfDict()
  325. self.root_ref = None
  326. self.info = PdfDict()
  327. self.info_ref = None
  328. self.page_tree_root = {}
  329. self.pages = []
  330. self.orig_pages = []
  331. self.pages_ref = None
  332. self.last_xref_section_offset = None
  333. self.trailer_dict = {}
  334. self.xref_table = XrefTable()
  335. self.xref_table.reading_finished = True
  336. if f:
  337. self.seek_end()
  338. def __enter__(self):
  339. return self
  340. def __exit__(self, exc_type, exc_value, traceback):
  341. self.close()
  342. return False # do not suppress exceptions
  343. def start_writing(self):
  344. self.close_buf()
  345. self.seek_end()
  346. def close_buf(self):
  347. try:
  348. self.buf.close()
  349. except AttributeError:
  350. pass
  351. self.buf = None
  352. def close(self):
  353. if self.should_close_buf:
  354. self.close_buf()
  355. if self.f is not None and self.should_close_file:
  356. self.f.close()
  357. self.f = None
  358. def seek_end(self):
  359. self.f.seek(0, os.SEEK_END)
  360. def write_header(self):
  361. self.f.write(b"%PDF-1.4\n")
  362. def write_comment(self, s):
  363. self.f.write(f"% {s}\n".encode())
  364. def write_catalog(self):
  365. self.del_root()
  366. self.root_ref = self.next_object_id(self.f.tell())
  367. self.pages_ref = self.next_object_id(0)
  368. self.rewrite_pages()
  369. self.write_obj(self.root_ref, Type=PdfName(b"Catalog"), Pages=self.pages_ref)
  370. self.write_obj(
  371. self.pages_ref,
  372. Type=PdfName(b"Pages"),
  373. Count=len(self.pages),
  374. Kids=self.pages,
  375. )
  376. return self.root_ref
  377. def rewrite_pages(self):
  378. pages_tree_nodes_to_delete = []
  379. for i, page_ref in enumerate(self.orig_pages):
  380. page_info = self.cached_objects[page_ref]
  381. del self.xref_table[page_ref.object_id]
  382. pages_tree_nodes_to_delete.append(page_info[PdfName(b"Parent")])
  383. if page_ref not in self.pages:
  384. # the page has been deleted
  385. continue
  386. # make dict keys into strings for passing to write_page
  387. stringified_page_info = {}
  388. for key, value in page_info.items():
  389. # key should be a PdfName
  390. stringified_page_info[key.name_as_str()] = value
  391. stringified_page_info["Parent"] = self.pages_ref
  392. new_page_ref = self.write_page(None, **stringified_page_info)
  393. for j, cur_page_ref in enumerate(self.pages):
  394. if cur_page_ref == page_ref:
  395. # replace the page reference with the new one
  396. self.pages[j] = new_page_ref
  397. # delete redundant Pages tree nodes from xref table
  398. for pages_tree_node_ref in pages_tree_nodes_to_delete:
  399. while pages_tree_node_ref:
  400. pages_tree_node = self.cached_objects[pages_tree_node_ref]
  401. if pages_tree_node_ref.object_id in self.xref_table:
  402. del self.xref_table[pages_tree_node_ref.object_id]
  403. pages_tree_node_ref = pages_tree_node.get(b"Parent", None)
  404. self.orig_pages = []
  405. def write_xref_and_trailer(self, new_root_ref=None):
  406. if new_root_ref:
  407. self.del_root()
  408. self.root_ref = new_root_ref
  409. if self.info:
  410. self.info_ref = self.write_obj(None, self.info)
  411. start_xref = self.xref_table.write(self.f)
  412. num_entries = len(self.xref_table)
  413. trailer_dict = {b"Root": self.root_ref, b"Size": num_entries}
  414. if self.last_xref_section_offset is not None:
  415. trailer_dict[b"Prev"] = self.last_xref_section_offset
  416. if self.info:
  417. trailer_dict[b"Info"] = self.info_ref
  418. self.last_xref_section_offset = start_xref
  419. self.f.write(
  420. b"trailer\n"
  421. + bytes(PdfDict(trailer_dict))
  422. + b"\nstartxref\n%d\n%%%%EOF" % start_xref
  423. )
  424. def write_page(self, ref, *objs, **dict_obj):
  425. if isinstance(ref, int):
  426. ref = self.pages[ref]
  427. if "Type" not in dict_obj:
  428. dict_obj["Type"] = PdfName(b"Page")
  429. if "Parent" not in dict_obj:
  430. dict_obj["Parent"] = self.pages_ref
  431. return self.write_obj(ref, *objs, **dict_obj)
  432. def write_obj(self, ref, *objs, **dict_obj):
  433. f = self.f
  434. if ref is None:
  435. ref = self.next_object_id(f.tell())
  436. else:
  437. self.xref_table[ref.object_id] = (f.tell(), ref.generation)
  438. f.write(bytes(IndirectObjectDef(*ref)))
  439. stream = dict_obj.pop("stream", None)
  440. if stream is not None:
  441. dict_obj["Length"] = len(stream)
  442. if dict_obj:
  443. f.write(pdf_repr(dict_obj))
  444. for obj in objs:
  445. f.write(pdf_repr(obj))
  446. if stream is not None:
  447. f.write(b"stream\n")
  448. f.write(stream)
  449. f.write(b"\nendstream\n")
  450. f.write(b"endobj\n")
  451. return ref
  452. def del_root(self):
  453. if self.root_ref is None:
  454. return
  455. del self.xref_table[self.root_ref.object_id]
  456. del self.xref_table[self.root[b"Pages"].object_id]
  457. @staticmethod
  458. def get_buf_from_file(f):
  459. if hasattr(f, "getbuffer"):
  460. return f.getbuffer()
  461. elif hasattr(f, "getvalue"):
  462. return f.getvalue()
  463. else:
  464. try:
  465. return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
  466. except ValueError: # cannot mmap an empty file
  467. return b""
  468. def read_pdf_info(self):
  469. self.file_size_total = len(self.buf)
  470. self.file_size_this = self.file_size_total - self.start_offset
  471. self.read_trailer()
  472. self.root_ref = self.trailer_dict[b"Root"]
  473. self.info_ref = self.trailer_dict.get(b"Info", None)
  474. self.root = PdfDict(self.read_indirect(self.root_ref))
  475. if self.info_ref is None:
  476. self.info = PdfDict()
  477. else:
  478. self.info = PdfDict(self.read_indirect(self.info_ref))
  479. check_format_condition(b"Type" in self.root, "/Type missing in Root")
  480. check_format_condition(
  481. self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog"
  482. )
  483. check_format_condition(b"Pages" in self.root, "/Pages missing in Root")
  484. check_format_condition(
  485. isinstance(self.root[b"Pages"], IndirectReference),
  486. "/Pages in Root is not an indirect reference",
  487. )
  488. self.pages_ref = self.root[b"Pages"]
  489. self.page_tree_root = self.read_indirect(self.pages_ref)
  490. self.pages = self.linearize_page_tree(self.page_tree_root)
  491. # save the original list of page references
  492. # in case the user modifies, adds or deletes some pages
  493. # and we need to rewrite the pages and their list
  494. self.orig_pages = self.pages[:]
  495. def next_object_id(self, offset=None):
  496. try:
  497. # TODO: support reuse of deleted objects
  498. reference = IndirectReference(max(self.xref_table.keys()) + 1, 0)
  499. except ValueError:
  500. reference = IndirectReference(1, 0)
  501. if offset is not None:
  502. self.xref_table[reference.object_id] = (offset, 0)
  503. return reference
  504. delimiter = rb"[][()<>{}/%]"
  505. delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]"
  506. whitespace = rb"[\000\011\012\014\015\040]"
  507. whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]"
  508. whitespace_optional = whitespace + b"*"
  509. whitespace_mandatory = whitespace + b"+"
  510. # No "\012" aka "\n" or "\015" aka "\r":
  511. whitespace_optional_no_nl = rb"[\000\011\014\040]*"
  512. newline_only = rb"[\r\n]+"
  513. newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl
  514. re_trailer_end = re.compile(
  515. whitespace_mandatory
  516. + rb"trailer"
  517. + whitespace_optional
  518. + rb"<<(.*>>)"
  519. + newline
  520. + rb"startxref"
  521. + newline
  522. + rb"([0-9]+)"
  523. + newline
  524. + rb"%%EOF"
  525. + whitespace_optional
  526. + rb"$",
  527. re.DOTALL,
  528. )
  529. re_trailer_prev = re.compile(
  530. whitespace_optional
  531. + rb"trailer"
  532. + whitespace_optional
  533. + rb"<<(.*?>>)"
  534. + newline
  535. + rb"startxref"
  536. + newline
  537. + rb"([0-9]+)"
  538. + newline
  539. + rb"%%EOF"
  540. + whitespace_optional,
  541. re.DOTALL,
  542. )
  543. def read_trailer(self):
  544. search_start_offset = len(self.buf) - 16384
  545. if search_start_offset < self.start_offset:
  546. search_start_offset = self.start_offset
  547. m = self.re_trailer_end.search(self.buf, search_start_offset)
  548. check_format_condition(m, "trailer end not found")
  549. # make sure we found the LAST trailer
  550. last_match = m
  551. while m:
  552. last_match = m
  553. m = self.re_trailer_end.search(self.buf, m.start() + 16)
  554. if not m:
  555. m = last_match
  556. trailer_data = m.group(1)
  557. self.last_xref_section_offset = int(m.group(2))
  558. self.trailer_dict = self.interpret_trailer(trailer_data)
  559. self.xref_table = XrefTable()
  560. self.read_xref_table(xref_section_offset=self.last_xref_section_offset)
  561. if b"Prev" in self.trailer_dict:
  562. self.read_prev_trailer(self.trailer_dict[b"Prev"])
  563. def read_prev_trailer(self, xref_section_offset):
  564. trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset)
  565. m = self.re_trailer_prev.search(
  566. self.buf[trailer_offset : trailer_offset + 16384]
  567. )
  568. check_format_condition(m, "previous trailer not found")
  569. trailer_data = m.group(1)
  570. check_format_condition(
  571. int(m.group(2)) == xref_section_offset,
  572. "xref section offset in previous trailer doesn't match what was expected",
  573. )
  574. trailer_dict = self.interpret_trailer(trailer_data)
  575. if b"Prev" in trailer_dict:
  576. self.read_prev_trailer(trailer_dict[b"Prev"])
  577. re_whitespace_optional = re.compile(whitespace_optional)
  578. re_name = re.compile(
  579. whitespace_optional
  580. + rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?="
  581. + delimiter_or_ws
  582. + rb")"
  583. )
  584. re_dict_start = re.compile(whitespace_optional + rb"<<")
  585. re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional)
  586. @classmethod
  587. def interpret_trailer(cls, trailer_data):
  588. trailer = {}
  589. offset = 0
  590. while True:
  591. m = cls.re_name.match(trailer_data, offset)
  592. if not m:
  593. m = cls.re_dict_end.match(trailer_data, offset)
  594. check_format_condition(
  595. m and m.end() == len(trailer_data),
  596. "name not found in trailer, remaining data: "
  597. + repr(trailer_data[offset:]),
  598. )
  599. break
  600. key = cls.interpret_name(m.group(1))
  601. value, offset = cls.get_value(trailer_data, m.end())
  602. trailer[key] = value
  603. check_format_condition(
  604. b"Size" in trailer and isinstance(trailer[b"Size"], int),
  605. "/Size not in trailer or not an integer",
  606. )
  607. check_format_condition(
  608. b"Root" in trailer and isinstance(trailer[b"Root"], IndirectReference),
  609. "/Root not in trailer or not an indirect reference",
  610. )
  611. return trailer
  612. re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?")
  613. @classmethod
  614. def interpret_name(cls, raw, as_text=False):
  615. name = b""
  616. for m in cls.re_hashes_in_name.finditer(raw):
  617. if m.group(3):
  618. name += m.group(1) + bytearray.fromhex(m.group(3).decode("us-ascii"))
  619. else:
  620. name += m.group(1)
  621. if as_text:
  622. return name.decode("utf-8")
  623. else:
  624. return bytes(name)
  625. re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")")
  626. re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")")
  627. re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")")
  628. re_int = re.compile(
  629. whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")"
  630. )
  631. re_real = re.compile(
  632. whitespace_optional
  633. + rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?="
  634. + delimiter_or_ws
  635. + rb")"
  636. )
  637. re_array_start = re.compile(whitespace_optional + rb"\[")
  638. re_array_end = re.compile(whitespace_optional + rb"]")
  639. re_string_hex = re.compile(
  640. whitespace_optional + rb"<(" + whitespace_or_hex + rb"*)>"
  641. )
  642. re_string_lit = re.compile(whitespace_optional + rb"\(")
  643. re_indirect_reference = re.compile(
  644. whitespace_optional
  645. + rb"([-+]?[0-9]+)"
  646. + whitespace_mandatory
  647. + rb"([-+]?[0-9]+)"
  648. + whitespace_mandatory
  649. + rb"R(?="
  650. + delimiter_or_ws
  651. + rb")"
  652. )
  653. re_indirect_def_start = re.compile(
  654. whitespace_optional
  655. + rb"([-+]?[0-9]+)"
  656. + whitespace_mandatory
  657. + rb"([-+]?[0-9]+)"
  658. + whitespace_mandatory
  659. + rb"obj(?="
  660. + delimiter_or_ws
  661. + rb")"
  662. )
  663. re_indirect_def_end = re.compile(
  664. whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")"
  665. )
  666. re_comment = re.compile(
  667. rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*"
  668. )
  669. re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n")
  670. re_stream_end = re.compile(
  671. whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")"
  672. )
  673. @classmethod
  674. def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1):
  675. if max_nesting == 0:
  676. return None, None
  677. m = cls.re_comment.match(data, offset)
  678. if m:
  679. offset = m.end()
  680. m = cls.re_indirect_def_start.match(data, offset)
  681. if m:
  682. check_format_condition(
  683. int(m.group(1)) > 0,
  684. "indirect object definition: object ID must be greater than 0",
  685. )
  686. check_format_condition(
  687. int(m.group(2)) >= 0,
  688. "indirect object definition: generation must be non-negative",
  689. )
  690. check_format_condition(
  691. expect_indirect is None
  692. or expect_indirect
  693. == IndirectReference(int(m.group(1)), int(m.group(2))),
  694. "indirect object definition different than expected",
  695. )
  696. object, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1)
  697. if offset is None:
  698. return object, None
  699. m = cls.re_indirect_def_end.match(data, offset)
  700. check_format_condition(m, "indirect object definition end not found")
  701. return object, m.end()
  702. check_format_condition(
  703. not expect_indirect, "indirect object definition not found"
  704. )
  705. m = cls.re_indirect_reference.match(data, offset)
  706. if m:
  707. check_format_condition(
  708. int(m.group(1)) > 0,
  709. "indirect object reference: object ID must be greater than 0",
  710. )
  711. check_format_condition(
  712. int(m.group(2)) >= 0,
  713. "indirect object reference: generation must be non-negative",
  714. )
  715. return IndirectReference(int(m.group(1)), int(m.group(2))), m.end()
  716. m = cls.re_dict_start.match(data, offset)
  717. if m:
  718. offset = m.end()
  719. result = {}
  720. m = cls.re_dict_end.match(data, offset)
  721. while not m:
  722. key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
  723. if offset is None:
  724. return result, None
  725. value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
  726. result[key] = value
  727. if offset is None:
  728. return result, None
  729. m = cls.re_dict_end.match(data, offset)
  730. offset = m.end()
  731. m = cls.re_stream_start.match(data, offset)
  732. if m:
  733. try:
  734. stream_len = int(result[b"Length"])
  735. except (TypeError, KeyError, ValueError) as e:
  736. msg = "bad or missing Length in stream dict (%r)" % result.get(
  737. b"Length", None
  738. )
  739. raise PdfFormatError(msg) from e
  740. stream_data = data[m.end() : m.end() + stream_len]
  741. m = cls.re_stream_end.match(data, m.end() + stream_len)
  742. check_format_condition(m, "stream end not found")
  743. offset = m.end()
  744. result = PdfStream(PdfDict(result), stream_data)
  745. else:
  746. result = PdfDict(result)
  747. return result, offset
  748. m = cls.re_array_start.match(data, offset)
  749. if m:
  750. offset = m.end()
  751. result = []
  752. m = cls.re_array_end.match(data, offset)
  753. while not m:
  754. value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1)
  755. result.append(value)
  756. if offset is None:
  757. return result, None
  758. m = cls.re_array_end.match(data, offset)
  759. return result, m.end()
  760. m = cls.re_null.match(data, offset)
  761. if m:
  762. return None, m.end()
  763. m = cls.re_true.match(data, offset)
  764. if m:
  765. return True, m.end()
  766. m = cls.re_false.match(data, offset)
  767. if m:
  768. return False, m.end()
  769. m = cls.re_name.match(data, offset)
  770. if m:
  771. return PdfName(cls.interpret_name(m.group(1))), m.end()
  772. m = cls.re_int.match(data, offset)
  773. if m:
  774. return int(m.group(1)), m.end()
  775. m = cls.re_real.match(data, offset)
  776. if m:
  777. # XXX Decimal instead of float???
  778. return float(m.group(1)), m.end()
  779. m = cls.re_string_hex.match(data, offset)
  780. if m:
  781. # filter out whitespace
  782. hex_string = bytearray(
  783. b for b in m.group(1) if b in b"0123456789abcdefABCDEF"
  784. )
  785. if len(hex_string) % 2 == 1:
  786. # append a 0 if the length is not even - yes, at the end
  787. hex_string.append(ord(b"0"))
  788. return bytearray.fromhex(hex_string.decode("us-ascii")), m.end()
  789. m = cls.re_string_lit.match(data, offset)
  790. if m:
  791. return cls.get_literal_string(data, m.end())
  792. # return None, offset # fallback (only for debugging)
  793. msg = "unrecognized object: " + repr(data[offset : offset + 32])
  794. raise PdfFormatError(msg)
  795. re_lit_str_token = re.compile(
  796. rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))"
  797. )
  798. escaped_chars = {
  799. b"n": b"\n",
  800. b"r": b"\r",
  801. b"t": b"\t",
  802. b"b": b"\b",
  803. b"f": b"\f",
  804. b"(": b"(",
  805. b")": b")",
  806. b"\\": b"\\",
  807. ord(b"n"): b"\n",
  808. ord(b"r"): b"\r",
  809. ord(b"t"): b"\t",
  810. ord(b"b"): b"\b",
  811. ord(b"f"): b"\f",
  812. ord(b"("): b"(",
  813. ord(b")"): b")",
  814. ord(b"\\"): b"\\",
  815. }
  816. @classmethod
  817. def get_literal_string(cls, data, offset):
  818. nesting_depth = 0
  819. result = bytearray()
  820. for m in cls.re_lit_str_token.finditer(data, offset):
  821. result.extend(data[offset : m.start()])
  822. if m.group(1):
  823. result.extend(cls.escaped_chars[m.group(1)[1]])
  824. elif m.group(2):
  825. result.append(int(m.group(2)[1:], 8))
  826. elif m.group(3):
  827. pass
  828. elif m.group(5):
  829. result.extend(b"\n")
  830. elif m.group(6):
  831. result.extend(b"(")
  832. nesting_depth += 1
  833. elif m.group(7):
  834. if nesting_depth == 0:
  835. return bytes(result), m.end()
  836. result.extend(b")")
  837. nesting_depth -= 1
  838. offset = m.end()
  839. msg = "unfinished literal string"
  840. raise PdfFormatError(msg)
  841. re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline)
  842. re_xref_subsection_start = re.compile(
  843. whitespace_optional
  844. + rb"([0-9]+)"
  845. + whitespace_mandatory
  846. + rb"([0-9]+)"
  847. + whitespace_optional
  848. + newline_only
  849. )
  850. re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
  851. def read_xref_table(self, xref_section_offset):
  852. subsection_found = False
  853. m = self.re_xref_section_start.match(
  854. self.buf, xref_section_offset + self.start_offset
  855. )
  856. check_format_condition(m, "xref section start not found")
  857. offset = m.end()
  858. while True:
  859. m = self.re_xref_subsection_start.match(self.buf, offset)
  860. if not m:
  861. check_format_condition(
  862. subsection_found, "xref subsection start not found"
  863. )
  864. break
  865. subsection_found = True
  866. offset = m.end()
  867. first_object = int(m.group(1))
  868. num_objects = int(m.group(2))
  869. for i in range(first_object, first_object + num_objects):
  870. m = self.re_xref_entry.match(self.buf, offset)
  871. check_format_condition(m, "xref entry not found")
  872. offset = m.end()
  873. is_free = m.group(3) == b"f"
  874. generation = int(m.group(2))
  875. if not is_free:
  876. new_entry = (int(m.group(1)), generation)
  877. check_format_condition(
  878. i not in self.xref_table or self.xref_table[i] == new_entry,
  879. "xref entry duplicated (and not identical)",
  880. )
  881. self.xref_table[i] = new_entry
  882. return offset
  883. def read_indirect(self, ref, max_nesting=-1):
  884. offset, generation = self.xref_table[ref[0]]
  885. check_format_condition(
  886. generation == ref[1],
  887. f"expected to find generation {ref[1]} for object ID {ref[0]} in xref "
  888. f"table, instead found generation {generation} at offset {offset}",
  889. )
  890. value = self.get_value(
  891. self.buf,
  892. offset + self.start_offset,
  893. expect_indirect=IndirectReference(*ref),
  894. max_nesting=max_nesting,
  895. )[0]
  896. self.cached_objects[ref] = value
  897. return value
  898. def linearize_page_tree(self, node=None):
  899. if node is None:
  900. node = self.page_tree_root
  901. check_format_condition(
  902. node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
  903. )
  904. pages = []
  905. for kid in node[b"Kids"]:
  906. kid_object = self.read_indirect(kid)
  907. if kid_object[b"Type"] == b"Page":
  908. pages.append(kid)
  909. else:
  910. pages.extend(self.linearize_page_tree(node=kid_object))
  911. return pages