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