CrossReference.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. /**
  3. * This file is part of FPDI
  4. *
  5. * @package Fpdi
  6. * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com)
  7. * @license http://opensource.org/licenses/mit-license The MIT License
  8. */
  9. namespace Fpdi\PdfParser\CrossReference;
  10. use Fpdi\PdfParser\PdfParser;
  11. use Fpdi\PdfParser\Type\PdfDictionary;
  12. use Fpdi\PdfParser\Type\PdfIndirectObject;
  13. use Fpdi\PdfParser\Type\PdfNumeric;
  14. use Fpdi\PdfParser\Type\PdfStream;
  15. use Fpdi\PdfParser\Type\PdfToken;
  16. use Fpdi\PdfParser\Type\PdfTypeException;
  17. /**
  18. * Class CrossReference
  19. *
  20. * This class processes the standard cross reference of a PDF document.
  21. */
  22. class CrossReference
  23. {
  24. /**
  25. * The byte length in which the "startxref" keyword should be searched.
  26. *
  27. * @var int
  28. */
  29. public static $trailerSearchLength = 5500;
  30. /**
  31. * @var int
  32. */
  33. protected $fileHeaderOffset = 0;
  34. /**
  35. * @var PdfParser
  36. */
  37. protected $parser;
  38. /**
  39. * @var ReaderInterface[]
  40. */
  41. protected $readers = [];
  42. /**
  43. * CrossReference constructor.
  44. *
  45. * @param PdfParser $parser
  46. * @throws CrossReferenceException
  47. * @throws PdfTypeException
  48. */
  49. public function __construct(PdfParser $parser, $fileHeaderOffset = 0)
  50. {
  51. $this->parser = $parser;
  52. $this->fileHeaderOffset = $fileHeaderOffset;
  53. $offset = $this->findStartXref();
  54. $reader = null;
  55. /** @noinspection TypeUnsafeComparisonInspection */
  56. while ($offset != false) { // By doing an unsafe comparsion we ignore faulty references to byte offset 0
  57. try {
  58. $reader = $this->readXref($offset + $this->fileHeaderOffset);
  59. } catch (CrossReferenceException $e) {
  60. // sometimes the file header offset is part of the byte offsets, so let's retry by resetting it to zero.
  61. if ($e->getCode() === CrossReferenceException::INVALID_DATA && $this->fileHeaderOffset !== 0) {
  62. $this->fileHeaderOffset = 0;
  63. $reader = $this->readXref($offset + $this->fileHeaderOffset);
  64. } else {
  65. throw $e;
  66. }
  67. }
  68. $trailer = $reader->getTrailer();
  69. $this->checkForEncryption($trailer);
  70. $this->readers[] = $reader;
  71. if (isset($trailer->value['Prev'])) {
  72. $offset = $trailer->value['Prev']->value;
  73. } else {
  74. $offset = false;
  75. }
  76. }
  77. // fix faulty sub-section header
  78. if ($reader instanceof FixedReader) {
  79. /**
  80. * @var FixedReader $reader
  81. */
  82. $reader->fixFaultySubSectionShift();
  83. }
  84. if ($reader === null) {
  85. throw new CrossReferenceException('No cross-reference found.', CrossReferenceException::NO_XREF_FOUND);
  86. }
  87. }
  88. /**
  89. * Get the size of the cross reference.
  90. *
  91. * @return integer
  92. */
  93. public function getSize()
  94. {
  95. return $this->getTrailer()->value['Size']->value;
  96. }
  97. /**
  98. * Get the trailer dictionary.
  99. *
  100. * @return PdfDictionary
  101. */
  102. public function getTrailer()
  103. {
  104. return $this->readers[0]->getTrailer();
  105. }
  106. /**
  107. * Get the cross reference readser instances.
  108. *
  109. * @return ReaderInterface[]
  110. */
  111. public function getReaders()
  112. {
  113. return $this->readers;
  114. }
  115. /**
  116. * Get the offset by an object number.
  117. *
  118. * @param int $objectNumber
  119. * @return integer|bool
  120. */
  121. public function getOffsetFor($objectNumber)
  122. {
  123. foreach ($this->getReaders() as $reader) {
  124. $offset = $reader->getOffsetFor($objectNumber);
  125. if ($offset !== false) {
  126. return $offset;
  127. }
  128. }
  129. return false;
  130. }
  131. /**
  132. * Get an indirect object by its object number.
  133. *
  134. * @param int $objectNumber
  135. * @return PdfIndirectObject
  136. * @throws CrossReferenceException
  137. */
  138. public function getIndirectObject($objectNumber)
  139. {
  140. $offset = $this->getOffsetFor($objectNumber);
  141. if ($offset === false) {
  142. throw new CrossReferenceException(
  143. \sprintf('Object (id:%s) not found.', $objectNumber),
  144. CrossReferenceException::OBJECT_NOT_FOUND
  145. );
  146. }
  147. $parser = $this->parser;
  148. $parser->getTokenizer()->clearStack();
  149. $parser->getStreamReader()->reset($offset + $this->fileHeaderOffset);
  150. try {
  151. /** @var PdfIndirectObject $object */
  152. $object = $parser->readValue(null, PdfIndirectObject::class);
  153. } catch (PdfTypeException $e) {
  154. throw new CrossReferenceException(
  155. \sprintf('Object (id:%s) not found at location (%s).', $objectNumber, $offset),
  156. CrossReferenceException::OBJECT_NOT_FOUND,
  157. $e
  158. );
  159. }
  160. if ($object->objectNumber !== $objectNumber) {
  161. throw new CrossReferenceException(
  162. \sprintf('Wrong object found, got %s while %s was expected.', $object->objectNumber, $objectNumber),
  163. CrossReferenceException::OBJECT_NOT_FOUND
  164. );
  165. }
  166. return $object;
  167. }
  168. /**
  169. * Read the cross-reference table at a given offset.
  170. *
  171. * Internally the method will try to evaluate the best reader for this cross-reference.
  172. *
  173. * @param int $offset
  174. * @return ReaderInterface
  175. * @throws CrossReferenceException
  176. * @throws PdfTypeException
  177. */
  178. protected function readXref($offset)
  179. {
  180. $this->parser->getStreamReader()->reset($offset);
  181. $this->parser->getTokenizer()->clearStack();
  182. $initValue = $this->parser->readValue();
  183. return $this->initReaderInstance($initValue);
  184. }
  185. /**
  186. * Get a cross-reference reader instance.
  187. *
  188. * @param PdfToken|PdfIndirectObject $initValue
  189. * @return ReaderInterface|bool
  190. * @throws CrossReferenceException
  191. * @throws PdfTypeException
  192. */
  193. protected function initReaderInstance($initValue)
  194. {
  195. $position = $this->parser->getStreamReader()->getPosition()
  196. + $this->parser->getStreamReader()->getOffset() + $this->fileHeaderOffset;
  197. if ($initValue instanceof PdfToken && $initValue->value === 'xref') {
  198. try {
  199. return new FixedReader($this->parser);
  200. } catch (CrossReferenceException $e) {
  201. $this->parser->getStreamReader()->reset($position);
  202. $this->parser->getTokenizer()->clearStack();
  203. return new LineReader($this->parser);
  204. }
  205. }
  206. if ($initValue instanceof PdfIndirectObject) {
  207. try {
  208. $stream = PdfStream::ensure($initValue->value);
  209. } catch (PdfTypeException $e) {
  210. throw new CrossReferenceException(
  211. 'Invalid object type at xref reference offset.',
  212. CrossReferenceException::INVALID_DATA,
  213. $e
  214. );
  215. }
  216. $type = PdfDictionary::get($stream->value, 'Type');
  217. if ($type->value !== 'XRef') {
  218. throw new CrossReferenceException(
  219. 'The xref position points to an incorrect object type.',
  220. CrossReferenceException::INVALID_DATA
  221. );
  222. }
  223. $this->checkForEncryption($stream->value);
  224. throw new CrossReferenceException(
  225. 'This PDF document probably uses a compression technique which is not supported by the ' .
  226. 'free parser shipped with FPDI. (See https://www.setasign.com/fpdi-pdf-parser for more details)',
  227. CrossReferenceException::COMPRESSED_XREF
  228. );
  229. }
  230. throw new CrossReferenceException(
  231. 'The xref position points to an incorrect object type.',
  232. CrossReferenceException::INVALID_DATA
  233. );
  234. }
  235. /**
  236. * Check for encryption.
  237. *
  238. * @param PdfDictionary $dictionary
  239. * @throws CrossReferenceException
  240. */
  241. protected function checkForEncryption(PdfDictionary $dictionary)
  242. {
  243. if (isset($dictionary->value['Encrypt'])) {
  244. throw new CrossReferenceException(
  245. 'This PDF document is encrypted and cannot be processed with FPDI.',
  246. CrossReferenceException::ENCRYPTED
  247. );
  248. }
  249. }
  250. /**
  251. * Find the start position for the first cross-reference.
  252. *
  253. * @return int The byte-offset position of the first cross-reference.
  254. * @throws CrossReferenceException
  255. */
  256. protected function findStartXref()
  257. {
  258. $reader = $this->parser->getStreamReader();
  259. $reader->reset(-self::$trailerSearchLength, self::$trailerSearchLength);
  260. $buffer = $reader->getBuffer(false);
  261. $pos = \strrpos($buffer, 'startxref');
  262. $addOffset = 9;
  263. if ($pos === false) {
  264. // Some corrupted documents uses startref, instead of startxref
  265. $pos = \strrpos($buffer, 'startref');
  266. if ($pos === false) {
  267. throw new CrossReferenceException(
  268. 'Unable to find pointer to xref table',
  269. CrossReferenceException::NO_STARTXREF_FOUND
  270. );
  271. }
  272. $addOffset = 8;
  273. }
  274. $reader->setOffset($pos + $addOffset);
  275. try {
  276. $value = $this->parser->readValue(null, PdfNumeric::class);
  277. } catch (PdfTypeException $e) {
  278. throw new CrossReferenceException(
  279. 'Invalid data after startxref keyword.',
  280. CrossReferenceException::INVALID_DATA,
  281. $e
  282. );
  283. }
  284. return $value->value;
  285. }
  286. }