6 use Mpdf\Color\ColorConverter
;
7 use Mpdf\Color\ColorModeConverter
;
10 use Mpdf\Language\LanguageToFontInterface
;
11 use Mpdf\Language\ScriptToLanguageInterface
;
12 use Mpdf\Log\Context
as LogContext
;
15 use Mpdf\SizeConverter
;
17 use Psr\Log\LoggerInterface
;
19 class ImageProcessor
implements \Psr\Log\LoggerAwareInterface
33 * @var \Mpdf\CssManager
38 * @var \Mpdf\SizeConverter
40 private $sizeConverter;
43 * @var \Mpdf\Color\ColorConverter
45 private $colorConverter;
48 * @var \Mpdf\Color\ColorModeConverter
50 private $colorModeConverter;
58 * @var \Mpdf\Image\ImageTypeGuesser
65 private $failedImages;
68 * @var \Mpdf\Image\Bmp
73 * @var \Mpdf\Image\Wmf
78 * @var \Mpdf\Language\LanguageToFontInterface
80 private $languageToFont;
83 * @var \Mpdf\Language\ScriptToLanguageInterface
85 public $scriptToLanguage;
88 * @var \Psr\Log\LoggerInterface
92 public function __construct(
95 CssManager
$cssManager,
96 SizeConverter
$sizeConverter,
97 ColorConverter
$colorConverter,
98 ColorModeConverter
$colorModeConverter,
100 LanguageToFontInterface
$languageToFont,
101 ScriptToLanguageInterface
$scriptToLanguage,
102 LoggerInterface
$logger
107 $this->cssManager
= $cssManager;
108 $this->sizeConverter
= $sizeConverter;
109 $this->colorConverter
= $colorConverter;
110 $this->colorModeConverter
= $colorModeConverter;
111 $this->cache
= $cache;
112 $this->languageToFont
= $languageToFont;
113 $this->scriptToLanguage
= $scriptToLanguage;
115 $this->logger
= $logger;
117 $this->guesser
= new ImageTypeGuesser();
119 $this->failedImages
= [];
123 * @param \Psr\Log\LoggerInterface
127 public function setLogger(LoggerInterface
$logger)
129 $this->logger
= $logger;
134 public function getImage(&$file, $firsttime = true, $allowvector = true, $orig_srcpath = false, $interpolation = false)
137 // firsttime i.e. whether to add to this->images - use false when calling iteratively
138 // Image Data passed directly as var:varname
139 if (preg_match('/var:\s*(.*)/', $file, $v)) {
140 if (!isset($this->mpdf
->imageVars
[$v[1]])) {
141 return $this->imageError($file, $firsttime, 'Unknown image variable');
143 $data = $this->mpdf
->imageVars
[$v[1]];
147 if (preg_match('/data:image\/(gif|jpeg|png);base64,(.*)/', $file, $v)) {
149 $data = base64_decode($v[2]);
154 if ($firsttime && $file && substr($file, 0, 5) != 'data:') {
155 $file = str_replace(" ", "%20", $file);
157 if ($firsttime && $orig_srcpath) {
158 // If orig_srcpath is a relative file path (and not a URL), then it needs to be URL decoded
159 if (substr($orig_srcpath, 0, 5) != 'data:') {
160 $orig_srcpath = str_replace(" ", "%20", $orig_srcpath);
162 if (!preg_match('/^(http|ftp)/', $orig_srcpath)) {
163 $orig_srcpath = $this->urldecodeParts($orig_srcpath);
168 if ($orig_srcpath && isset($this->mpdf
->images
[$orig_srcpath])) {
169 $file = $orig_srcpath;
170 return $this->mpdf
->images
[$orig_srcpath];
173 if (isset($this->mpdf
->images
[$file])) {
174 return $this->mpdf
->images
[$file];
175 } elseif ($orig_srcpath && isset($this->mpdf
->formobjects
[$orig_srcpath])) {
176 $file = $orig_srcpath;
177 return $this->mpdf
->formobjects
[$file];
178 } elseif (isset($this->mpdf
->formobjects
[$file])) {
179 return $this->mpdf
->formobjects
[$file];
180 } elseif ($firsttime && isset($this->failedImages
[$file])) { // Save re-trying image URL's which have already failed
181 return $this->imageError($file, $firsttime, '');
189 if ($orig_srcpath && $this->mpdf
->basepathIsLocal
&& $check = @fopen
($orig_srcpath, "rb")) {
191 $file = $orig_srcpath;
192 $this->logger
->debug(sprintf('Fetching (file_get_contents) content of file "%s" with local basepath', $file), ['context' => LogContext
::REMOTE_CONTENT
]);
193 $data = file_get_contents($file);
194 $type = $this->guesser
->guess($data);
197 if (!$data && $check = @fopen
($file, "rb")) {
199 $this->logger
->debug(sprintf('Fetching (file_get_contents) content of file "%s" with non-local basepath', $file), ['context' => LogContext
::REMOTE_CONTENT
]);
200 $data = file_get_contents($file);
201 $type = $this->guesser
->guess($data);
204 if ((!$data ||
!$type) && function_exists('curl_init')) { // mPDF 5.7.4
205 $this->mpdf
->getFileContentsByCurl($file, $data); // needs full url?? even on local (never needed for local)
207 $type = $this->guesser
->guess($data);
211 if ((!$data ||
!$type) && !ini_get('allow_url_fopen')) { // only worth trying if remote file and !ini_get('allow_url_fopen')
212 $this->mpdf
->getFileContentsBySocket($file, $data); // needs full url?? even on local (never needed for local)
214 $type = $this->guesser
->guess($data);
220 return $this->imageError($file, $firsttime, 'Could not find image file');
224 $type = $this->guesser
->guess($data);
227 if (($type == 'wmf' ||
$type == 'svg') && !$allowvector) {
228 return $this->imageError($file, $firsttime, 'WMF or SVG image file not supported in this context');
232 if ($type == 'svg') {
233 $svg = new Svg($this->mpdf
, $this->otl
, $this->cssManager
, $this, $this->sizeConverter
, $this->colorConverter
, $this->languageToFont
, $this->scriptToLanguage
);
234 $family = $this->mpdf
->FontFamily
;
235 $style = $this->mpdf
->FontStyle
;
236 $size = $this->mpdf
->FontSizePt
;
237 $info = $svg->ImageSVG($data);
240 $this->mpdf
->SetFont($family, $style, $size, false);
243 return $this->imageError($file, $firsttime, 'Error parsing SVG file');
245 $info['type'] = 'svg';
246 $info['i'] = count($this->mpdf
->formobjects
) +
1;
247 $this->mpdf
->formobjects
[$file] = $info;
253 if ($type == 'jpeg' ||
$type == 'jpg') {
254 $hdr = $this->jpgHeaderFromString($data);
256 return $this->imageError($file, $firsttime, 'Error parsing JPG header');
258 $a = $this->jpgDataFromHeader($hdr);
259 $channels = intval($a[4]);
260 $j = strpos($data, 'JFIF');
263 $unitSp = ord(substr($data, ($j +
7), 1));
265 $ppUx = $this->twoBytesToInt(substr($data, ($j +
8), 2)); // horizontal pixels per meter, usually set to zero
266 if ($unitSp == 2) { // = dots per cm (if == 1 set as dpi)
267 $ppUx = round($ppUx / 10 * 25.4);
271 if ($a[2] == 'DeviceCMYK' && (($this->mpdf
->PDFA
&& $this->mpdf
->restrictColorSpace
!= 3) ||
$this->mpdf
->restrictColorSpace
== 2)) {
273 // convert to RGB image
274 if (!function_exists("gd_info")) {
275 throw new \Mpdf\
MpdfException("JPG image may not use CMYK color space (" . $file . ").");
278 if ($this->mpdf
->PDFA
&& !$this->mpdf
->PDFAauto
) {
279 $this->mpdf
->PDFAXwarnings
[] = "JPG image may not use CMYK color space - " . $file . " - (Image converted to RGB. NB This will alter the colour profile of the image.)";
282 $im = @imagecreatefromstring
($data);
284 $tempfile = $this->cache
->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png');
285 imageinterlace($im, false);
286 $check = @imagepng
($im, $tempfile);
288 return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') whilst using GD library to parse JPG(CMYK) image');
290 $info = $this->getImage($tempfile, false);
292 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse JPG(CMYK) image');
296 $info['type'] = 'jpg';
298 $info['i'] = count($this->mpdf
->images
) +
1;
299 $info['interpolation'] = $interpolation; // mPDF 6
300 $this->mpdf
->images
[$file] = $info;
304 return $this->imageError($file, $firsttime, 'Error creating GD image file from JPG(CMYK) image');
307 } elseif ($a[2] == 'DeviceRGB' && ($this->mpdf
->PDFX ||
$this->mpdf
->restrictColorSpace
== 3)) {
308 // Convert to CMYK image stream - nominally returned as type='png'
309 $info = $this->convImage($data, $a[2], 'DeviceCMYK', $a[0], $a[1], $ppUx, false);
310 if (($this->mpdf
->PDFA
&& !$this->mpdf
->PDFAauto
) ||
($this->mpdf
->PDFX
&& !$this->mpdf
->PDFXauto
)) {
311 $this->mpdf
->PDFAXwarnings
[] = "JPG image may not use RGB color space - " . $file . " - (Image converted to CMYK. NB This will alter the colour profile of the image.)";
314 } elseif (($a[2] == 'DeviceRGB' ||
$a[2] == 'DeviceCMYK') && $this->mpdf
->restrictColorSpace
== 1) {
315 // Convert to Grayscale image stream - nominally returned as type='png'
316 $info = $this->convImage($data, $a[2], 'DeviceGray', $a[0], $a[1], $ppUx, false);
319 // mPDF 6 Detect Adobe APP14 Tag
320 //$pos = strpos($data, "\xFF\xEE\x00\x0EAdobe\0");
321 //if ($pos !== false) {
323 // mPDF 6 ICC profile
326 while (($pos = strpos($data, "ICC_PROFILE\0", $offset)) !== false) {
327 // get ICC sequence length
328 $length = $this->twoBytesToInt(substr($data, ($pos - 2), 2)) - 16;
329 $sn = max(1, ord($data[($pos +
12)]));
330 $nom = max(1, ord($data[($pos +
13)]));
331 $icc[($sn - 1)] = substr($data, ($pos +
14), $length);
332 $offset = ($pos +
14 +
$length);
334 // order and compact ICC segments
335 if (count($icc) > 0) {
337 $icc = implode('', $icc);
338 if (substr($icc, 36, 4) != 'acsp') {
339 // invalid ICC profile
342 $input = substr($icc, 16, 4);
343 $output = substr($icc, 20, 4);
344 // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab
345 if ($input != 'RGB ' ||
$output != 'XYZ ') {
352 $info = ['w' => $a[0], 'h' => $a[1], 'cs' => $a[2], 'bpc' => $a[3], 'f' => 'DCTDecode', 'data' => $data, 'type' => 'jpg', 'ch' => $channels, 'icc' => $icc];
354 $info['set-dpi'] = $ppUx;
359 return $this->imageError($file, $firsttime, 'Error parsing or converting JPG image');
363 $info['i'] = count($this->mpdf
->images
) +
1;
364 $info['interpolation'] = $interpolation; // mPDF 6
365 $this->mpdf
->images
[$file] = $info;
369 } elseif ($type == 'png') {
372 if (substr($data, 0, 8) != chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) {
373 return $this->imageError($file, $firsttime, 'Error parsing PNG identifier');
377 if (substr($data, 12, 4) != 'IHDR') {
378 return $this->imageError($file, $firsttime, 'Incorrect PNG file (no IHDR block found)');
381 $w = $this->fourBytesToInt(substr($data, 16, 4));
382 $h = $this->fourBytesToInt(substr($data, 20, 4));
383 $bpc = ord(substr($data, 24, 1));
388 // if($bpc>8) { $errpng = 'not 8-bit depth'; } // mPDF 6 Allow through to be handled as native PNG
390 $ct = ord(substr($data, 25, 1));
392 $colspace = 'DeviceGray';
394 } elseif ($ct == 2) {
395 $colspace = 'DeviceRGB';
397 } elseif ($ct == 3) {
398 $colspace = 'Indexed';
400 } elseif ($ct == 4) {
401 $colspace = 'DeviceGray';
403 $errpng = 'alpha channel';
406 $colspace = 'DeviceRGB';
408 $errpng = 'alpha channel';
412 if ($ct < 4 && strpos($data, 'tRNS') !== false) {
413 $errpng = 'transparency';
417 if ($ct == 3 && strpos($data, 'iCCP') !== false) {
418 $errpng = 'indexed plus ICC';
420 // $pngalpha is used as a FLAG of any kind of transparency which COULD be tranferred to an alpha channel
421 // incl. single-color tarnsparency, depending which type of handling occurs later
423 if (ord(substr($data, 26, 1)) != 0) {
424 $errpng = 'compression method';
425 } // only 0 should be specified
426 if (ord(substr($data, 27, 1)) != 0) {
427 $errpng = 'filter method';
428 } // only 0 should be specified
429 if (ord(substr($data, 28, 1)) != 0) {
430 $errpng = 'interlaced file';
433 $j = strpos($data, 'pHYs');
436 $unitSp = ord(substr($data, ($j +
12), 1));
438 $ppUx = $this->fourBytesToInt(substr($data, ($j +
4), 4)); // horizontal pixels per meter, usually set to zero
439 $ppUx = round($ppUx / 1000 * 25.4);
443 // mPDF 6 Gamma correction
446 $j = strpos($data, 'gAMA');
447 if ($j && strpos($data, 'sRGB') === false) { // sRGB colorspace - overrides gAMA
448 $gAMA = $this->fourBytesToInt(substr($data, ($j +
4), 4)); // Gamma value times 100000
451 // http://www.libpng.org/pub/png/spec/1.2/PNG-Encoders.html
452 // "If the source file's gamma value is greater than 1.0, it is probably a display system exponent,..."
453 // ("..and you should use its reciprocal for the PNG gamma.")
454 //if ($gAMA > 1) { $gAMA = 1/$gAMA; }
455 // (Some) Applications seem to ignore it... appearing how it was probably intended
456 // Test Case - image(s) on http://www.w3.org/TR/CSS21/intro.html - PNG has gAMA set as 1.45454
457 // Probably unintentional as mentioned above and should be 0.45454 which is 1 / 2.2
458 // Tested on Windows PC
459 // Firefox and Opera display gray as 234 (correct, but looks wrong)
460 // IE9 and Safari display gray as 193 (incorrect but looks right)
461 // See test different gamma chunks at http://www.libpng.org/pub/png/pngsuite-all-good.html
468 // Don't need to apply gamma correction if == default i.e. 2.2
469 if ($gamma > 2.15 && $gamma < 2.25) {
473 // NOT supported at present
474 //$j = strpos($data,'sRGB'); // sRGB colorspace - overrides gAMA
475 //$j = strpos($data,'cHRM'); // Chromaticity and Whitepoint
476 // $firsttime added mPDF 6 so when PNG Grayscale with alpha using resrtictcolorspace to CMYK
477 // the alpha channel is sent through as secondtime as Indexed and should not be converted to CMYK
478 if ($firsttime && ($colspace == 'DeviceRGB' ||
$colspace == 'Indexed') && ($this->mpdf
->PDFX ||
$this->mpdf
->restrictColorSpace
== 3)) {
479 // Convert to CMYK image stream - nominally returned as type='png'
480 $info = $this->convImage($data, $colspace, 'DeviceCMYK', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction
481 if (($this->mpdf
->PDFA
&& !$this->mpdf
->PDFAauto
) ||
($this->mpdf
->PDFX
&& !$this->mpdf
->PDFXauto
)) {
482 $this->mpdf
->PDFAXwarnings
[] = "PNG image may not use RGB color space - " . $file . " - (Image converted to CMYK. NB This will alter the colour profile of the image.)";
484 } // $firsttime added mPDF 6 so when PNG Grayscale with alpha using resrtictcolorspace to CMYK
485 // the alpha channel is sent through as secondtime as Indexed and should not be converted to CMYK
486 elseif ($firsttime && ($colspace == 'DeviceRGB' ||
$colspace == 'Indexed') && $this->mpdf
->restrictColorSpace
== 1) {
487 // Convert to Grayscale image stream - nominally returned as type='png'
488 $info = $this->convImage($data, $colspace, 'DeviceGray', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction
489 } elseif (($this->mpdf
->PDFA ||
$this->mpdf
->PDFX
) && $pngalpha) {
490 // Remove alpha channel
491 if ($this->mpdf
->restrictColorSpace
== 1) { // Grayscale
492 $info = $this->convImage($data, $colspace, 'DeviceGray', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction
493 } elseif ($this->mpdf
->restrictColorSpace
== 3) { // CMYK
494 $info = $this->convImage($data, $colspace, 'DeviceCMYK', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction
495 } elseif ($this->mpdf
->PDFA
) { // RGB
496 $info = $this->convImage($data, $colspace, 'DeviceRGB', $w, $h, $ppUx, $pngalpha, $gamma, $ct); // mPDF 5.7.2 Gamma correction
498 if (($this->mpdf
->PDFA
&& !$this->mpdf
->PDFAauto
) ||
($this->mpdf
->PDFX
&& !$this->mpdf
->PDFXauto
)) {
499 $this->mpdf
->PDFAXwarnings
[] = "Transparency (alpha channel) not permitted in PDFA or PDFX files - " . $file . " - (Image converted to one without transparency.)";
501 } elseif ($firsttime && ($errpng ||
$pngalpha ||
$gamma)) { // mPDF 5.7.2 Gamma correction
502 if (function_exists('gd_info')) {
507 if (!isset($gd['PNG Support'])) {
508 return $this->imageError($file, $firsttime, 'GD library required for PNG image (' . $errpng . ')');
510 $im = imagecreatefromstring($data);
513 return $this->imageError($file, $firsttime, 'Error creating GD image from PNG file (' . $errpng . ')');
518 $tempfile = $this->cache
->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png');
520 // Alpha channel set (including using tRNS for Paletted images)
522 if ($this->mpdf
->PDFA
) {
523 throw new \Mpdf\
MpdfException("PDFA1-b does not permit images with alpha channel transparency (" . $file . ").");
526 $imgalpha = imagecreate($w, $h);
527 // generate gray scale pallete
528 for ($c = 0; $c < 256; ++
$c) {
529 imagecolorallocate($imgalpha, $c, $c, $c);
533 if ($colspace == 'Indexed') { // generate Alpha channel values from tRNS
534 // Read transparency info
535 $p = strpos($data, 'tRNS');
537 $n = $this->fourBytesToInt(substr($data, ($p - 4), 4));
538 $transparency = substr($data, ($p +
4), $n);
539 // ord($transparency{$index}) = the alpha value for that index
540 // generate alpha channel
541 for ($ypx = 0; $ypx < $h; ++
$ypx) {
542 for ($xpx = 0; $xpx < $w; ++
$xpx) {
543 $colorindex = imagecolorat($im, $xpx, $ypx);
544 if ($colorindex >= $n) {
547 $alpha = ord($transparency{$colorindex});
550 imagesetpixel($imgalpha, $xpx, $ypx, $alpha);
555 } elseif ($ct === 0 ||
$ct == 2) { // generate Alpha channel values from tRNS
556 // Get transparency as array of RGB
557 $p = strpos($data, 'tRNS');
560 $n = $this->fourBytesToInt(substr($data, ($p - 4), 4));
561 $t = substr($data, ($p +
4), $n);
562 if ($colspace == 'DeviceGray') { // ct===0
563 $trns = [$this->translateValue(substr($t, 0, 2), $bpc)];
564 } else /* $colspace=='DeviceRGB' */ { // ct==2
566 $trns[0] = $this->translateValue(substr($t, 0, 2), $bpc);
567 $trns[1] = $this->translateValue(substr($t, 2, 2), $bpc);
568 $trns[2] = $this->translateValue(substr($t, 4, 2), $bpc);
571 // generate alpha channel
572 for ($ypx = 0; $ypx < $h; ++
$ypx) {
573 for ($xpx = 0; $xpx < $w; ++
$xpx) {
574 $rgb = imagecolorat($im, $xpx, $ypx);
575 $r = ($rgb >> 16) & 0xFF;
576 $g = ($rgb >> 8) & 0xFF;
578 if ($colspace == 'DeviceGray' && $b == $trns[0]) {
580 } elseif ($r == $trns[0] && $g == $trns[1] && $b == $trns[2]) {
587 imagesetpixel($imgalpha, $xpx, $ypx, $alpha);
593 // extract alpha channel
594 for ($ypx = 0; $ypx < $h; ++
$ypx) {
595 for ($xpx = 0; $xpx < $w; ++
$xpx) {
596 $alpha = (imagecolorat($im, $xpx, $ypx) & 0x7F000000) >> 24;
598 imagesetpixel($imgalpha, $xpx, $ypx, (255 - ($alpha * 2)));
604 // NB This must happen after the Alpha channel is extracted
605 // imagegammacorrect() removes the alpha channel data in $im - (I think this is a bug in PHP)
607 imagegammacorrect($im, $gamma, 2.2);
610 $tempfile_alpha = $this->cache
->tempFilename('_tempMskPNG' . md5($file) . random_int(1, 10000) . '.png');
612 $check = @imagepng
($imgalpha, $tempfile_alpha);
615 return $this->imageError($file, $firsttime, 'Failed to create temporary image file (' . $tempfile_alpha . ') parsing PNG image with alpha channel (' . $errpng . ')');
618 imagedestroy($imgalpha);
619 // extract image without alpha channel
620 $imgplain = imagecreatetruecolor($w, $h);
621 imagealphablending($imgplain, false); // mPDF 5.7.2
622 imagecopy($imgplain, $im, 0, 0, 0, 0, $w, $h);
624 // create temp image file
625 $check = @imagepng
($imgplain, $tempfile);
627 return $this->imageError($file, $firsttime, 'Failed to create temporary image file (' . $tempfile . ') parsing PNG image with alpha channel (' . $errpng . ')');
629 imagedestroy($imgplain);
631 $minfo = $this->getImage($tempfile_alpha, false);
632 unlink($tempfile_alpha);
635 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile_alpha . ') created with GD library to parse PNG image');
638 $imgmask = count($this->mpdf
->images
) +
1;
639 $minfo['cs'] = 'DeviceGray';
640 $minfo['i'] = $imgmask;
641 $this->mpdf
->images
[$tempfile_alpha] = $minfo;
642 // embed image, masked with previously embedded mask
643 $info = $this->getImage($tempfile, false);
647 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse PNG image');
650 $info['masked'] = $imgmask;
652 $info['set-dpi'] = $ppUx;
654 $info['type'] = 'png';
656 $info['i'] = count($this->mpdf
->images
) +
1;
657 $info['interpolation'] = $interpolation; // mPDF 6
658 $this->mpdf
->images
[$file] = $info;
662 } else { // No alpha/transparency set (but cannot read directly because e.g. bit-depth != 8, interlaced etc)
665 $p = strpos($data, 'iCCP');
666 if ($p && $colspace == "Indexed") { // Cannot have ICC profile and Indexed together
668 $n = $this->fourBytesToInt(substr($data, ($p - 8), 4));
669 $nullsep = strpos(substr($data, $p, 80), chr(0));
670 $icc = substr($data, ($p +
$nullsep +
2), ($n - ($nullsep +
2)));
671 $icc = @gzuncompress
($icc); // Ignored if fails
673 if (substr($icc, 36, 4) != 'acsp') {
675 } // invalid ICC profile
677 $input = substr($icc, 16, 4);
678 $output = substr($icc, 20, 4);
679 // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab
680 if ($input != 'RGB ' ||
$output != 'XYZ ') {
685 // Convert to RGB colorspace so can use ICC Profile
687 imagepalettetotruecolor($im);
688 $colspace = 'DeviceRGB';
694 imagegammacorrect($im, $gamma, 2.2);
697 imagealphablending($im, false);
698 imagesavealpha($im, false);
699 imageinterlace($im, false);
701 $check = @imagepng
($im, $tempfile);
703 return $this->imageError($file, $firsttime, 'Failed to create temporary image file (' . $tempfile . ') parsing PNG image (' . $errpng . ')');
706 $info = $this->getImage($tempfile, false);
709 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse PNG image');
713 $info['set-dpi'] = $ppUx;
715 $info['type'] = 'png';
717 $info['i'] = count($this->mpdf
->images
) +
1;
718 $info['interpolation'] = $interpolation; // mPDF 6
720 $info['ch'] = $channels;
723 $this->mpdf
->images
[$file] = $info;
728 } else { // PNG image with no need to convert alph channels, bpc <> 8 etc.
729 $parms = '/DecodeParms <</Predictor 15 /Colors ' . $channels . ' /BitsPerComponent ' . $bpc . ' /Columns ' . $w . '>>';
730 //Scan chunks looking for palette, transparency and image data
737 $n = $this->fourBytesToInt(substr($data, $p, 4));
739 $type = substr($data, $p, 4);
741 if ($type == 'PLTE') {
743 $pal = substr($data, $p, $n);
746 } elseif ($type == 'tRNS') {
747 //Read transparency info
748 $t = substr($data, $p, $n);
751 $trns = [ord(substr($t, 1, 1))];
752 } elseif ($ct == 2) {
753 $trns = [ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1))];
755 $pos = strpos($t, chr(0));
761 } elseif ($type == 'IDAT') {
762 $pngdata.=substr($data, $p, $n);
765 } elseif ($type == 'iCCP') {
766 $nullsep = strpos(substr($data, $p, 80), chr(0));
767 $icc = substr($data, ($p +
$nullsep +
2), ($n - ($nullsep +
2)));
768 $icc = @gzuncompress
($icc); // Ignored if fails
770 if (substr($icc, 36, 4) != 'acsp') {
772 } // invalid ICC profile
774 $input = substr($icc, 16, 4);
775 $output = substr($icc, 20, 4);
776 // Ignore Color profiles for conversion to other colorspaces e.g. CMYK/Lab
777 if ($input != 'RGB ' ||
$output != 'XYZ ') {
784 } elseif ($type == 'IEND') {
786 } elseif (preg_match('/[a-zA-Z]{4}/', $type)) {
789 return $this->imageError($file, $firsttime, 'Error parsing PNG image data');
793 return $this->imageError($file, $firsttime, 'Error parsing PNG image data - no IDAT data found');
795 if ($colspace == 'Indexed' && empty($pal)) {
796 return $this->imageError($file, $firsttime, 'Error parsing PNG image data - missing colour palette');
799 if ($colspace == 'Indexed' && $icc) {
801 } // mPDF 6 cannot have ICC profile and Indexed in a PDF document as both use the colorspace tag.
803 $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'FlateDecode', 'parms' => $parms, 'pal' => $pal, 'trns' => $trns, 'data' => $pngdata, 'ch' => $channels, 'icc' => $icc];
804 $info['type'] = 'png';
806 $info['set-dpi'] = $ppUx;
811 return $this->imageError($file, $firsttime, 'Error parsing or converting PNG image');
815 $info['i'] = count($this->mpdf
->images
) +
1;
816 $info['interpolation'] = $interpolation; // mPDF 6
817 $this->mpdf
->images
[$file] = $info;
822 } elseif ($type == 'gif') { // GIF
824 if (function_exists('gd_info')) {
830 if (isset($gd['GIF Read Support']) && $gd['GIF Read Support']) {
832 $im = @imagecreatefromstring
($data);
834 $tempfile = $this->cache
->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png');
835 imagealphablending($im, false);
836 imagesavealpha($im, false);
837 imageinterlace($im, false);
838 if (!is_writable($tempfile)) {
840 $check = @imagepng
($im);
842 return $this->imageError($file, $firsttime, 'Error creating temporary image object whilst using GD library to parse GIF image');
844 $this->mpdf
->imageVars
['tempImage'] = ob_get_contents();
845 $tempimglnk = 'var:tempImage';
847 $info = $this->getImage($tempimglnk, false);
849 return $this->imageError($file, $firsttime, 'Error parsing temporary file image object created with GD library to parse GIF image');
853 $check = @imagepng
($im, $tempfile);
855 return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') whilst using GD library to parse GIF image');
857 $info = $this->getImage($tempfile, false);
859 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse GIF image');
864 $info['type'] = 'gif';
866 $info['i'] = count($this->mpdf
->images
) +
1;
867 $info['interpolation'] = $interpolation; // mPDF 6
868 $this->mpdf
->images
[$file] = $info;
872 return $this->imageError($file, $firsttime, 'Error creating GD image file from GIF image');
880 $gif->loadFile($data, 0);
882 if (isset($gif->m_img
->m_gih
->m_bLocalClr
) && $gif->m_img
->m_gih
->m_bLocalClr
) {
883 $nColors = $gif->m_img
->m_gih
->m_nTableSize
;
884 $pal = $gif->m_img
->m_gih
->m_colorTable
->toString();
885 if ((isset($bgColor)) and $bgColor != -1) { // mPDF 5.7.3
886 $bgColor = $gif->m_img
->m_gih
->m_colorTable
->colorIndex($bgColor);
888 $colspace = 'Indexed';
889 } elseif (isset($gif->m_gfh
->m_bGlobalClr
) && $gif->m_gfh
->m_bGlobalClr
) {
890 $nColors = $gif->m_gfh
->m_nTableSize
;
891 $pal = $gif->m_gfh
->m_colorTable
->toString();
892 if ((isset($bgColor)) and $bgColor != -1) {
893 $bgColor = $gif->m_gfh
->m_colorTable
->colorIndex($bgColor);
895 $colspace = 'Indexed';
899 $colspace = 'DeviceGray';
904 if (isset($gif->m_img
->m_bTrans
) && $gif->m_img
->m_bTrans
&& ($nColors > 0)) {
905 $trns = [$gif->m_img
->m_nTrans
];
907 $gifdata = $gif->m_img
->m_data
;
908 $w = $gif->m_gfh
->m_nWidth
;
909 $h = $gif->m_gfh
->m_nHeight
;
912 if ($colspace == 'Indexed' and empty($pal)) {
913 return $this->imageError($file, $firsttime, 'Error parsing GIF image - missing colour palette');
916 if ($this->mpdf
->compress
) {
917 $gifdata = $this->gzCompress($gifdata);
918 $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => 8, 'f' => 'FlateDecode', 'pal' => $pal, 'trns' => $trns, 'data' => $gifdata];
920 $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => 8, 'pal' => $pal, 'trns' => $trns, 'data' => $gifdata];
923 $info['type'] = 'gif';
925 $info['i'] = count($this->mpdf
->images
) +
1;
926 $info['interpolation'] = $interpolation; // mPDF 6
927 $this->mpdf
->images
[$file] = $info;
932 } elseif ($type == 'bmp') {
934 if (empty($this->bmp
)) {
935 $this->bmp
= new Bmp($this->mpdf
);
937 $info = $this->bmp
->_getBMPimage($data, $file);
938 if (isset($info['error'])) {
939 return $this->imageError($file, $firsttime, $info['error']);
942 $info['i'] = count($this->mpdf
->images
) +
1;
943 $info['interpolation'] = $interpolation; // mPDF 6
944 $this->mpdf
->images
[$file] = $info;
948 } elseif ($type == 'wmf') {
950 if (empty($this->wmf
)) {
951 $this->wmf
= new Wmf($this->mpdf
, $this->colorConverter
);
954 $wmfres = $this->wmf
->_getWMFimage($data);
956 if ($wmfres[0] == 0) {
958 return $this->imageError($file, $firsttime, $wmfres[1]);
960 return $this->imageError($file, $firsttime, 'Error parsing WMF image');
963 $info = ['x' => $wmfres[2][0], 'y' => $wmfres[2][1], 'w' => $wmfres[3][0], 'h' => $wmfres[3][1], 'data' => $wmfres[1]];
964 $info['i'] = count($this->mpdf
->formobjects
) +
1;
965 $info['type'] = 'wmf';
966 $this->mpdf
->formobjects
[$file] = $info;
970 } else { // UNKNOWN TYPE - try GD imagecreatefromstring
972 if (function_exists('gd_info')) {
978 if (isset($gd['PNG Support']) && $gd['PNG Support']) {
980 $im = @imagecreatefromstring
($data);
983 return $this->imageError($file, $firsttime, 'Error parsing image file - image type not recognised, and not supported by GD imagecreate');
986 $tempfile = $this->cache
->tempFilename('_tempImgPNG' . md5($file) . random_int(1, 10000) . '.png');
988 imagealphablending($im, false);
989 imagesavealpha($im, false);
990 imageinterlace($im, false);
992 $check = @imagepng
($im, $tempfile);
995 return $this->imageError($file, $firsttime, 'Error creating temporary file (' . $tempfile . ') whilst using GD library to parse unknown image type');
998 $info = $this->getImage($tempfile, false);
1004 return $this->imageError($file, $firsttime, 'Error parsing temporary file (' . $tempfile . ') created with GD library to parse unknown image type');
1007 $info['type'] = 'png';
1009 $info['i'] = count($this->mpdf
->images
) +
1;
1010 $info['interpolation'] = $interpolation; // mPDF 6
1011 $this->mpdf
->images
[$file] = $info;
1018 return $this->imageError($file, $firsttime, 'Error parsing image file - image type not recognised');
1021 private function convImage(&$data, $colspace, $targetcs, $w, $h, $dpi, $mask, $gamma_correction = false, $pngcolortype = false)
1023 if ($this->mpdf
->PDFA ||
$this->mpdf
->PDFX
) {
1027 $im = @imagecreatefromstring
($data);
1029 $bpc = ord(substr($data, 24, 1));
1036 // mPDF 6 Gamma correction
1037 // Need to extract alpha channel info before imagegammacorrect (which loses the data)
1038 if ($mask) { // i.e. $pngalpha for PNG
1040 if ($colspace == 'Indexed') { // generate Alpha channel values from tRNS - only from PNG
1041 //Read transparency info
1043 $p = strpos($data, 'tRNS');
1045 $n = $this->fourBytesToInt(substr($data, ($p - 4), 4));
1046 $transparency = substr($data, ($p +
4), $n);
1047 // ord($transparency{$index}) = the alpha value for that index
1048 // generate alpha channel
1049 for ($ypx = 0; $ypx < $h; ++
$ypx) {
1050 for ($xpx = 0; $xpx < $w; ++
$xpx) {
1051 $colorindex = imagecolorat($im, $xpx, $ypx);
1052 if ($colorindex >= $n) {
1055 $alpha = ord($transparency{$colorindex});
1057 $mimgdata .= chr($alpha);
1061 } elseif ($pngcolortype === 0 ||
$pngcolortype == 2) { // generate Alpha channel values from tRNS
1062 // Get transparency as array of RGB
1063 $p = strpos($data, 'tRNS');
1066 $n = $this->fourBytesToInt(substr($data, ($p - 4), 4));
1067 $t = substr($data, ($p +
4), $n);
1068 if ($colspace == 'DeviceGray') { // ct===0
1069 $trns = [$this->translateValue(substr($t, 0, 2), $bpc)];
1070 } else /* $colspace=='DeviceRGB' */ { // ct==2
1072 $trns[0] = $this->translateValue(substr($t, 0, 2), $bpc);
1073 $trns[1] = $this->translateValue(substr($t, 2, 2), $bpc);
1074 $trns[2] = $this->translateValue(substr($t, 4, 2), $bpc);
1077 // generate alpha channel
1078 for ($ypx = 0; $ypx < $h; ++
$ypx) {
1079 for ($xpx = 0; $xpx < $w; ++
$xpx) {
1080 $rgb = imagecolorat($im, $xpx, $ypx);
1081 $r = ($rgb >> 16) & 0xFF;
1082 $g = ($rgb >> 8) & 0xFF;
1084 if ($colspace == 'DeviceGray' && $b == $trns[0]) {
1086 } elseif ($r == $trns[0] && $g == $trns[1] && $b == $trns[2]) {
1092 $mimgdata .= chr($alpha);
1097 for ($i = 0; $i < $h; $i++
) {
1098 for ($j = 0; $j < $w; $j++
) {
1099 $rgb = imagecolorat($im, $j, $i);
1100 $alpha = ($rgb & 0x7F000000) >> 24;
1102 $mimgdata .= chr(255 - ($alpha * 2));
1104 $mimgdata .= chr(0);
1111 // mPDF 6 Gamma correction
1112 if ($gamma_correction) {
1113 imagegammacorrect($im, $gamma_correction, 2.2);
1116 //Read transparency info
1119 if (!$this->mpdf
->PDFA
&& !$this->mpdf
->PDFX
&& !$mask) { // mPDF 6 added NOT mask
1120 $p = strpos($data, 'tRNS');
1122 $n = $this->fourBytesToInt(substr($data, ($p - 4), 4));
1123 $t = substr($data, ($p +
4), $n);
1124 if ($colspace == 'DeviceGray') { // ct===0
1125 $trns = [$this->translateValue(substr($t, 0, 2), $bpc)];
1126 } elseif ($colspace == 'DeviceRGB') { // ct==2
1127 $trns[0] = $this->translateValue(substr($t, 0, 2), $bpc);
1128 $trns[1] = $this->translateValue(substr($t, 2, 2), $bpc);
1129 $trns[2] = $this->translateValue(substr($t, 4, 2), $bpc);
1131 if ($targetcs == 'DeviceCMYK') {
1132 $col = $this->colorModeConverter
->rgb2cmyk([3, $trns[0], $trns[1], $trns[2]]);
1133 $c1 = intval($col[1] * 2.55);
1134 $c2 = intval($col[2] * 2.55);
1135 $c3 = intval($col[3] * 2.55);
1136 $c4 = intval($col[4] * 2.55);
1137 $trns = [$c1, $c2, $c3, $c4];
1138 } elseif ($targetcs == 'DeviceGray') {
1139 $c = intval(($trns[0] * .21) +
($trns[1] * .71) +
($trns[2] * .07));
1143 $pos = strpos($t, chr(0));
1145 $pal = imagecolorsforindex($im, $pos);
1149 $trns = [$r, $g, $b]; // ****
1151 if ($targetcs == 'DeviceCMYK') {
1152 $col = $this->colorModeConverter
->rgb2cmyk([3, $r, $g, $b]);
1153 $c1 = intval($col[1] * 2.55);
1154 $c2 = intval($col[2] * 2.55);
1155 $c3 = intval($col[3] * 2.55);
1156 $c4 = intval($col[4] * 2.55);
1157 $trns = [$c1, $c2, $c3, $c4];
1158 } elseif ($targetcs == 'DeviceGray') {
1159 $c = intval(($r * .21) +
($g * .71) +
($b * .07));
1167 for ($i = 0; $i < $h; $i++
) {
1168 for ($j = 0; $j < $w; $j++
) {
1169 $rgb = imagecolorat($im, $j, $i);
1170 $r = ($rgb >> 16) & 0xFF;
1171 $g = ($rgb >> 8) & 0xFF;
1173 if ($colspace == 'Indexed') {
1174 $pal = imagecolorsforindex($im, $rgb);
1180 if ($targetcs == 'DeviceCMYK') {
1181 $col = $this->colorModeConverter
->rgb2cmyk([3, $r, $g, $b]);
1182 $c1 = intval($col[1] * 2.55);
1183 $c2 = intval($col[2] * 2.55);
1184 $c3 = intval($col[3] * 2.55);
1185 $c4 = intval($col[4] * 2.55);
1187 // original pixel was not set as transparent but processed color does match
1188 if ($trnsrgb != [$r, $g, $b] && $trns == [$c1, $c2, $c3, $c4]) {
1196 $imgdata .= chr($c1) . chr($c2) . chr($c3) . chr($c4);
1197 } elseif ($targetcs == 'DeviceGray') {
1198 $c = intval(($r * .21) +
($g * .71) +
($b * .07));
1200 // original pixel was not set as transparent but processed color does match
1201 if ($trnsrgb != [$r, $g, $b] && $trns == [$c]) {
1209 $imgdata .= chr($c);
1210 } elseif ($targetcs == 'DeviceRGB') {
1211 $imgdata .= chr($r) . chr($g) . chr($b);
1216 if ($targetcs == 'DeviceGray') {
1218 } elseif ($targetcs == 'DeviceRGB') {
1220 } elseif ($targetcs == 'DeviceCMYK') {
1224 $imgdata = $this->gzCompress($imgdata);
1225 $info = ['w' => $w, 'h' => $h, 'cs' => $targetcs, 'bpc' => 8, 'f' => 'FlateDecode', 'data' => $imgdata, 'type' => 'png',
1226 'parms' => '/DecodeParms <</Colors ' . $ncols . ' /BitsPerComponent 8 /Columns ' . $w . '>>'];
1228 $info['set-dpi'] = $dpi;
1231 $mimgdata = $this->gzCompress($mimgdata);
1232 $minfo = ['w' => $w, 'h' => $h, 'cs' => 'DeviceGray', 'bpc' => 8, 'f' => 'FlateDecode', 'data' => $mimgdata, 'type' => 'png',
1233 'parms' => '/DecodeParms <</Colors ' . $ncols . ' /BitsPerComponent 8 /Columns ' . $w . '>>'];
1235 $minfo['set-dpi'] = $dpi;
1237 $tempfile = '_tempImgPNG' . md5($data) . random_int(1, 10000) . '.png';
1238 $imgmask = count($this->mpdf
->images
) +
1;
1239 $minfo['i'] = $imgmask;
1240 $this->mpdf
->images
[$tempfile] = $minfo;
1241 $info['masked'] = $imgmask;
1243 $info['trns'] = $trns;
1250 private function jpgHeaderFromString(&$data)
1253 $p +
= $this->twoBytesToInt(substr($data, $p, 2)); // Length of initial marker block
1254 $marker = substr($data, $p, 2);
1256 while ($marker != chr(255) . chr(192) && $marker != chr(255) . chr(194) && $p < strlen($data)) {
1257 // Start of frame marker (FFC0) or (FFC2) mPDF 4.4.004
1258 $p +
= ($this->twoBytesToInt(substr($data, $p +
2, 2))) +
2; // Length of marker block
1259 $marker = substr($data, $p, 2);
1262 if ($marker != chr(255) . chr(192) && $marker != chr(255) . chr(194)) {
1265 return substr($data, $p +
2, 10);
1268 private function jpgDataFromHeader($hdr)
1270 $bpc = ord(substr($hdr, 2, 1));
1276 $h = $this->twoBytesToInt(substr($hdr, 3, 2));
1277 $w = $this->twoBytesToInt(substr($hdr, 5, 2));
1279 $channels = ord(substr($hdr, 7, 1));
1281 if ($channels == 3) {
1282 $colspace = 'DeviceRGB';
1283 } elseif ($channels == 4) {
1284 $colspace = 'DeviceCMYK';
1286 $colspace = 'DeviceGray';
1289 return [$w, $h, $colspace, $bpc, $channels];
1293 * Corrects 2-byte integer to 8-bit depth value
1294 * If original image is bpc != 8, tRNS will be in this bpc
1295 * $im from imagecreatefromstring will always be in bpc=8
1296 * So why do we only need to correct 16-bit tRNS and NOT 2 or 4-bit???
1298 private function translateValue($s, $bpc)
1300 $n = $this->twoBytesToInt($s);
1306 //elseif ($bpc==4) { $n = ($n << 2); }
1307 //elseif ($bpc==2) { $n = ($n << 4); }
1313 * Read a 4-byte integer from string
1315 private function fourBytesToInt($s)
1317 return (ord($s[0]) << 24) +
(ord($s[1]) << 16) +
(ord($s[2]) << 8) +
ord($s[3]);
1321 * Equivalent to _get_ushort
1322 * Read a 2-byte integer from string
1324 private function twoBytesToInt($s)
1326 return (ord(substr($s, 0, 1)) << 8) +
ord(substr($s, 1, 1));
1329 private function gzCompress($data)
1331 if (!function_exists('gzcompress')) {
1332 throw new \Mpdf\
MpdfException('gzcompress is not available. install ext-zlib extension.');
1335 return gzcompress($data);
1339 * Throw an exception and save re-trying image URL's which have already failed
1341 private function imageError($file, $firsttime, $msg)
1343 $this->failedImages
[$file] = true;
1345 if ($firsttime && ($this->mpdf
->showImageErrors ||
$this->mpdf
->debug
)) {
1346 throw new \Mpdf\
MpdfImageException(sprintf('%s (%s)', $msg, $file));
1349 $this->logger
->warning(sprintf('%s (%s)', $msg, $file), ['context' => LogContext
::IMAGES
]);
1354 * @param string $url
1357 private function urldecodeParts($url)
1361 if (preg_match('/[?]/', $url)) {
1362 $bits = preg_split('/[?]/', $url, 2);
1364 $query = '?' . $bits[1];
1366 $file = rawurldecode($file);
1367 $query = urldecode($query);
1369 return $file . $query;