1<?php 2/** 3 * This file is part of FPDI 4 * 5 * @package setasign\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 10namespace setasign\Fpdi\PdfParser\CrossReference; 11 12use setasign\Fpdi\PdfParser\PdfParser; 13use setasign\Fpdi\PdfParser\StreamReader; 14 15/** 16 * Class LineReader 17 * 18 * This reader class read all cross-reference entries in a single run. 19 * It supports reading cross-references with e.g. invalid data (e.g. entries with a length < or > 20 bytes). 20 * 21 * @package setasign\Fpdi\PdfParser\CrossReference 22 */ 23class LineReader extends AbstractReader implements ReaderInterface 24{ 25 /** 26 * The object offsets. 27 * 28 * @var array 29 */ 30 protected $offsets; 31 32 /** 33 * LineReader constructor. 34 * 35 * @param PdfParser $parser 36 * @throws CrossReferenceException 37 */ 38 public function __construct(PdfParser $parser) 39 { 40 $this->read($this->extract($parser->getStreamReader())); 41 parent::__construct($parser); 42 } 43 44 /** 45 * @inheritdoc 46 */ 47 public function getOffsetFor($objectNumber) 48 { 49 if (isset($this->offsets[$objectNumber])) { 50 return $this->offsets[$objectNumber][0]; 51 } 52 53 return false; 54 } 55 56 /** 57 * Get all found offsets. 58 * 59 * @return array 60 */ 61 public function getOffsets() 62 { 63 return $this->offsets; 64 } 65 66 /** 67 * Extracts the cross reference data from the stream reader. 68 * 69 * @param StreamReader $reader 70 * @return string 71 * @throws CrossReferenceException 72 */ 73 protected function extract(StreamReader $reader) 74 { 75 $cycles = -1; 76 $bytesPerCycle = 100; 77 78 $reader->reset(null, $bytesPerCycle); 79 80 while ( 81 ($trailerPos = \strpos($reader->getBuffer(false), 'trailer', \max($bytesPerCycle * $cycles++, 0))) === false 82 ) { 83 if ($reader->increaseLength($bytesPerCycle) === false) { 84 break; 85 } 86 } 87 88 if ($trailerPos === false) { 89 throw new CrossReferenceException( 90 'Unexpected end of cross reference. "trailer"-keyword not found.', 91 CrossReferenceException::NO_TRAILER_FOUND 92 ); 93 } 94 95 $xrefContent = \substr($reader->getBuffer(false), 0, $trailerPos); 96 $reader->reset($reader->getPosition() + $trailerPos); 97 98 return $xrefContent; 99 } 100 101 /** 102 * Read the cross-reference entries. 103 * 104 * @param string $xrefContent 105 * @throws CrossReferenceException 106 */ 107 protected function read($xrefContent) 108 { 109 // get eol markers in the first 100 bytes 110 \preg_match_all("/(\r\n|\n|\r)/", \substr($xrefContent, 0, 100), $m); 111 112 if (\count($m[0]) === 0) { 113 throw new CrossReferenceException( 114 'No data found in cross-reference.', 115 CrossReferenceException::INVALID_DATA 116 ); 117 } 118 119 // count(array_count_values()) is faster then count(array_unique()) 120 // @see https://github.com/symfony/symfony/pull/23731 121 // can be reverted for php7.2 122 $differentLineEndings = \count(\array_count_values($m[0])); 123 if ($differentLineEndings > 1) { 124 $lines = \preg_split("/(\r\n|\n|\r)/", $xrefContent, -1, PREG_SPLIT_NO_EMPTY); 125 } else { 126 $lines = \explode($m[0][0], $xrefContent); 127 } 128 129 unset($differentLineEndings, $m); 130 $linesCount = \count($lines); 131 $start = null; 132 $entryCount = 0; 133 134 $offsets = []; 135 136 /** @noinspection ForeachInvariantsInspection */ 137 for ($i = 0; $i < $linesCount; $i++) { 138 $line = \trim($lines[$i]); 139 if ($line) { 140 $pieces = \explode(' ', $line); 141 142 $c = \count($pieces); 143 switch ($c) { 144 case 2: 145 $start = (int) $pieces[0]; 146 $entryCount += (int) $pieces[1]; 147 break; 148 149 /** @noinspection PhpMissingBreakStatementInspection */ 150 case 3: 151 switch ($pieces[2]) { 152 case 'n': 153 $offsets[$start] = [(int) $pieces[0], (int) $pieces[1]]; 154 $start++; 155 break 2; 156 case 'f': 157 $start++; 158 break 2; 159 } 160 // fall through if pieces doesn't match 161 162 default: 163 throw new CrossReferenceException( 164 \sprintf('Unexpected data in xref table (%s)', \implode(' ', $pieces)), 165 CrossReferenceException::INVALID_DATA 166 ); 167 } 168 } 169 } 170 171 $this->offsets = $offsets; 172 } 173} 174