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\Tcpdf; 12 13use setasign\Fpdi\FpdiException; 14use setasign\Fpdi\FpdiTrait; 15use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; 16use setasign\Fpdi\PdfParser\Filter\AsciiHex; 17use setasign\Fpdi\PdfParser\PdfParserException; 18use setasign\Fpdi\PdfParser\Type\PdfArray; 19use setasign\Fpdi\PdfParser\Type\PdfDictionary; 20use setasign\Fpdi\PdfParser\Type\PdfHexString; 21use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; 22use setasign\Fpdi\PdfParser\Type\PdfName; 23use setasign\Fpdi\PdfParser\Type\PdfNull; 24use setasign\Fpdi\PdfParser\Type\PdfNumeric; 25use setasign\Fpdi\PdfParser\Type\PdfStream; 26use setasign\Fpdi\PdfParser\Type\PdfString; 27use setasign\Fpdi\PdfParser\Type\PdfType; 28use setasign\Fpdi\PdfParser\Type\PdfTypeException; 29 30/** 31 * Class Fpdi 32 * 33 * This class let you import pages of existing PDF documents into a reusable structure for TCPDF. 34 * 35 * @method _encrypt_data(int $n, string $s) string 36 */ 37class Fpdi extends \TCPDF 38{ 39 use FpdiTrait { 40 writePdfType as fpdiWritePdfType; 41 useImportedPage as fpdiUseImportedPage; 42 } 43 44 /** 45 * FPDI version 46 * 47 * @string 48 */ 49 const VERSION = '2.6.4'; 50 51 /** 52 * A counter for template ids. 53 * 54 * @var int 55 */ 56 protected $templateId = 0; 57 58 /** 59 * The currently used object number. 60 * 61 * @var int|null 62 */ 63 protected $currentObjectNumber; 64 65 protected function _enddoc() 66 { 67 parent::_enddoc(); 68 $this->cleanUp(); 69 } 70 71 /** 72 * Get the next template id. 73 * 74 * @return int 75 */ 76 protected function getNextTemplateId() 77 { 78 return $this->templateId++; 79 } 80 81 /** 82 * Draws an imported page onto the page or another template. 83 * 84 * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the 85 * aspect ratio. 86 * 87 * @param mixed $tpl The template id 88 * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array 89 * with the keys "x", "y", "width", "height", "adjustPageSize". 90 * @param float|int $y The ordinate of upper-left corner. 91 * @param float|int|null $width The width. 92 * @param float|int|null $height The height. 93 * @param bool $adjustPageSize 94 * @return array The size 95 * @see FpdiTrait::getTemplateSize() 96 */ 97 public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) 98 { 99 return $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize); 100 } 101 102 /** 103 * Draws an imported page onto the page. 104 * 105 * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the 106 * aspect ratio. 107 * 108 * @param mixed $pageId The page id 109 * @param float|int|array $x The abscissa of upper-left corner. Alternatively you could use an assoc array 110 * with the keys "x", "y", "width", "height", "adjustPageSize". 111 * @param float|int $y The ordinate of upper-left corner. 112 * @param float|int|null $width The width. 113 * @param float|int|null $height The height. 114 * @param bool $adjustPageSize 115 * @return array The size. 116 * @see Fpdi::getTemplateSize() 117 */ 118 public function useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) 119 { 120 $size = $this->fpdiUseImportedPage($pageId, $x, $y, $width, $height, $adjustPageSize); 121 if ($this->inxobj) { 122 $importedPage = $this->importedPages[$pageId]; 123 $this->xobjects[$this->xobjid]['importedPages'][$importedPage['id']] = $pageId; 124 } 125 126 return $size; 127 } 128 129 /** 130 * Get the size of an imported page. 131 * 132 * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the 133 * aspect ratio. 134 * 135 * @param mixed $tpl The template id 136 * @param float|int|null $width The width. 137 * @param float|int|null $height The height. 138 * @return array|bool An array with following keys: width, height, 0 (=width), 1 (=height), orientation (L or P) 139 */ 140 public function getTemplateSize($tpl, $width = null, $height = null) 141 { 142 return $this->getImportedPageSize($tpl, $width, $height); 143 } 144 145 /** 146 * @inheritdoc 147 * @return string 148 */ 149 protected function _getxobjectdict() 150 { 151 $out = parent::_getxobjectdict(); 152 153 foreach ($this->importedPages as $pageData) { 154 $out .= '/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R '; 155 } 156 157 return $out; 158 } 159 160 /** 161 * @inheritdoc 162 * @throws CrossReferenceException 163 * @throws PdfParserException 164 */ 165 protected function _putxobjects() 166 { 167 foreach ($this->importedPages as $key => $pageData) { 168 $this->currentObjectNumber = $this->_newobj(); 169 $this->importedPages[$key]['objectNumber'] = $this->currentObjectNumber; 170 $this->currentReaderId = $pageData['readerId']; 171 $this->writePdfType($pageData['stream']); 172 $this->_put('endobj'); 173 } 174 175 foreach (\array_keys($this->readers) as $readerId) { 176 $parser = $this->getPdfReader($readerId)->getParser(); 177 $this->currentReaderId = $readerId; 178 179 while (($objectNumber = \array_pop($this->objectsToCopy[$readerId])) !== null) { 180 try { 181 $object = $parser->getIndirectObject($objectNumber); 182 } catch (CrossReferenceException $e) { 183 if ($e->getCode() === CrossReferenceException::OBJECT_NOT_FOUND) { 184 $object = PdfIndirectObject::create($objectNumber, 0, new PdfNull()); 185 } else { 186 throw $e; 187 } 188 } 189 190 $this->writePdfType($object); 191 } 192 } 193 194 // let's prepare resources for imported pages in templates 195 foreach ($this->xobjects as $xObjectId => $data) { 196 if (!isset($data['importedPages'])) { 197 continue; 198 } 199 200 foreach ($data['importedPages'] as $id => $pageKey) { 201 $page = $this->importedPages[$pageKey]; 202 $this->xobjects[$xObjectId]['xobjects'][$id] = ['n' => $page['objectNumber']]; 203 } 204 } 205 206 207 parent::_putxobjects(); 208 $this->currentObjectNumber = null; 209 } 210 211 /** 212 * Append content to the buffer of TCPDF. 213 * 214 * @param string $s 215 * @param bool $newLine 216 */ 217 protected function _put($s, $newLine = true) 218 { 219 if ($newLine) { 220 $this->setBuffer($s . "\n"); 221 } else { 222 $this->setBuffer($s); 223 } 224 } 225 226 /** 227 * Begin a new object and return the object number. 228 * 229 * @param int|string $objid Object ID (leave empty to get a new ID). 230 * @return int object number 231 */ 232 protected function _newobj($objid = '') 233 { 234 $this->_out($this->_getobj($objid)); 235 return $this->n; 236 } 237 238 /** 239 * Writes a PdfType object to the resulting buffer. 240 * 241 * @param PdfType $value 242 * @throws PdfTypeException 243 */ 244 protected function writePdfType(PdfType $value) 245 { 246 if (!$this->encrypted) { 247 $this->fpdiWritePdfType($value); 248 return; 249 } 250 251 if ($value instanceof PdfString) { 252 $string = PdfString::unescape($value->value); 253 $string = $this->_encrypt_data($this->currentObjectNumber, $string); 254 $value->value = PdfString::escape($string); 255 } elseif ($value instanceof PdfHexString) { 256 $filter = new AsciiHex(); 257 $string = $filter->decode($value->value); 258 $string = $this->_encrypt_data($this->currentObjectNumber, $string); 259 $value->value = $filter->encode($string, true); 260 } elseif ($value instanceof PdfStream) { 261 $stream = $value->getStream(); 262 $stream = $this->_encrypt_data($this->currentObjectNumber, $stream); 263 $dictionary = $value->value; 264 $dictionary->value['Length'] = PdfNumeric::create(\strlen($stream)); 265 $value = PdfStream::create($dictionary, $stream); 266 } elseif ($value instanceof PdfIndirectObject) { 267 /** 268 * @var PdfIndirectObject $value 269 */ 270 $this->currentObjectNumber = $this->objectMap[$this->currentReaderId][$value->objectNumber]; 271 } 272 273 $this->fpdiWritePdfType($value); 274 } 275 276 /** 277 * This method will add additional data to the last created link/annotation. 278 * 279 * It will copy styling properties (supported by TCPDF) of the imported link. 280 * 281 * @param array $externalLink 282 * @param float|int $xPt 283 * @param float|int $scaleX 284 * @param float|int $yPt 285 * @param float|int $newHeightPt 286 * @param float|int $scaleY 287 * @param array $importedPage 288 * @return void 289 */ 290 protected function adjustLastLink($externalLink, $xPt, $scaleX, $yPt, $newHeightPt, $scaleY, $importedPage) 291 { 292 $parser = $this->getPdfReader($importedPage['readerId'])->getParser(); 293 294 if ($this->inxobj) { 295 // store parameters for later use on template 296 $lastAnnotationKey = count($this->xobjects[$this->xobjid]['annotations']) - 1; 297 $lastAnnotationOpt = &$this->xobjects[$this->xobjid]['annotations'][$lastAnnotationKey]['opt']; 298 } else { 299 $lastAnnotationKey = count($this->PageAnnots[$this->page]) - 1; 300 $lastAnnotationOpt = &$this->PageAnnots[$this->page][$lastAnnotationKey]['opt']; 301 } 302 303 // ensure we have a default value - otherwise TCPDF will set it to 4 throughout 304 $lastAnnotationOpt['f'] = 0; 305 306 // values in this dictonary are all direct objects and we don't need to resolve them here again. 307 $values = $externalLink['pdfObject']->value; 308 309 foreach ($values as $key => $value) { 310 try { 311 switch ($key) { 312 case 'BS': 313 $value = PdfDictionary::ensure($value); 314 $bs = []; 315 if (isset($value->value['W'])) { 316 $bs['w'] = PdfNumeric::ensure($value->value['W'])->value; 317 } 318 319 if (isset($value->value['S'])) { 320 $bs['s'] = PdfName::ensure($value->value['S'])->value; 321 } 322 323 if (isset($value->value['D'])) { 324 $d = []; 325 foreach (PdfArray::ensure($value->value['D'])->value as $item) { 326 $d[] = PdfNumeric::ensure($item)->value; 327 } 328 $bs['d'] = $d; 329 } 330 331 $lastAnnotationOpt['bs'] = $bs; 332 break; 333 334 case 'Border': 335 $borderArray = PdfArray::ensure($value)->value; 336 if (count($borderArray) < 3) { 337 continue 2; 338 } 339 340 $border = [ 341 PdfNumeric::ensure($borderArray[0])->value, 342 PdfNumeric::ensure($borderArray[1])->value, 343 PdfNumeric::ensure($borderArray[2])->value, 344 ]; 345 if (isset($borderArray[3])) { 346 $dashArray = []; 347 foreach (PdfArray::ensure($borderArray[3])->value as $item) { 348 $dashArray[] = PdfNumeric::ensure($item)->value; 349 } 350 $border[] = $dashArray; 351 } 352 353 $lastAnnotationOpt['border'] = $border; 354 break; 355 356 case 'C': 357 $c = []; 358 $colors = PdfArray::ensure(PdfType::resolve($value, $parser))->value; 359 $m = count($colors) === 4 ? 100 : 255; 360 foreach ($colors as $item) { 361 $c[] = PdfNumeric::ensure($item)->value * $m; 362 } 363 $lastAnnotationOpt['c'] = $c; 364 break; 365 366 case 'F': 367 $lastAnnotationOpt['f'] = $value->value; 368 break; 369 370 case 'BE': 371 // is broken in current TCPDF version: "bc" key is checked but "bs" is used. 372 break; 373 } 374 // let's silence invalid/not supported values 375 } catch (FpdiException $e) { 376 continue; 377 } 378 } 379 380 // QuadPoints are not supported by TCPDF 381// if (count($externalLink['quadPoints']) > 0) { 382// $quadPoints = []; 383// for ($i = 0, $n = count($externalLink['quadPoints']); $i < $n; $i += 2) { 384// $quadPoints[] = $xPt + $externalLink['quadPoints'][$i] * $scaleX; 385// $quadPoints[] = $this->hPt - $yPt - $newHeightPt + $externalLink['quadPoints'][$i + 1] * $scaleY; 386// } 387// 388// ????? = $quadPoints; 389// } 390 } 391} 392