1<?php 2 3/** 4 * This file is part of FPDI 5 * 6 * @package setasign\Fpdi 7 * @copyright Copyright (c) 2024 Setasign GmbH & Co. KG (https://www.setasign.com) 8 * @license http://opensource.org/licenses/mit-license The MIT License 9 */ 10 11namespace setasign\Fpdi\PdfReader; 12 13use setasign\Fpdi\FpdiException; 14use setasign\Fpdi\GraphicsState; 15use setasign\Fpdi\Math\Vector; 16use setasign\Fpdi\PdfParser\Filter\FilterException; 17use setasign\Fpdi\PdfParser\PdfParser; 18use setasign\Fpdi\PdfParser\PdfParserException; 19use setasign\Fpdi\PdfParser\Type\PdfArray; 20use setasign\Fpdi\PdfParser\Type\PdfDictionary; 21use setasign\Fpdi\PdfParser\Type\PdfHexString; 22use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; 23use setasign\Fpdi\PdfParser\Type\PdfName; 24use setasign\Fpdi\PdfParser\Type\PdfNull; 25use setasign\Fpdi\PdfParser\Type\PdfNumeric; 26use setasign\Fpdi\PdfParser\Type\PdfStream; 27use setasign\Fpdi\PdfParser\Type\PdfString; 28use setasign\Fpdi\PdfParser\Type\PdfType; 29use setasign\Fpdi\PdfParser\Type\PdfTypeException; 30use setasign\Fpdi\PdfReader\DataStructure\Rectangle; 31use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; 32 33/** 34 * Class representing a page of a PDF document 35 */ 36class Page 37{ 38 /** 39 * @var PdfIndirectObject 40 */ 41 protected $pageObject; 42 43 /** 44 * @var PdfDictionary 45 */ 46 protected $pageDictionary; 47 48 /** 49 * @var PdfParser 50 */ 51 protected $parser; 52 53 /** 54 * Inherited attributes 55 * 56 * @var null|array 57 */ 58 protected $inheritedAttributes; 59 60 /** 61 * Page constructor. 62 * 63 * @param PdfIndirectObject $page 64 * @param PdfParser $parser 65 */ 66 public function __construct(PdfIndirectObject $page, PdfParser $parser) 67 { 68 $this->pageObject = $page; 69 $this->parser = $parser; 70 } 71 72 /** 73 * Get the indirect object of this page. 74 * 75 * @return PdfIndirectObject 76 */ 77 public function getPageObject() 78 { 79 return $this->pageObject; 80 } 81 82 /** 83 * Get the dictionary of this page. 84 * 85 * @return PdfDictionary 86 * @throws PdfParserException 87 * @throws PdfTypeException 88 * @throws CrossReferenceException 89 */ 90 public function getPageDictionary() 91 { 92 if ($this->pageDictionary === null) { 93 $this->pageDictionary = PdfDictionary::ensure(PdfType::resolve($this->getPageObject(), $this->parser)); 94 } 95 96 return $this->pageDictionary; 97 } 98 99 /** 100 * Get a page attribute. 101 * 102 * @param string $name 103 * @param bool $inherited 104 * @return PdfType|null 105 * @throws PdfParserException 106 * @throws PdfTypeException 107 * @throws CrossReferenceException 108 */ 109 public function getAttribute($name, $inherited = true) 110 { 111 $dict = $this->getPageDictionary(); 112 113 if (isset($dict->value[$name])) { 114 return $dict->value[$name]; 115 } 116 117 $inheritedKeys = ['Resources', 'MediaBox', 'CropBox', 'Rotate']; 118 if ($inherited && \in_array($name, $inheritedKeys, true)) { 119 if ($this->inheritedAttributes === null) { 120 $this->inheritedAttributes = []; 121 $inheritedKeys = \array_filter($inheritedKeys, function ($key) use ($dict) { 122 return !isset($dict->value[$key]); 123 }); 124 125 if (\count($inheritedKeys) > 0) { 126 $parentDict = PdfType::resolve(PdfDictionary::get($dict, 'Parent'), $this->parser); 127 while ($parentDict instanceof PdfDictionary) { 128 foreach ($inheritedKeys as $index => $key) { 129 if (isset($parentDict->value[$key])) { 130 $this->inheritedAttributes[$key] = $parentDict->value[$key]; 131 unset($inheritedKeys[$index]); 132 } 133 } 134 135 /** @noinspection NotOptimalIfConditionsInspection */ 136 if (isset($parentDict->value['Parent']) && \count($inheritedKeys) > 0) { 137 $parentDict = PdfType::resolve(PdfDictionary::get($parentDict, 'Parent'), $this->parser); 138 } else { 139 break; 140 } 141 } 142 } 143 } 144 145 if (isset($this->inheritedAttributes[$name])) { 146 return $this->inheritedAttributes[$name]; 147 } 148 } 149 150 return null; 151 } 152 153 /** 154 * Get the rotation value. 155 * 156 * @return int 157 * @throws PdfParserException 158 * @throws PdfTypeException 159 * @throws CrossReferenceException 160 */ 161 public function getRotation() 162 { 163 $rotation = $this->getAttribute('Rotate'); 164 if ($rotation === null) { 165 return 0; 166 } 167 168 $rotation = PdfNumeric::ensure(PdfType::resolve($rotation, $this->parser))->value % 360; 169 170 if ($rotation < 0) { 171 $rotation += 360; 172 } 173 174 return $rotation; 175 } 176 177 /** 178 * Get a boundary of this page. 179 * 180 * @param string $box 181 * @param bool $fallback 182 * @return bool|Rectangle 183 * @throws PdfParserException 184 * @throws PdfTypeException 185 * @throws CrossReferenceException 186 * @see PageBoundaries 187 */ 188 public function getBoundary($box = PageBoundaries::CROP_BOX, $fallback = true) 189 { 190 $value = $this->getAttribute($box); 191 192 if ($value !== null) { 193 return Rectangle::byPdfArray($value, $this->parser); 194 } 195 196 if ($fallback === false) { 197 return false; 198 } 199 200 switch ($box) { 201 case PageBoundaries::BLEED_BOX: 202 case PageBoundaries::TRIM_BOX: 203 case PageBoundaries::ART_BOX: 204 return $this->getBoundary(PageBoundaries::CROP_BOX, true); 205 case PageBoundaries::CROP_BOX: 206 return $this->getBoundary(PageBoundaries::MEDIA_BOX, true); 207 } 208 209 return false; 210 } 211 212 /** 213 * Get the width and height of this page. 214 * 215 * @param string $box 216 * @param bool $fallback 217 * @return array|bool 218 * @throws PdfParserException 219 * @throws PdfTypeException 220 * @throws CrossReferenceException 221 */ 222 public function getWidthAndHeight($box = PageBoundaries::CROP_BOX, $fallback = true) 223 { 224 $boundary = $this->getBoundary($box, $fallback); 225 if ($boundary === false) { 226 return false; 227 } 228 229 $rotation = $this->getRotation(); 230 $interchange = ($rotation / 90) % 2; 231 232 return [ 233 $interchange ? $boundary->getHeight() : $boundary->getWidth(), 234 $interchange ? $boundary->getWidth() : $boundary->getHeight() 235 ]; 236 } 237 238 /** 239 * Get the raw content stream. 240 * 241 * @return string 242 * @throws PdfReaderException 243 * @throws PdfTypeException 244 * @throws FilterException 245 * @throws PdfParserException 246 */ 247 public function getContentStream() 248 { 249 $dict = $this->getPageDictionary(); 250 $contents = PdfType::resolve(PdfDictionary::get($dict, 'Contents'), $this->parser); 251 if ($contents instanceof PdfNull) { 252 return ''; 253 } 254 255 if ($contents instanceof PdfArray) { 256 $result = []; 257 foreach ($contents->value as $content) { 258 $content = PdfType::resolve($content, $this->parser); 259 if (!($content instanceof PdfStream)) { 260 continue; 261 } 262 $result[] = $content->getUnfilteredStream(); 263 } 264 265 return \implode("\n", $result); 266 } 267 268 if ($contents instanceof PdfStream) { 269 return $contents->getUnfilteredStream(); 270 } 271 272 throw new PdfReaderException( 273 'Array or stream expected.', 274 PdfReaderException::UNEXPECTED_DATA_TYPE 275 ); 276 } 277 278 /** 279 * Get information of all external links on this page. 280 * 281 * All coordinates are normalized in view to rotation and translation of the boundary-box, so that their 282 * origin is lower-left. 283 * 284 * The URI is the binary value of the PDF string object. It can be in PdfDocEncoding or in UTF-16BE encoding. 285 * 286 * @return array 287 */ 288 public function getExternalLinks($box = PageBoundaries::CROP_BOX) 289 { 290 try { 291 $dict = $this->getPageDictionary(); 292 $annotations = PdfType::resolve(PdfDictionary::get($dict, 'Annots'), $this->parser); 293 } catch (FpdiException $e) { 294 return []; 295 } 296 297 if (!$annotations instanceof PdfArray) { 298 return []; 299 } 300 301 $links = []; 302 303 foreach ($annotations->value as $entry) { 304 try { 305 $annotation = PdfType::resolve($entry, $this->parser); 306 307 $value = PdfType::resolve(PdfDictionary::get($annotation, 'Subtype'), $this->parser); 308 if (!$value instanceof PdfName || $value->value !== 'Link') { 309 continue; 310 } 311 312 $dest = PdfType::resolve(PdfDictionary::get($annotation, 'Dest'), $this->parser); 313 if (!$dest instanceof PdfNull) { 314 continue; 315 } 316 317 $action = PdfType::resolve(PdfDictionary::get($annotation, 'A'), $this->parser); 318 if (!$action instanceof PdfDictionary) { 319 continue; 320 } 321 322 $actionType = PdfType::resolve(PdfDictionary::get($action, 'S'), $this->parser); 323 if (!$actionType instanceof PdfName || $actionType->value !== 'URI') { 324 continue; 325 } 326 327 $uri = PdfType::resolve(PdfDictionary::get($action, 'URI'), $this->parser); 328 if ($uri instanceof PdfString) { 329 $uriValue = PdfString::unescape($uri->value); 330 } elseif ($uri instanceof PdfHexString) { 331 $uriValue = \hex2bin($uri->value); 332 } else { 333 continue; 334 } 335 336 $rect = PdfType::resolve(PdfDictionary::get($annotation, 'Rect'), $this->parser); 337 if (!$rect instanceof PdfArray || count($rect->value) !== 4) { 338 continue; 339 } 340 341 $rect = Rectangle::byPdfArray($rect, $this->parser); 342 if ($rect->getWidth() === 0 || $rect->getHeight() === 0) { 343 continue; 344 } 345 346 $bbox = $this->getBoundary($box); 347 $rotation = $this->getRotation(); 348 349 $gs = new GraphicsState(); 350 $gs->translate(-$bbox->getLlx(), -$bbox->getLly()); 351 $gs->rotate($bbox->getLlx(), $bbox->getLly(), -$rotation); 352 353 switch ($rotation) { 354 case 90: 355 $gs->translate(-$bbox->getWidth(), 0); 356 break; 357 case 180: 358 $gs->translate(-$bbox->getWidth(), -$bbox->getHeight()); 359 break; 360 case 270: 361 $gs->translate(0, -$bbox->getHeight()); 362 break; 363 } 364 365 $normalizedRect = Rectangle::byVectors( 366 $gs->toUserSpace(new Vector($rect->getLlx(), $rect->getLly())), 367 $gs->toUserSpace(new Vector($rect->getUrx(), $rect->getUry())) 368 ); 369 370 $quadPoints = PdfType::resolve(PdfDictionary::get($annotation, 'QuadPoints'), $this->parser); 371 $normalizedQuadPoints = []; 372 if ($quadPoints instanceof PdfArray) { 373 $quadPointsCount = count($quadPoints->value); 374 if ($quadPointsCount % 8 === 0) { 375 for ($i = 0; ($i + 1) < $quadPointsCount; $i += 2) { 376 $x = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i], $this->parser)); 377 $y = PdfNumeric::ensure(PdfType::resolve($quadPoints->value[$i + 1], $this->parser)); 378 379 $v = $gs->toUserSpace(new Vector($x->value, $y->value)); 380 $normalizedQuadPoints[] = $v->getX(); 381 $normalizedQuadPoints[] = $v->getY(); 382 } 383 } 384 } 385 386 // we remove unsupported/unneeded values here 387 unset( 388 $annotation->value['P'], 389 $annotation->value['NM'], 390 $annotation->value['AP'], 391 $annotation->value['AS'], 392 $annotation->value['Type'], 393 $annotation->value['Subtype'], 394 $annotation->value['Rect'], 395 $annotation->value['A'], 396 $annotation->value['QuadPoints'], 397 $annotation->value['Rotate'], 398 $annotation->value['M'], 399 $annotation->value['StructParent'], 400 $annotation->value['OC'] 401 ); 402 403 // ...and flatten the PDF object to eliminate any indirect references. 404 // Indirect references are a problem when writing the output in FPDF 405 // because FPDF uses pre-calculated object numbers while FPDI creates 406 // them at runtime. 407 $annotation = PdfType::flatten($annotation, $this->parser); 408 409 $links[] = [ 410 'rect' => $normalizedRect, 411 'quadPoints' => $normalizedQuadPoints, 412 'uri' => $uriValue, 413 'pdfObject' => $annotation 414 ]; 415 } catch (FpdiException $e) { 416 continue; 417 } 418 } 419 420 return $links; 421 } 422} 423