1<?php 2 3namespace Mpdf\Image; 4 5use Mpdf\Cache; 6use Mpdf\Color\ColorConverter; 7use Mpdf\Color\ColorModeConverter; 8use Mpdf\CssManager; 9use Mpdf\File\StreamWrapperChecker; 10use Mpdf\Gif\Gif; 11use Mpdf\Language\LanguageToFontInterface; 12use Mpdf\Language\ScriptToLanguageInterface; 13use Mpdf\Log\Context as LogContext; 14use Mpdf\Mpdf; 15use Mpdf\Otl; 16use Mpdf\RemoteContentFetcher; 17use Mpdf\SizeConverter; 18use Psr\Log\LoggerInterface; 19 20class ImageProcessor implements \Psr\Log\LoggerAwareInterface 21{ 22 23 /** 24 * @var \Mpdf\Mpdf 25 */ 26 private $mpdf; 27 28 /** 29 * @var \Mpdf\Otl 30 */ 31 private $otl; 32 33 /** 34 * @var \Mpdf\CssManager 35 */ 36 private $cssManager; 37 38 /** 39 * @var \Mpdf\SizeConverter 40 */ 41 private $sizeConverter; 42 43 /** 44 * @var \Mpdf\Color\ColorConverter 45 */ 46 private $colorConverter; 47 48 /** 49 * @var \Mpdf\Color\ColorModeConverter 50 */ 51 private $colorModeConverter; 52 53 /** 54 * @var \Mpdf\Cache 55 */ 56 private $cache; 57 58 /** 59 * @var \Mpdf\Image\ImageTypeGuesser 60 */ 61 private $guesser; 62 63 /** 64 * @var string[] 65 */ 66 private $failedImages; 67 68 /** 69 * @var \Mpdf\Image\Bmp 70 */ 71 private $bmp; 72 73 /** 74 * @var \Mpdf\Image\Wmf 75 */ 76 private $wmf; 77 78 /** 79 * @var \Mpdf\Language\LanguageToFontInterface 80 */ 81 private $languageToFont; 82 83 /** 84 * @var \Mpdf\Language\ScriptToLanguageInterface 85 */ 86 public $scriptToLanguage; 87 88 /** 89 * @var \Mpdf\RemoteContentFetcher 90 */ 91 private $remoteContentFetcher; 92 93 /** 94 * @var \Psr\Log\LoggerInterface 95 */ 96 public $logger; 97 98 public function __construct( 99 Mpdf $mpdf, 100 Otl $otl, 101 CssManager $cssManager, 102 SizeConverter $sizeConverter, 103 ColorConverter $colorConverter, 104 ColorModeConverter $colorModeConverter, 105 Cache $cache, 106 LanguageToFontInterface $languageToFont, 107 ScriptToLanguageInterface $scriptToLanguage, 108 RemoteContentFetcher $remoteContentFetcher, 109 LoggerInterface $logger 110 ) { 111 112 $this->mpdf = $mpdf; 113 $this->otl = $otl; 114 $this->cssManager = $cssManager; 115 $this->sizeConverter = $sizeConverter; 116 $this->colorConverter = $colorConverter; 117 $this->colorModeConverter = $colorModeConverter; 118 $this->cache = $cache; 119 $this->languageToFont = $languageToFont; 120 $this->scriptToLanguage = $scriptToLanguage; 121 $this->remoteContentFetcher = $remoteContentFetcher; 122 123 $this->logger = $logger; 124 125 $this->guesser = new ImageTypeGuesser(); 126 127 $this->failedImages = []; 128 } 129 130 /** 131 * @param \Psr\Log\LoggerInterface 132 * 133 * @return self 134 */ 135 public function setLogger(LoggerInterface $logger) 136 { 137 $this->logger = $logger; 138 139 return $this; 140 } 141 142 public function getImage(&$file, $firsttime = true, $allowvector = true, $orig_srcpath = false, $interpolation = false) 143 { 144 /** 145 * Prevents insecure PHP object injection through phar:// wrapper 146 * @see https://github.com/mpdf/mpdf/issues/949 147 * @see https://github.com/mpdf/mpdf/issues/1381 148 */ 149 $wrapperChecker = new StreamWrapperChecker($this->mpdf); 150 if ($wrapperChecker->hasBlacklistedStreamWrapper($file)) { 151 return $this->imageError($file, $firsttime, 'File contains an invalid stream. Only ' . implode(', ', $wrapperChecker->getWhitelistedStreamWrappers()) . ' streams are allowed.'); 152 } 153 if ($wrapperChecker->hasBlacklistedStreamWrapper($orig_srcpath)) { 154 return $this->imageError($orig_srcpath, $firsttime, 'File contains an invalid stream. Only ' . implode(', ', $wrapperChecker->getWhitelistedStreamWrappers()) . ' streams are allowed.'); 155 } 156 157 // mPDF 6 158 // firsttime i.e. whether to add to this->images - use false when calling iteratively 159 // Image Data passed directly as var:varname 160 161 $type = null; 162 $data = ''; 163 164 if (preg_match('/var:\s*(.*)/', $file, $v)) { 165 if (!isset($this->mpdf->imageVars[$v[1]])) { 166 return $this->imageError($file, $firsttime, 'Unknown image variable'); 167 } 168 $data = $this->mpdf->imageVars[$v[1]]; 169 $file = md5($data); 170 } 171 172 if (preg_match('/data:image\/(gif|jpe?g|png|webp);base64,(.*)/', $file, $v)) { 173 $type = $v[1]; 174 $data = base64_decode($v[2]); 175 $file = md5($data); 176 } 177 178 // mPDF 5.7.4 URLs 179 if ($firsttime && $file && strpos($file, 'data:') !== 0) { 180 $file = str_replace(' ', '%20', $file); 181 } 182 if ($firsttime && $orig_srcpath) { 183 // If orig_srcpath is a relative file path (and not a URL), then it needs to be URL decoded 184 if (strpos($orig_srcpath, 'data:') !== 0) { 185 $orig_srcpath = str_replace(' ', '%20', $orig_srcpath); 186 } 187 if (!preg_match('/^(http|ftp)/', $orig_srcpath)) { 188 $orig_srcpath = $this->urldecodeParts($orig_srcpath); 189 } 190 } 191 192 $ppUx = 0; 193 if ($orig_srcpath && isset($this->mpdf->images[$orig_srcpath])) { 194 $file = $orig_srcpath; 195 return $this->mpdf->images[$orig_srcpath]; 196 } 197 198 if (isset($this->mpdf->images[$file])) { 199 return $this->mpdf->images[$file]; 200 } 201 202 if ($orig_srcpath && isset($this->mpdf->formobjects[$orig_srcpath])) { 203 $file = $orig_srcpath; 204 return $this->mpdf->formobjects[$file]; 205 } 206 207 if (isset($this->mpdf->formobjects[$file])) { 208 return $this->mpdf->formobjects[$file]; 209 } 210 211 if ($firsttime && isset($this->failedImages[$file])) { // Save re-trying image URL's which have already failed 212 return $this->imageError($file, $firsttime, ''); 213 } 214 215 if (empty($data)) { 216 217 $data = ''; 218 219 if ($orig_srcpath && $this->mpdf->basepathIsLocal && $check = @fopen($orig_srcpath, 'rb')) { 220 fclose($check); 221 $file = $orig_srcpath; 222 $this->logger->debug(sprintf('Fetching (file_get_contents) content of file "%s" with local basepath', $file), ['context' => LogContext::REMOTE_CONTENT]); 223 $data = file_get_contents($file); 224 $type = $this->guesser->guess($data); 225 } 226 227 if ($file && !$data && $check = @fopen($file, 'rb')) { 228 fclose($check); 229 $this->logger->debug(sprintf('Fetching (file_get_contents) content of file "%s" with non-local basepath', $file), ['context' => LogContext::REMOTE_CONTENT]); 230 $data = file_get_contents($file); 231 $type = $this->guesser->guess($data); 232 } 233 234 if ((!$data || !$type) && function_exists('curl_init')) { // mPDF 5.7.4 235 $data = $this->remoteContentFetcher->getFileContentsByCurl($file); // needs full url?? even on local (never needed for local) 236 if ($data) { 237 $type = $this->guesser->guess($data); 238 } 239 } 240 241 if ((!$data || !$type) && !ini_get('allow_url_fopen')) { // only worth trying if remote file and !ini_get('allow_url_fopen') 242 $data = $this->remoteContentFetcher->getFileContentsBySocket($file); // needs full url?? even on local (never needed for local) 243 if ($data) { 244 $type = $this->guesser->guess($data); 245 } 246 } 247 } 248 249 if (!$data) { 250 return $this->imageError($file, $firsttime, 'Could not find image file'); 251 } 252 253 if ($type === null) { 254 $type = $this->guesser->guess($data); 255 } 256 257 if (($type === 'wmf' || $type === 'svg') && !$allowvector) { 258 return $this->imageError($file, $firsttime, 'WMF or SVG image file not supported in this context'); 259 } 260 261 // SVG 262 if ($type === 'svg') { 263 $svg = new Svg($this->mpdf, $this->otl, $this->cssManager, $this, $this->sizeConverter, $this->colorConverter, $this->languageToFont, $this->scriptToLanguage); 264 $family = $this->mpdf->FontFamily; 265 $style = $this->mpdf->FontStyle; 266 $size = $this->mpdf->FontSizePt; 267 $info = $svg->ImageSVG($data); 268 // Restore font 269 if ($family) { 270 $this->mpdf->SetFont($family, $style, $size, false); 271 } 272 if (!$info) { 273 return $this->imageError($file, $firsttime, 'Error parsing SVG file'); 274 } 275 $info['type'] = 'svg'; 276 $info['i'] = count($this->mpdf->formobjects) + 1; 277 $this->mpdf->formobjects[$file] = $info; 278 279 return $info; 280 } 281 282 if ($type === 'webp') { // Convert webp images to JPG and treat them as such 283 284 $im = @imagecreatefromstring($data); 285 286 if (!function_exists('imagewebp') || false === $im) { 287 return $this->imageError($file, $firsttime, 'Missing GD support for WEBP images.'); 288 } 289 290 $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.jpg'); 291 $checkfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.jpg'); 292 $check = @imagewebp($im, $checkfile); 293 294 if (!$check) { 295 return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') when using GD library to parse WEBP image'); 296 } 297 298 @imagejpeg($im, $tempfile); 299 $data = file_get_contents($tempfile); 300 imagedestroy($im); 301 unlink($tempfile); 302 unlink($checkfile); 303 $type = 'jpeg'; 304 305 } 306 307 // JPEG 308 if ($type === 'jpeg' || $type === 'jpg') { 309 310 $hdr = $this->jpgHeaderFromString($data); 311 if (!$hdr) { 312 return $this->imageError($file, $firsttime, 'Error parsing JPG header'); 313 } 314 315 $a = $this->jpgDataFromHeader($hdr); 316 $channels = (int) $a[4]; 317 $j = strpos($data, 'JFIF'); 318 319 if ($j) { 320 // Read resolution 321 $unitSp = ord(substr($data, $j + 7, 1)); 322 if ($unitSp > 0) { 323 $ppUx = $this->twoBytesToInt(substr($data, $j + 8, 2)); // horizontal pixels per meter, usually set to zero 324 if ($unitSp === 2) { // = dots per cm (if == 1 set as dpi) 325 $ppUx = round($ppUx / 10 * 25.4); 326 } 327 } 328 } 329 330 if ($a[2] === 'DeviceCMYK' && ($this->mpdf->restrictColorSpace === 2 || ($this->mpdf->PDFA && $this->mpdf->restrictColorSpace !== 3))) { 331 332 // convert to RGB image 333 if (!function_exists('gd_info')) { 334 throw new \Mpdf\MpdfException(sprintf('JPG image may not use CMYK color space (%s).', $file)); 335 } 336 337 if ($this->mpdf->PDFA && !$this->mpdf->PDFAauto) { 338 $this->mpdf->PDFAXwarnings[] = sprintf('JPG image may not use CMYK color space - %s - (Image converted to RGB. NB This will alter the colour profile of the image.)', $file); 339 } 340 341 $im = @imagecreatefromstring($data); 342 343 if ($im) { 344 $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); 345 imageinterlace($im, false); 346 $check = @imagepng($im, $tempfile); 347 if (!$check) { 348 return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') when using GD library to parse JPG(CMYK) image'); 349 } 350 $info = $this->getImage($tempfile, false); 351 if (!$info) { 352 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse JPG(CMYK) image'); 353 } 354 imagedestroy($im); 355 unlink($tempfile); 356 $info['type'] = 'jpg'; 357 if ($firsttime) { 358 $info['i'] = count($this->mpdf->images) + 1; 359 $info['interpolation'] = $interpolation; // mPDF 6 360 $this->mpdf->images[$file] = $info; 361 } 362 return $info; 363 } 364 365 return $this->imageError($file, $firsttime, 'Error creating GD image file from JPG(CMYK) image'); 366 367 } 368 369 if ($a[2] === 'DeviceRGB' && ($this->mpdf->PDFX || $this->mpdf->restrictColorSpace === 3)) { 370 // Convert to CMYK image stream - nominally returned as type='png' 371 $info = $this->convertImage($data, $a[2], 'DeviceCMYK', $a[0], $a[1], $ppUx, false); 372 if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { 373 $this->mpdf->PDFAXwarnings[] = sprintf('JPG image may not use RGB color space - %s - (Image converted to CMYK. NB This will alter the colour profile of the image.)', $file); 374 } 375 376 } elseif (($a[2] === 'DeviceRGB' || $a[2] === 'DeviceCMYK') && $this->mpdf->restrictColorSpace === 1) { 377 // Convert to Grayscale image stream - nominally returned as type='png' 378 $info = $this->convertImage($data, $a[2], 'DeviceGray', $a[0], $a[1], $ppUx, false); 379 380 } else { 381 // mPDF 6 Detect Adobe APP14 Tag 382 //$pos = strpos($data, "\xFF\xEE\x00\x0EAdobe\0"); 383 //if ($pos !== false) { 384 //} 385 // mPDF 6 ICC profile 386 $offset = 0; 387 $icc = []; 388 while (($pos = strpos($data, "ICC_PROFILE\0", $offset)) !== false) { 389 // get ICC sequence length 390 $length = $this->twoBytesToInt(substr($data, $pos - 2, 2)) - 16; 391 $sn = max(1, ord($data[$pos + 12])); 392 $nom = max(1, ord($data[$pos + 13])); 393 $icc[$sn - 1] = substr($data, $pos + 14, $length); 394 $offset = ($pos + 14 + $length); 395 } 396 // order and compact ICC segments 397 if (count($icc) > 0) { 398 ksort($icc); 399 $icc = implode('', $icc); 400 if (substr($icc, 36, 4) !== 'acsp') { 401 // invalid ICC profile 402 $icc = false; 403 } 404 $input = substr($icc, 16, 4); 405 $output = substr($icc, 20, 4); 406 // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab 407 if ($input !== 'RGB ' || $output !== 'XYZ ') { 408 $icc = false; 409 } 410 } else { 411 $icc = false; 412 } 413 414 $info = ['w' => $a[0], 'h' => $a[1], 'cs' => $a[2], 'bpc' => $a[3], 'f' => 'DCTDecode', 'data' => $data, 'type' => 'jpg', 'ch' => $channels, 'icc' => $icc]; 415 if ($ppUx) { 416 $info['set-dpi'] = $ppUx; 417 } 418 } 419 420 if (!$info) { 421 return $this->imageError($file, $firsttime, 'Error parsing or converting JPG image'); 422 } 423 424 if ($firsttime) { 425 $info['i'] = count($this->mpdf->images) + 1; 426 $info['interpolation'] = $interpolation; // mPDF 6 427 $this->mpdf->images[$file] = $info; 428 } 429 430 return $info; 431 432 } 433 434 if ($type === 'png') { 435 436 // Check signature 437 if (strpos($data, chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) !== 0) { 438 return $this->imageError($file, $firsttime, 'Error parsing PNG identifier'); 439 } 440 441 // Read header chunk 442 if (substr($data, 12, 4) !== 'IHDR') { 443 return $this->imageError($file, $firsttime, 'Incorrect PNG file (no IHDR block found)'); 444 } 445 446 $w = $this->fourBytesToInt(substr($data, 16, 4)); 447 $h = $this->fourBytesToInt(substr($data, 20, 4)); 448 $bpc = ord(substr($data, 24, 1)); 449 $errpng = false; 450 $pngalpha = false; 451 $channels = 0; 452 453 // if($bpc>8) { $errpng = 'not 8-bit depth'; } // mPDF 6 Allow through to be handled as native PNG 454 $ct = ord(substr($data, 25, 1)); 455 456 if ($ct === 0) { 457 $colspace = 'DeviceGray'; 458 $channels = 1; 459 } elseif ($ct === 2) { 460 $colspace = 'DeviceRGB'; 461 $channels = 3; 462 } elseif ($ct === 3) { 463 $colspace = 'Indexed'; 464 $channels = 1; 465 } elseif ($ct === 4) { 466 $colspace = 'DeviceGray'; 467 $channels = 1; 468 $errpng = 'alpha channel'; 469 $pngalpha = true; 470 } else { 471 $colspace = 'DeviceRGB'; 472 $channels = 3; 473 $errpng = 'alpha channel'; 474 $pngalpha = true; 475 } 476 477 if ($ct < 4 && strpos($data, 'tRNS') !== false) { 478 $errpng = 'transparency'; 479 $pngalpha = true; 480 } // mPDF 6 481 482 if ($ct === 3 && strpos($data, 'iCCP') !== false) { 483 $errpng = 'indexed plus ICC'; 484 } // mPDF 6 485 486 // $pngalpha is used as a FLAG of any kind of transparency which COULD be tranferred to an alpha channel 487 // incl. single-color tarnsparency, depending which type of handling occurs later 488 if (ord(substr($data, 26, 1)) !== 0) { 489 $errpng = 'compression method'; 490 } // only 0 should be specified 491 492 if (ord(substr($data, 27, 1)) !== 0) { 493 $errpng = 'filter method'; 494 } // only 0 should be specified 495 496 if (ord(substr($data, 28, 1)) !== 0) { 497 $errpng = 'interlaced file'; 498 } 499 500 $j = strpos($data, 'pHYs'); 501 if ($j) { 502 //Read resolution 503 $unitSp = ord(substr($data, $j + 12, 1)); 504 if ($unitSp === 1) { 505 $ppUx = $this->fourBytesToInt(substr($data, $j + 4, 4)); // horizontal pixels per meter, usually set to zero 506 $ppUx = round($ppUx / 1000 * 25.4); 507 } 508 } 509 510 // mPDF 6 Gamma correction 511 $gamma = 0; 512 $gAMA = 0; 513 $j = strpos($data, 'gAMA'); 514 if ($j && strpos($data, 'sRGB') === false) { // sRGB colorspace - overrides gAMA 515 $gAMA = $this->fourBytesToInt(substr($data, $j + 4, 4)); // Gamma value times 100000 516 $gAMA /= 100000; 517 518 // http://www.libpng.org/pub/png/spec/1.2/PNG-Encoders.html 519 // "If the source file's gamma value is greater than 1.0, it is probably a display system exponent,..." 520 // ("..and you should use its reciprocal for the PNG gamma.") 521 //if ($gAMA > 1) { $gAMA = 1/$gAMA; } 522 // (Some) Applications seem to ignore it... appearing how it was probably intended 523 // Test Case - image(s) on http://www.w3.org/TR/CSS21/intro.html - PNG has gAMA set as 1.45454 524 // Probably unintentional as mentioned above and should be 0.45454 which is 1 / 2.2 525 // Tested on Windows PC 526 // Firefox and Opera display gray as 234 (correct, but looks wrong) 527 // IE9 and Safari display gray as 193 (incorrect but looks right) 528 // See test different gamma chunks at http://www.libpng.org/pub/png/pngsuite-all-good.html 529 } 530 531 if ($gAMA) { 532 $gamma = 1 / $gAMA; 533 } 534 535 // Don't need to apply gamma correction if == default i.e. 2.2 536 if ($gamma > 2.15 && $gamma < 2.25) { 537 $gamma = 0; 538 } 539 540 // NOT supported at present 541 //$j = strpos($data,'sRGB'); // sRGB colorspace - overrides gAMA 542 //$j = strpos($data,'cHRM'); // Chromaticity and Whitepoint 543 // $firsttime added mPDF 6 so when PNG Grayscale with alpha using resrtictcolorspace to CMYK 544 // the alpha channel is sent through as secondtime as Indexed and should not be converted to CMYK 545 if ($firsttime && ($colspace === 'DeviceRGB' || $colspace === 'Indexed') && ($this->mpdf->PDFX || $this->mpdf->restrictColorSpace === 3)) { 546 547 // Convert to CMYK image stream - nominally returned as type='png' 548 $info = $this->convertImage($data, $colspace, 'DeviceCMYK', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction 549 if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { 550 $this->mpdf->PDFAXwarnings[] = sprintf('PNG image may not use RGB color space - %s - (Image converted to CMYK. NB This will alter the colour profile of the image.)', $file); 551 } 552 553 } elseif ($firsttime && ($colspace === 'DeviceRGB' || $colspace === 'Indexed') && $this->mpdf->restrictColorSpace === 1) { 554 555 // $firsttime added mPDF 6 so when PNG Grayscale with alpha using resrtictcolorspace to CMYK 556 // the alpha channel is sent through as secondtime as Indexed and should not be converted to CMYK 557 // Convert to Grayscale image stream - nominally returned as type='png' 558 $info = $this->convertImage($data, $colspace, 'DeviceGray', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction 559 560 } elseif (($this->mpdf->PDFA || $this->mpdf->PDFX) && $pngalpha) { 561 562 // Remove alpha channel 563 if ($this->mpdf->restrictColorSpace === 1) { // Grayscale 564 $info = $this->convertImage($data, $colspace, 'DeviceGray', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction 565 } elseif ($this->mpdf->restrictColorSpace === 3) { // CMYK 566 $info = $this->convertImage($data, $colspace, 'DeviceCMYK', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction 567 } elseif ($this->mpdf->PDFA) { // RGB 568 $info = $this->convertImage($data, $colspace, 'DeviceRGB', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction 569 } 570 if (($this->mpdf->PDFA && !$this->mpdf->PDFAauto) || ($this->mpdf->PDFX && !$this->mpdf->PDFXauto)) { 571 $this->mpdf->PDFAXwarnings[] = sprintf('Transparency (alpha channel) not permitted in PDFA or PDFX files - %s - (Image converted to one without transparency.)', $file); 572 } 573 574 } elseif ($firsttime && ($errpng || $pngalpha || $gamma)) { // mPDF 5.7.2 Gamma correction 575 576 $gd = function_exists('gd_info') ? gd_info() : []; 577 if (!isset($gd['PNG Support'])) { 578 return $this->imageError($file, $firsttime, sprintf('GD library with PNG support required for image (%s)', $errpng)); 579 } 580 581 $im = imagecreatefromstring($data); 582 if (!$im) { 583 return $this->imageError($file, $firsttime, sprintf('Error creating GD image from PNG file (%s)', $errpng)); 584 } 585 586 $w = imagesx($im); 587 $h = imagesy($im); 588 589 $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); 590 591 // Alpha channel set (including using tRNS for Paletted images) 592 if ($pngalpha) { 593 if ($this->mpdf->PDFA) { 594 throw new \Mpdf\MpdfException(sprintf('PDFA1-b does not permit images with alpha channel transparency (%s).', $file)); 595 } 596 597 $imgalpha = imagecreate($w, $h); 598 // generate gray scale pallete 599 for ($c = 0; $c < 256; ++$c) { 600 imagecolorallocate($imgalpha, $c, $c, $c); 601 } 602 603 // mPDF 6 604 if ($colspace === 'Indexed') { // generate Alpha channel values from tRNS 605 // Read transparency info 606 $p = strpos($data, 'tRNS'); 607 if ($p) { 608 $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); 609 $transparency = substr($data, $p + 4, $n); 610 // ord($transparency[$index]) = the alpha value for that index 611 // generate alpha channel 612 for ($ypx = 0; $ypx < $h; ++$ypx) { 613 for ($xpx = 0; $xpx < $w; ++$xpx) { 614 $colorindex = imagecolorat($im, $xpx, $ypx); 615 if ($colorindex >= $n) { 616 $alpha = 255; 617 } else { 618 $alpha = ord($transparency[$colorindex]); 619 } // 0-255 620 if ($alpha > 0) { 621 imagesetpixel($imgalpha, $xpx, $ypx, $alpha); 622 } 623 } 624 } 625 } 626 } elseif ($ct === 0 || $ct === 2) { // generate Alpha channel values from tRNS 627 // Get transparency as array of RGB 628 $p = strpos($data, 'tRNS'); 629 if ($p) { 630 $trns = ''; 631 $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); 632 $t = substr($data, $p + 4, $n); 633 if ($colspace === 'DeviceGray') { // ct===0 634 $trns = [$this->translateValue(substr($t, 0, 2), $bpc)]; 635 } else /* $colspace=='DeviceRGB' */ { // ct==2 636 $trns = []; 637 $trns[0] = $this->translateValue(substr($t, 0, 2), $bpc); 638 $trns[1] = $this->translateValue(substr($t, 2, 2), $bpc); 639 $trns[2] = $this->translateValue(substr($t, 4, 2), $bpc); 640 } 641 642 // generate alpha channel 643 for ($ypx = 0; $ypx < $h; ++$ypx) { 644 for ($xpx = 0; $xpx < $w; ++$xpx) { 645 $rgb = imagecolorat($im, $xpx, $ypx); 646 $r = ($rgb >> 16) & 0xFF; 647 $g = ($rgb >> 8) & 0xFF; 648 $b = $rgb & 0xFF; 649 if ($colspace === 'DeviceGray' && $b == $trns[0]) { 650 $alpha = 0; 651 } elseif ($r == $trns[0] && $g == $trns[1] && $b == $trns[2]) { 652 $alpha = 0; 653 } else { // ct==2 654 $alpha = 255; 655 } 656 if ($alpha > 0) { 657 imagesetpixel($imgalpha, $xpx, $ypx, $alpha); 658 } 659 } 660 } 661 } 662 } else { 663 // extract alpha channel 664 for ($ypx = 0; $ypx < $h; ++$ypx) { 665 for ($xpx = 0; $xpx < $w; ++$xpx) { 666 $alpha = (imagecolorat($im, $xpx, $ypx) & 0x7F000000) >> 24; 667 if ($alpha < 127) { 668 imagesetpixel($imgalpha, $xpx, $ypx, 255 - ($alpha * 2)); 669 } 670 } 671 } 672 } 673 674 // NB This must happen after the Alpha channel is extracted 675 // imagegammacorrect() removes the alpha channel data in $im - (I think this is a bug in PHP) 676 if ($gamma) { 677 imagegammacorrect($im, $gamma, 2.2); 678 } 679 680 $tempfile_alpha = $this->cache->tempFilename('_tempMskPNG' . md5($file) . random_int(1, 10000) . '.png'); 681 682 $check = @imagepng($imgalpha, $tempfile_alpha); 683 684 if (!$check) { 685 return $this->imageError($file, $firsttime, 'Failed to create temporary image file (' . $tempfile_alpha . ') parsing PNG image with alpha channel (' . $errpng . ')'); 686 } 687 688 imagedestroy($imgalpha); 689 // extract image without alpha channel 690 $imgplain = imagecreatetruecolor($w, $h); 691 imagealphablending($imgplain, false); // mPDF 5.7.2 692 imagecopy($imgplain, $im, 0, 0, 0, 0, $w, $h); 693 694 // create temp image file 695 $check = @imagepng($imgplain, $tempfile); 696 if (!$check) { 697 return $this->imageError($file, $firsttime, 'Failed to create temporary image file (' . $tempfile . ') parsing PNG image with alpha channel (' . $errpng . ')'); 698 } 699 imagedestroy($imgplain); 700 // embed mask image 701 $minfo = $this->getImage($tempfile_alpha, false); 702 unlink($tempfile_alpha); 703 704 if (!$minfo) { 705 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile_alpha . ') created with GD library to parse PNG image'); 706 } 707 708 $imgmask = count($this->mpdf->images) + 1; 709 $minfo['cs'] = 'DeviceGray'; 710 $minfo['i'] = $imgmask; 711 $this->mpdf->images[$tempfile_alpha] = $minfo; 712 // embed image, masked with previously embedded mask 713 $info = $this->getImage($tempfile, false); 714 unlink($tempfile); 715 716 if (!$info) { 717 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse PNG image'); 718 } 719 720 $info['masked'] = $imgmask; 721 if ($ppUx) { 722 $info['set-dpi'] = $ppUx; 723 } 724 $info['type'] = 'png'; 725 if ($firsttime) { 726 $info['i'] = count($this->mpdf->images) + 1; 727 $info['interpolation'] = $interpolation; // mPDF 6 728 $this->mpdf->images[$file] = $info; 729 } 730 731 return $info; 732 } 733 734 // No alpha/transparency set (but cannot read directly because e.g. bit-depth != 8, interlaced etc) 735 // ICC profile 736 $icc = false; 737 $p = strpos($data, 'iCCP'); 738 if ($p && $colspace === "Indexed") { // Cannot have ICC profile and Indexed together 739 $p += 4; 740 $n = $this->fourBytesToInt(substr($data, ($p - 8), 4)); 741 $nullsep = strpos(substr($data, $p, 80), chr(0)); 742 $icc = substr($data, ($p + $nullsep + 2), ($n - ($nullsep + 2))); 743 $icc = @gzuncompress($icc); // Ignored if fails 744 if ($icc) { 745 if (substr($icc, 36, 4) !== 'acsp') { 746 $icc = false; 747 } // invalid ICC profile 748 else { 749 $input = substr($icc, 16, 4); 750 $output = substr($icc, 20, 4); 751 // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab 752 if ($input !== 'RGB ' || $output !== 'XYZ ') { 753 $icc = false; 754 } 755 } 756 } 757 // Convert to RGB colorspace so can use ICC Profile 758 if ($icc) { 759 imagepalettetotruecolor($im); 760 $colspace = 'DeviceRGB'; 761 $channels = 3; 762 } 763 } 764 765 if ($gamma) { 766 imagegammacorrect($im, $gamma, 2.2); 767 } 768 769 imagealphablending($im, false); 770 imagesavealpha($im, false); 771 imageinterlace($im, false); 772 773 $check = @imagepng($im, $tempfile); 774 if (!$check) { 775 return $this->imageError($file, $firsttime, 'Failed to create temporary image file (' . $tempfile . ') parsing PNG image (' . $errpng . ')'); 776 } 777 imagedestroy($im); 778 $info = $this->getImage($tempfile, false); 779 unlink($tempfile); 780 if (!$info) { 781 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse PNG image'); 782 } 783 784 if ($ppUx) { 785 $info['set-dpi'] = $ppUx; 786 } 787 $info['type'] = 'png'; 788 if ($firsttime) { 789 $info['i'] = count($this->mpdf->images) + 1; 790 $info['interpolation'] = $interpolation; // mPDF 6 791 if ($icc) { 792 $info['ch'] = $channels; 793 $info['icc'] = $icc; 794 } 795 $this->mpdf->images[$file] = $info; 796 } 797 798 return $info; 799 800 } else { // PNG image with no need to convert alph channels, bpc <> 8 etc. 801 802 $parms = '/DecodeParms <</Predictor 15 /Colors ' . $channels . ' /BitsPerComponent ' . $bpc . ' /Columns ' . $w . '>>'; 803 //Scan chunks looking for palette, transparency and image data 804 $pal = ''; 805 $trns = ''; 806 $pngdata = ''; 807 $icc = false; 808 $p = 33; 809 810 do { 811 $n = $this->fourBytesToInt(substr($data, $p, 4)); 812 $p += 4; 813 $type = substr($data, $p, 4); 814 $p += 4; 815 if ($type === 'PLTE') { 816 //Read palette 817 $pal = substr($data, $p, $n); 818 $p += $n; 819 $p += 4; 820 } elseif ($type === 'tRNS') { 821 //Read transparency info 822 $t = substr($data, $p, $n); 823 $p += $n; 824 if ($ct === 0) { 825 $trns = [ord(substr($t, 1, 1))]; 826 } elseif ($ct === 2) { 827 $trns = [ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1))]; 828 } else { 829 $pos = strpos($t, chr(0)); 830 if (is_int($pos)) { 831 $trns = [$pos]; 832 } 833 } 834 $p += 4; 835 } elseif ($type === 'IDAT') { 836 $pngdata.=substr($data, $p, $n); 837 $p += $n; 838 $p += 4; 839 } elseif ($type === 'iCCP') { 840 $nullsep = strpos(substr($data, $p, 80), chr(0)); 841 $icc = substr($data, $p + $nullsep + 2, $n - ($nullsep + 2)); 842 $icc = @gzuncompress($icc); // Ignored if fails 843 if ($icc) { 844 if (substr($icc, 36, 4) !== 'acsp') { 845 $icc = false; 846 } // invalid ICC profile 847 else { 848 $input = substr($icc, 16, 4); 849 $output = substr($icc, 20, 4); 850 // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab 851 if ($input !== 'RGB ' || $output !== 'XYZ ') { 852 $icc = false; 853 } 854 } 855 } 856 $p += $n; 857 $p += 4; 858 } elseif ($type === 'IEND') { 859 break; 860 } elseif (preg_match('/[a-zA-Z]{4}/', $type)) { 861 $p += $n + 4; 862 } else { 863 return $this->imageError($file, $firsttime, 'Error parsing PNG image data'); 864 } 865 866 } while ($n); 867 868 if (!$pngdata) { 869 return $this->imageError($file, $firsttime, 'Error parsing PNG image data - no IDAT data found'); 870 } 871 872 if ($colspace === 'Indexed' && empty($pal)) { 873 return $this->imageError($file, $firsttime, 'Error parsing PNG image data - missing colour palette'); 874 } 875 876 if ($colspace === 'Indexed' && $icc) { 877 $icc = false; 878 } // mPDF 6 cannot have ICC profile and Indexed in a PDF document as both use the colorspace tag. 879 880 $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'FlateDecode', 'parms' => $parms, 'pal' => $pal, 'trns' => $trns, 'data' => $pngdata, 'ch' => $channels, 'icc' => $icc]; 881 $info['type'] = 'png'; 882 if ($ppUx) { 883 $info['set-dpi'] = $ppUx; 884 } 885 } 886 887 if (!$info) { 888 return $this->imageError($file, $firsttime, 'Error parsing or converting PNG image'); 889 } 890 891 if ($firsttime) { 892 $info['i'] = count($this->mpdf->images) + 1; 893 $info['interpolation'] = $interpolation; // mPDF 6 894 $this->mpdf->images[$file] = $info; 895 } 896 897 return $info; 898 899 } elseif ($type === 'gif') { // GIF 900 901 $gd = function_exists('gd_info') 902 ? gd_info() 903 : []; 904 905 if (isset($gd['GIF Read Support']) && $gd['GIF Read Support']) { 906 907 $im = @imagecreatefromstring($data); 908 if ($im) { 909 $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); 910 imagealphablending($im, false); 911 imagesavealpha($im, false); 912 imageinterlace($im, false); 913 if (!is_writable($tempfile)) { 914 ob_start(); 915 $check = @imagepng($im); 916 if (!$check) { 917 return $this->imageError($file, $firsttime, 'Error creating temporary image object when using GD library to parse GIF image'); 918 } 919 $this->mpdf->imageVars['tempImage'] = ob_get_contents(); 920 $tempimglnk = 'var:tempImage'; 921 ob_end_clean(); 922 $info = $this->getImage($tempimglnk, false); 923 if (!$info) { 924 return $this->imageError($file, $firsttime, 'Error parsing temporary file image object created with GD library to parse GIF image'); 925 } 926 imagedestroy($im); 927 } else { 928 $check = @imagepng($im, $tempfile); 929 if (!$check) { 930 return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') when using GD library to parse GIF image'); 931 } 932 $info = $this->getImage($tempfile, false); 933 if (!$info) { 934 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse GIF image'); 935 } 936 imagedestroy($im); 937 unlink($tempfile); 938 } 939 $info['type'] = 'gif'; 940 if ($firsttime) { 941 $info['i'] = count($this->mpdf->images) + 1; 942 $info['interpolation'] = $interpolation; // mPDF 6 943 $this->mpdf->images[$file] = $info; 944 } 945 return $info; 946 } 947 948 return $this->imageError($file, $firsttime, 'Error creating GD image file from GIF image'); 949 } 950 951 $gif = new Gif(); 952 953 $h = 0; 954 $w = 0; 955 956 $gif->loadFile($data, 0); 957 958 $nColors = 0; 959 $bgColor = -1; 960 $colspace = 'DeviceGray'; 961 $pal = ''; 962 963 if (isset($gif->m_img->m_gih->m_bLocalClr) && $gif->m_img->m_gih->m_bLocalClr) { 964 $nColors = $gif->m_img->m_gih->m_nTableSize; 965 $pal = $gif->m_img->m_gih->m_colorTable->toString(); 966 if ((isset($bgColor)) && $bgColor !== -1) { // mPDF 5.7.3 967 $bgColor = $gif->m_img->m_gih->m_colorTable->colorIndex($bgColor); 968 } 969 $colspace = 'Indexed'; 970 } elseif (isset($gif->m_gfh->m_bGlobalClr) && $gif->m_gfh->m_bGlobalClr) { 971 $nColors = $gif->m_gfh->m_nTableSize; 972 $pal = $gif->m_gfh->m_colorTable->toString(); 973 if ((isset($bgColor)) && $bgColor != -1) { 974 $bgColor = $gif->m_gfh->m_colorTable->colorIndex($bgColor); 975 } 976 $colspace = 'Indexed'; 977 } 978 979 $trns = ''; 980 981 if (isset($gif->m_img->m_bTrans) && $gif->m_img->m_bTrans && ($nColors > 0)) { 982 $trns = [$gif->m_img->m_nTrans]; 983 } 984 985 $gifdata = $gif->m_img->m_data; 986 $w = $gif->m_gfh->m_nWidth; 987 $h = $gif->m_gfh->m_nHeight; 988 $gif->ClearData(); 989 990 if ($colspace === 'Indexed' && empty($pal)) { 991 return $this->imageError($file, $firsttime, 'Error parsing GIF image - missing colour palette'); 992 } 993 994 if ($this->mpdf->compress) { 995 $gifdata = $this->gzCompress($gifdata); 996 $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => 8, 'f' => 'FlateDecode', 'pal' => $pal, 'trns' => $trns, 'data' => $gifdata]; 997 } else { 998 $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => 8, 'pal' => $pal, 'trns' => $trns, 'data' => $gifdata]; 999 } 1000 1001 $info['type'] = 'gif'; 1002 if ($firsttime) { 1003 $info['i'] = count($this->mpdf->images) + 1; 1004 $info['interpolation'] = $interpolation; // mPDF 6 1005 $this->mpdf->images[$file] = $info; 1006 } 1007 1008 return $info; 1009 1010 } elseif ($type === 'bmp') { 1011 1012 if ($this->bmp === null) { 1013 $this->bmp = new Bmp($this->mpdf); 1014 } 1015 1016 $info = $this->bmp->_getBMPimage($data, $file); 1017 if (isset($info['error'])) { 1018 return $this->imageError($file, $firsttime, $info['error']); 1019 } 1020 1021 if ($firsttime) { 1022 $info['i'] = count($this->mpdf->images) + 1; 1023 $info['interpolation'] = $interpolation; // mPDF 6 1024 $this->mpdf->images[$file] = $info; 1025 } 1026 1027 return $info; 1028 1029 } elseif ($type === 'wmf') { 1030 1031 if ($this->wmf === null) { 1032 $this->wmf = new Wmf($this->mpdf, $this->colorConverter); 1033 } 1034 1035 $wmfres = $this->wmf->_getWMFimage($data); 1036 1037 if ($wmfres[0] == 0) { 1038 if ($wmfres[1]) { 1039 return $this->imageError($file, $firsttime, $wmfres[1]); 1040 } 1041 return $this->imageError($file, $firsttime, 'Error parsing WMF image'); 1042 } 1043 1044 $info = ['x' => $wmfres[2][0], 'y' => $wmfres[2][1], 'w' => $wmfres[3][0], 'h' => $wmfres[3][1], 'data' => $wmfres[1]]; 1045 $info['i'] = count($this->mpdf->formobjects) + 1; 1046 $info['type'] = 'wmf'; 1047 $this->mpdf->formobjects[$file] = $info; 1048 1049 return $info; 1050 1051 } else { // UNKNOWN TYPE - try GD imagecreatefromstring 1052 1053 $gd = function_exists('gd_info') 1054 ? gd_info() 1055 : []; 1056 1057 if (isset($gd['PNG Support']) && $gd['PNG Support']) { 1058 1059 $im = @imagecreatefromstring($data); 1060 1061 if (!$im) { 1062 return $this->imageError($file, $firsttime, 'Error parsing image file - image type not recognised, and not supported by GD imagecreate'); 1063 } 1064 1065 $tempfile = $this->cache->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png'); 1066 1067 imagealphablending($im, false); 1068 imagesavealpha($im, false); 1069 imageinterlace($im, false); 1070 1071 $check = @imagepng($im, $tempfile); 1072 1073 if (!$check) { 1074 return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') when using GD library to parse unknown image type'); 1075 } 1076 1077 $info = $this->getImage($tempfile, false); 1078 1079 imagedestroy($im); 1080 unlink($tempfile); 1081 1082 if (!$info) { 1083 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse unknown image type'); 1084 } 1085 1086 $info['type'] = 'png'; 1087 if ($firsttime) { 1088 $info['i'] = count($this->mpdf->images) + 1; 1089 $info['interpolation'] = $interpolation; // mPDF 6 1090 $this->mpdf->images[$file] = $info; 1091 } 1092 1093 return $info; 1094 } 1095 } 1096 1097 return $this->imageError($file, $firsttime, 'Error parsing image file - image type not recognised'); 1098 } 1099 1100 private function convertImage(&$data, $colspace, $targetcs, $w, $h, $dpi, $mask, $gamma_correction = false, $pngcolortype = false) 1101 { 1102 if (!function_exists('gd_info')) { 1103 return $this->imageError($file, $firsttime, 'GD library needed to parse image files'); 1104 } 1105 1106 if ($this->mpdf->PDFA || $this->mpdf->PDFX) { 1107 $mask = false; 1108 } 1109 1110 $im = @imagecreatefromstring($data); 1111 $info = []; 1112 $bpc = ord(substr($data, 24, 1)); 1113 1114 if ($im) { 1115 $imgdata = ''; 1116 $mimgdata = ''; 1117 $minfo = []; 1118 1119 // mPDF 6 Gamma correction 1120 // Need to extract alpha channel info before imagegammacorrect (which loses the data) 1121 if ($mask) { // i.e. $pngalpha for PNG 1122 // mPDF 6 1123 if ($colspace === 'Indexed') { // generate Alpha channel values from tRNS - only from PNG 1124 //Read transparency info 1125 $transparency = ''; 1126 $p = strpos($data, 'tRNS'); 1127 if ($p) { 1128 $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); 1129 $transparency = substr($data, $p + 4, $n); 1130 // ord($transparency[$index]) = the alpha value for that index 1131 // generate alpha channel 1132 for ($ypx = 0; $ypx < $h; ++$ypx) { 1133 for ($xpx = 0; $xpx < $w; ++$xpx) { 1134 $colorindex = imagecolorat($im, $xpx, $ypx); 1135 if ($colorindex >= $n) { 1136 $alpha = 255; 1137 } else { 1138 $alpha = ord($transparency[$colorindex]); 1139 } // 0-255 1140 $mimgdata .= chr($alpha); 1141 } 1142 } 1143 } 1144 } elseif ($pngcolortype === 0 || $pngcolortype === 2) { // generate Alpha channel values from tRNS 1145 // Get transparency as array of RGB 1146 $p = strpos($data, 'tRNS'); 1147 if ($p) { 1148 $trns = ''; 1149 $n = $this->fourBytesToInt(substr($data, $p - 4, 4)); 1150 $t = substr($data, $p + 4, $n); 1151 if ($colspace === 'DeviceGray') { // ct===0 1152 $trns = [$this->translateValue(substr($t, 0, 2), $bpc)]; 1153 } else /* $colspace=='DeviceRGB' */ { // ct==2 1154 $trns = []; 1155 $trns[0] = $this->translateValue(substr($t, 0, 2), $bpc); 1156 $trns[1] = $this->translateValue(substr($t, 2, 2), $bpc); 1157 $trns[2] = $this->translateValue(substr($t, 4, 2), $bpc); 1158 } 1159 1160 // generate alpha channel 1161 for ($ypx = 0; $ypx < $h; ++$ypx) { 1162 for ($xpx = 0; $xpx < $w; ++$xpx) { 1163 $rgb = imagecolorat($im, $xpx, $ypx); 1164 $r = ($rgb >> 16) & 0xFF; 1165 $g = ($rgb >> 8) & 0xFF; 1166 $b = $rgb & 0xFF; 1167 if ($colspace === 'DeviceGray' && $b == $trns[0]) { 1168 $alpha = 0; 1169 } elseif ($r == $trns[0] && $g == $trns[1] && $b == $trns[2]) { 1170 $alpha = 0; 1171 } // ct==2 1172 else { 1173 $alpha = 255; 1174 } 1175 $mimgdata .= chr($alpha); 1176 } 1177 } 1178 } 1179 } else { 1180 for ($i = 0; $i < $h; $i++) { 1181 for ($j = 0; $j < $w; $j++) { 1182 $rgb = imagecolorat($im, $j, $i); 1183 $alpha = ($rgb & 0x7F000000) >> 24; 1184 if ($alpha < 127) { 1185 $mimgdata .= chr(255 - ($alpha * 2)); 1186 } else { 1187 $mimgdata .= chr(0); 1188 } 1189 } 1190 } 1191 } 1192 } 1193 1194 // mPDF 6 Gamma correction 1195 if ($gamma_correction) { 1196 imagegammacorrect($im, $gamma_correction, 2.2); 1197 } 1198 1199 //Read transparency info 1200 $trns = []; 1201 $trnsrgb = false; 1202 if (!$this->mpdf->PDFA && !$this->mpdf->PDFX && !$mask) { // mPDF 6 added NOT mask 1203 $p = strpos($data, 'tRNS'); 1204 if ($p) { 1205 $n = $this->fourBytesToInt(substr($data, ($p - 4), 4)); 1206 $t = substr($data, $p + 4, $n); 1207 if ($colspace === 'DeviceGray') { // ct===0 1208 $trns = [$this->translateValue(substr($t, 0, 2), $bpc)]; 1209 } elseif ($colspace === 'DeviceRGB') { // ct==2 1210 $trns[0] = $this->translateValue(substr($t, 0, 2), $bpc); 1211 $trns[1] = $this->translateValue(substr($t, 2, 2), $bpc); 1212 $trns[2] = $this->translateValue(substr($t, 4, 2), $bpc); 1213 $trnsrgb = $trns; 1214 if ($targetcs === 'DeviceCMYK') { 1215 $col = $this->colorModeConverter->rgb2cmyk([3, $trns[0], $trns[1], $trns[2]]); 1216 $c1 = (int) ($col[1] * 2.55); 1217 $c2 = (int) ($col[2] * 2.55); 1218 $c3 = (int) ($col[3] * 2.55); 1219 $c4 = (int) ($col[4] * 2.55); 1220 $trns = [$c1, $c2, $c3, $c4]; 1221 } elseif ($targetcs === 'DeviceGray') { 1222 $c = (int) (($trns[0] * .21) + ($trns[1] * .71) + ($trns[2] * .07)); 1223 $trns = [$c]; 1224 } 1225 } else { // Indexed 1226 $pos = strpos($t, chr(0)); 1227 if (is_int($pos)) { 1228 $pal = imagecolorsforindex($im, $pos); 1229 $r = $pal['red']; 1230 $g = $pal['green']; 1231 $b = $pal['blue']; 1232 $trns = [$r, $g, $b]; // **** 1233 $trnsrgb = $trns; 1234 if ($targetcs === 'DeviceCMYK') { 1235 $col = $this->colorModeConverter->rgb2cmyk([3, $r, $g, $b]); 1236 $c1 = (int) ($col[1] * 2.55); 1237 $c2 = (int) ($col[2] * 2.55); 1238 $c3 = (int) ($col[3] * 2.55); 1239 $c4 = (int) ($col[4] * 2.55); 1240 $trns = [$c1, $c2, $c3, $c4]; 1241 } elseif ($targetcs === 'DeviceGray') { 1242 $c = (int) (($r * .21) + ($g * .71) + ($b * .07)); 1243 $trns = [$c]; 1244 } 1245 } 1246 } 1247 } 1248 } 1249 1250 for ($i = 0; $i < $h; $i++) { 1251 for ($j = 0; $j < $w; $j++) { 1252 $rgb = imagecolorat($im, $j, $i); 1253 $r = ($rgb >> 16) & 0xFF; 1254 $g = ($rgb >> 8) & 0xFF; 1255 $b = $rgb & 0xFF; 1256 if ($colspace === 'Indexed') { 1257 $pal = imagecolorsforindex($im, $rgb); 1258 $r = $pal['red']; 1259 $g = $pal['green']; 1260 $b = $pal['blue']; 1261 } 1262 1263 if ($targetcs === 'DeviceCMYK') { 1264 $col = $this->colorModeConverter->rgb2cmyk([3, $r, $g, $b]); 1265 $c1 = (int) ($col[1] * 2.55); 1266 $c2 = (int) ($col[2] * 2.55); 1267 $c3 = (int) ($col[3] * 2.55); 1268 $c4 = (int) ($col[4] * 2.55); 1269 if ($trnsrgb) { 1270 // original pixel was not set as transparent but processed color does match 1271 if ($trnsrgb !== [$r, $g, $b] && $trns === [$c1, $c2, $c3, $c4]) { 1272 if ($c4 === 0) { 1273 $c4 = 1; 1274 } else { 1275 $c4--; 1276 } 1277 } 1278 } 1279 $imgdata .= chr($c1) . chr($c2) . chr($c3) . chr($c4); 1280 } elseif ($targetcs === 'DeviceGray') { 1281 $c = (int) (($r * .21) + ($g * .71) + ($b * .07)); 1282 if ($trnsrgb) { 1283 // original pixel was not set as transparent but processed color does match 1284 if ($trnsrgb !== [$r, $g, $b] && $trns === [$c]) { 1285 if ($c === 0) { 1286 $c = 1; 1287 } else { 1288 $c--; 1289 } 1290 } 1291 } 1292 $imgdata .= chr($c); 1293 } elseif ($targetcs === 'DeviceRGB') { 1294 $imgdata .= chr($r) . chr($g) . chr($b); 1295 } 1296 } 1297 } 1298 1299 if ($targetcs === 'DeviceGray') { 1300 $ncols = 1; 1301 } elseif ($targetcs === 'DeviceRGB') { 1302 $ncols = 3; 1303 } elseif ($targetcs === 'DeviceCMYK') { 1304 $ncols = 4; 1305 } 1306 1307 $imgdata = $this->gzCompress($imgdata); 1308 $info = ['w' => $w, 'h' => $h, 'cs' => $targetcs, 'bpc' => 8, 'f' => 'FlateDecode', 'data' => $imgdata, 'type' => 'png', 1309 'parms' => '/DecodeParms <</Colors ' . $ncols . ' /BitsPerComponent 8 /Columns ' . $w . '>>']; 1310 if ($dpi) { 1311 $info['set-dpi'] = $dpi; 1312 } 1313 if ($mask) { 1314 $mimgdata = $this->gzCompress($mimgdata); 1315 $minfo = ['w' => $w, 'h' => $h, 'cs' => 'DeviceGray', 'bpc' => 8, 'f' => 'FlateDecode', 'data' => $mimgdata, 'type' => 'png', 1316 'parms' => '/DecodeParms <</Colors ' . $ncols . ' /BitsPerComponent 8 /Columns ' . $w . '>>']; 1317 if ($dpi) { 1318 $minfo['set-dpi'] = $dpi; 1319 } 1320 $tempfile = '_tempImgPNG' . md5($data) . random_int(1, 10000) . '.png'; 1321 $imgmask = count($this->mpdf->images) + 1; 1322 $minfo['i'] = $imgmask; 1323 $this->mpdf->images[$tempfile] = $minfo; 1324 $info['masked'] = $imgmask; 1325 } elseif ($trns) { 1326 $info['trns'] = $trns; 1327 } 1328 1329 imagedestroy($im); 1330 } 1331 return $info; 1332 } 1333 1334 private function jpgHeaderFromString(&$data) 1335 { 1336 $p = 4; 1337 $p += $this->twoBytesToInt(substr($data, $p, 2)); // Length of initial marker block 1338 $marker = substr($data, $p, 2); 1339 1340 while ($marker !== chr(255) . chr(192) && $marker !== chr(255) . chr(194) && $marker !== chr(255) . chr(193) && $p < strlen($data)) { 1341 // Start of frame marker (FFC0) (FFC1) or (FFC2) 1342 $p += $this->twoBytesToInt(substr($data, $p + 2, 2)) + 2; // Length of marker block 1343 $marker = substr($data, $p, 2); 1344 } 1345 1346 if ($marker !== chr(255) . chr(192) && $marker !== chr(255) . chr(194) && $marker !== chr(255) . chr(193)) { 1347 return false; 1348 } 1349 return substr($data, $p + 2, 10); 1350 } 1351 1352 private function jpgDataFromHeader($hdr) 1353 { 1354 $bpc = ord(substr($hdr, 2, 1)); 1355 1356 if (!$bpc) { 1357 $bpc = 8; 1358 } 1359 1360 $h = $this->twoBytesToInt(substr($hdr, 3, 2)); 1361 $w = $this->twoBytesToInt(substr($hdr, 5, 2)); 1362 1363 $channels = ord(substr($hdr, 7, 1)); 1364 1365 if ($channels === 3) { 1366 $colspace = 'DeviceRGB'; 1367 } elseif ($channels === 4) { 1368 $colspace = 'DeviceCMYK'; 1369 } else { 1370 $colspace = 'DeviceGray'; 1371 } 1372 1373 return [$w, $h, $colspace, $bpc, $channels]; 1374 } 1375 1376 /** 1377 * Corrects 2-byte integer to 8-bit depth value 1378 * If original image is bpc != 8, tRNS will be in this bpc 1379 * $im from imagecreatefromstring will always be in bpc=8 1380 * So why do we only need to correct 16-bit tRNS and NOT 2 or 4-bit??? 1381 */ 1382 private function translateValue($s, $bpc) 1383 { 1384 $n = $this->twoBytesToInt($s); 1385 1386 if ($bpc == 16) { 1387 $n = ($n >> 8); 1388 } 1389 1390 //elseif ($bpc==4) { $n = ($n << 2); } 1391 //elseif ($bpc==2) { $n = ($n << 4); } 1392 1393 return $n; 1394 } 1395 1396 /** 1397 * Read a 4-byte integer from string 1398 */ 1399 private function fourBytesToInt($s) 1400 { 1401 return (ord($s[0]) << 24) + (ord($s[1]) << 16) + (ord($s[2]) << 8) + ord($s[3]); 1402 } 1403 1404 /** 1405 * Equivalent to _get_ushort 1406 * Read a 2-byte integer from string 1407 */ 1408 private function twoBytesToInt($s) 1409 { 1410 return (ord(substr($s, 0, 1)) << 8) + ord(substr($s, 1, 1)); 1411 } 1412 1413 private function gzCompress($data) 1414 { 1415 if (!function_exists('gzcompress')) { 1416 throw new \Mpdf\MpdfException('gzcompress is not available. install ext-zlib extension.'); 1417 } 1418 1419 return gzcompress($data); 1420 } 1421 1422 /** 1423 * Throw an exception and save re-trying image URL's which have already failed 1424 */ 1425 private function imageError($file, $firsttime, $msg) 1426 { 1427 $this->failedImages[$file] = true; 1428 1429 if ($firsttime && ($this->mpdf->showImageErrors || $this->mpdf->debug)) { 1430 throw new \Mpdf\MpdfImageException(sprintf('%s (%s)', $msg, substr($file, 0, 256))); 1431 } 1432 1433 $this->logger->warning(sprintf('%s (%s)', $msg, $file), ['context' => LogContext::IMAGES]); 1434 } 1435 1436 /** 1437 * @since mPDF 5.7.4 1438 * @param string $url 1439 * @return string 1440 */ 1441 private function urldecodeParts($url) 1442 { 1443 $file = $url; 1444 $query = ''; 1445 if (preg_match('/[?]/', $url)) { 1446 $bits = preg_split('/[?]/', $url, 2); 1447 $file = $bits[0]; 1448 $query = '?' . $bits[1]; 1449 } 1450 $file = rawurldecode($file); 1451 $query = urldecode($query); 1452 1453 return $file . $query; 1454 } 1455 1456} 1457