generated from jric11/baseProject
931 lines
33 KiB
PHP
931 lines
33 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Import.php
|
|
*
|
|
* @since 2011-05-23
|
|
* @category Library
|
|
* @package PdfImage
|
|
* @author Nicola Asuni <info@tecnick.com>
|
|
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
|
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
|
* @link https://github.com/tecnickcom/tc-lib-pdf-image
|
|
*
|
|
* This file is part of tc-lib-pdf-image software library.
|
|
*/
|
|
|
|
namespace Com\Tecnick\Pdf\Image;
|
|
|
|
use Com\Tecnick\Pdf\Image\Exception as ImageException;
|
|
use Com\Tecnick\Pdf\Image\Import\ImageImportInterface;
|
|
|
|
/**
|
|
* Com\Tecnick\Pdf\Image\Import
|
|
*
|
|
* @since 2011-05-23
|
|
* @category Library
|
|
* @package PdfImage
|
|
* @author Nicola Asuni <info@tecnick.com>
|
|
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
|
|
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
|
|
* @link https://github.com/tecnickcom/tc-lib-pdf-image
|
|
*
|
|
* @phpstan-type ImageBaseData array{
|
|
* 'bits': int,
|
|
* 'channels': int,
|
|
* 'colspace': string,
|
|
* 'data': string,
|
|
* 'exturl': bool,
|
|
* 'file': string,
|
|
* 'filter': string,
|
|
* 'height': int,
|
|
* 'icc': string,
|
|
* 'ismask': bool,
|
|
* 'key': string,
|
|
* 'mapto': int,
|
|
* 'native': bool,
|
|
* 'obj': int,
|
|
* 'obj_alt': int,
|
|
* 'obj_icc': int,
|
|
* 'obj_pal': int,
|
|
* 'pal': string,
|
|
* 'parms': string,
|
|
* 'raw': string,
|
|
* 'recode': bool,
|
|
* 'recoded': bool,
|
|
* 'splitalpha': bool,
|
|
* 'trns': array<int, int>,
|
|
* 'type': int,
|
|
* 'width': int,
|
|
* }
|
|
*
|
|
* @phpstan-type ImageRawData array{
|
|
* 'bits': int,
|
|
* 'channels': int,
|
|
* 'colspace': string,
|
|
* 'data': string,
|
|
* 'exturl': bool,
|
|
* 'file': string,
|
|
* 'filter': string,
|
|
* 'height': int,
|
|
* 'icc': string,
|
|
* 'ismask': bool,
|
|
* 'key': string,
|
|
* 'mapto': int,
|
|
* 'mask'?: ImageBaseData,
|
|
* 'native': bool,
|
|
* 'obj': int,
|
|
* 'obj_alt': int,
|
|
* 'obj_icc': int,
|
|
* 'obj_pal': int,
|
|
* 'out'?: true,
|
|
* 'pal': string,
|
|
* 'parms': string,
|
|
* 'plain'?: ImageBaseData,
|
|
* 'raw': string,
|
|
* 'recode': bool,
|
|
* 'recoded': bool,
|
|
* 'splitalpha': bool,
|
|
* 'trns': array<int, int>,
|
|
* 'type': int,
|
|
* 'width': int,
|
|
* }
|
|
*
|
|
* @SuppressWarnings("PHPMD.ExcessiveClassLength")
|
|
* @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
|
|
*/
|
|
class Import extends \Com\Tecnick\Pdf\Image\Output
|
|
{
|
|
/**
|
|
* Image index.
|
|
* Count the number of added images.
|
|
*/
|
|
protected int $iid = 0;
|
|
|
|
/**
|
|
* Namespace + schema version prefix for external cache keys.
|
|
* Bump the version when the ImageRawData shape changes so that stale
|
|
* persisted entries are ignored instead of being fed into output.
|
|
*/
|
|
private const CACHE_PREFIX = 'tc-lib-pdf-image:v2:';
|
|
|
|
/**
|
|
* Native image types and associated importing class.
|
|
* (Image types for which we have an import method).
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
private const NATIVE = [
|
|
IMAGETYPE_PNG => 'Png',
|
|
IMAGETYPE_JPEG => 'Jpeg',
|
|
];
|
|
|
|
/**
|
|
* Lossless image types.
|
|
*
|
|
* @var array<int>
|
|
*/
|
|
protected const LOSSLESS = [
|
|
IMAGETYPE_GIF, // 1
|
|
IMAGETYPE_PNG, // 3
|
|
IMAGETYPE_PSD, // 5
|
|
IMAGETYPE_BMP, // 6
|
|
IMAGETYPE_WBMP, // 15
|
|
IMAGETYPE_XBM, // 16
|
|
IMAGETYPE_TIFF_II, // 7
|
|
IMAGETYPE_TIFF_MM, // 8
|
|
IMAGETYPE_IFF, // 14
|
|
IMAGETYPE_SWC, // 13
|
|
IMAGETYPE_ICO, // 17
|
|
];
|
|
|
|
/**
|
|
* Map number of channels with color space name.
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
protected const COLSPACEMAP = [
|
|
1 => 'DeviceGray',
|
|
3 => 'DeviceRGB',
|
|
4 => 'DeviceCMYK',
|
|
];
|
|
|
|
/**
|
|
* Add a new image.
|
|
*
|
|
* @param string $image Image file name, URL or a '@' character followed by the image data string.
|
|
* To link an image without embedding it on the document, set an asterisk
|
|
* character before the URL (i.e.: '*http://www.example.com/image.jpg').
|
|
* @param ?int $width New width in pixels or null to keep the original value.
|
|
* @param ?int $height New height in pixels or null to keep the original value.
|
|
* @param bool $ismask True if the image is a transparency mask.
|
|
* @param int $quality Quality for JPEG files (0 = max compression; 100 = best quality, bigger file).
|
|
* @param bool $defprint Indicate if the image is the default
|
|
* for printing when used as alternative image.
|
|
* @param array<int, int> $altimgs Arrays of alternate image keys.
|
|
*
|
|
* @return int Image ID.
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the image cannot be added.
|
|
* @throws \Com\Tecnick\File\Exception If reading image content fails.
|
|
*/
|
|
public function add(
|
|
string $image,
|
|
?int $width = null,
|
|
?int $height = null,
|
|
bool $ismask = false,
|
|
int $quality = 100,
|
|
bool $defprint = false,
|
|
array $altimgs = [],
|
|
): int {
|
|
$data = $this->import($image, $width, $height, $ismask, $quality);
|
|
++$this->iid;
|
|
$this->image[$this->iid] = [
|
|
'iid' => $this->iid,
|
|
'key' => $data['key'],
|
|
'width' => $data['width'],
|
|
'height' => $data['height'],
|
|
'defprint' => $defprint,
|
|
'altimgs' => $altimgs,
|
|
];
|
|
return $this->iid;
|
|
}
|
|
|
|
/**
|
|
* Get the Image key used for caching.
|
|
*
|
|
* @param string $image Image file name or content.
|
|
* @param int $width Width in pixels.
|
|
* @param int $height Height in pixels.
|
|
* @param int $quality Quality for JPEG files.
|
|
*/
|
|
public function getKey(string $image, int $width = 0, int $height = 0, int $quality = 100): string
|
|
{
|
|
// The parts are joined with a delimiter so that distinct inputs cannot
|
|
// collapse into the same digest (e.g. ('img', 12, 3) vs ('img1', 2, 3)).
|
|
$seed = $image . '|' . $width . '|' . $height . '|' . $quality;
|
|
return \strtr(\rtrim(\base64_encode(\pack('H*', \md5($seed))), '='), '+/', '-_');
|
|
}
|
|
|
|
/**
|
|
* Get an imported image by key.
|
|
*
|
|
* @param string $key Image key.
|
|
*
|
|
* @return ImageRawData Image raw data array.
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the key is not found.
|
|
*/
|
|
public function getImageDataByKey(string $key): array
|
|
{
|
|
$cache = $this->cache[$key] ?? null;
|
|
if ($cache === null) {
|
|
throw new ImageException('Unknown key');
|
|
}
|
|
|
|
return $cache;
|
|
}
|
|
|
|
/**
|
|
* Resolve the image dimensions for a cached image.
|
|
*
|
|
* - If both $width and $height are zero, the original image dimensions are returned.
|
|
* - If only one of $width and $height is greater than zero, the other is
|
|
* calculated preserving the original aspect ratio.
|
|
* - If both are greater than zero, they are returned as-is, unless $fit is
|
|
* true, in which case the image is scaled to fit within the $width x $height
|
|
* bounding box preserving the original aspect ratio.
|
|
*
|
|
* @param string $key Image key.
|
|
* @param int $width Target width in pixels (0 = unspecified).
|
|
* @param int $height Target height in pixels (0 = unspecified).
|
|
* @param bool $fit If true and both sides are specified, scale to fit
|
|
* within the bounding box preserving the aspect ratio.
|
|
*
|
|
* @return array{width: int, height: int} Image dimensions in pixels.
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the key is not found.
|
|
*/
|
|
public function getImageDimensionsByKey(string $key, int $width = 0, int $height = 0, bool $fit = false): array
|
|
{
|
|
$data = $this->getImageDataByKey($key);
|
|
|
|
[$width, $height] = $this->scaleToAspect(
|
|
$data['width'],
|
|
$data['height'],
|
|
$width > 0 ? $width : null,
|
|
$height > 0 ? $height : null,
|
|
$fit,
|
|
);
|
|
|
|
return [
|
|
'width' => $width,
|
|
'height' => $height,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Import the original image raw data.
|
|
*
|
|
* @param string $image Image file name, URL or a '@' character followed by the image data string.
|
|
* To link an image without embedding it on the document, set an asterisk
|
|
* character before the URL (i.e.: '*http://www.example.com/image.jpg').
|
|
* @param ?int $width New width in pixels or null to keep the original value.
|
|
* @param ?int $height New height in pixels or null to keep the original value.
|
|
* @param bool $ismask True if the image is a transparency mask.
|
|
* @param int $quality Quality for JPEG files (0 = max compression; 100 = best quality, bigger file).
|
|
*
|
|
* @return ImageRawData Image raw data array
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the image cannot be imported.
|
|
* @throws \Com\Tecnick\File\Exception If reading image content fails.
|
|
*/
|
|
protected function import(
|
|
string $image,
|
|
?int $width = null,
|
|
?int $height = null,
|
|
bool $ismask = false,
|
|
int $quality = 100,
|
|
): array {
|
|
$quality = \max(0, \min(100, $quality));
|
|
$imgkey = $this->getKey($image, (int) $width, (int) $height, $quality);
|
|
|
|
// Tier 1: in-process memory cache (fast path, also dedups within a document).
|
|
if (isset($this->cache[$imgkey])) {
|
|
return $this->cache[$imgkey];
|
|
}
|
|
|
|
// Tier 2: optional external cache (survives across instances and processes).
|
|
$cache = $this->imageCache;
|
|
$extkey = '';
|
|
if ($cache !== null) {
|
|
$extkey = $this->getExternalCacheKey($image, $imgkey, (int) $width, (int) $height, $quality);
|
|
$cached = $cache->get($extkey);
|
|
if ($cached !== null) {
|
|
$cached['key'] = $imgkey;
|
|
$this->cache[$imgkey] = $cached;
|
|
return $cached;
|
|
}
|
|
}
|
|
|
|
$data = $this->getRawData($image);
|
|
$data['key'] = $imgkey;
|
|
|
|
[$width, $height] = $this->resolveDimensions($data, $width, $height);
|
|
|
|
if (!$data['native'] || $width !== $data['width'] || $height !== $data['height']) {
|
|
$data = $this->getResizedRawData($this->getBaseData($data), $width, $height, true, $quality);
|
|
}
|
|
|
|
$data = $this->getData($data, $width, $height, $quality);
|
|
|
|
$data = $this->enrichMaskData($data, $width, $height, $quality, $ismask);
|
|
|
|
// store data in the in-process cache
|
|
$this->cache[$imgkey] = $data;
|
|
|
|
// write-through to the external cache (best-effort, persistable snapshot)
|
|
if ($cache !== null) {
|
|
$cache->set($extkey, $this->getPersistableData($data));
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Build the persistent (external) cache key for an image.
|
|
*
|
|
* For local file inputs the file modification time and size are folded into
|
|
* the key, so that editing a file in place invalidates stale persisted
|
|
* entries. Inline data ('@...') and remote URLs are identified by their
|
|
* string, which already carries their content/identity.
|
|
*
|
|
* @param string $image Image file name, URL or '@'-prefixed data string.
|
|
* @param string $imgkey Image key already computed from the raw image string.
|
|
* @param int $width Width in pixels.
|
|
* @param int $height Height in pixels.
|
|
* @param int $quality Quality for JPEG files.
|
|
*/
|
|
private function getExternalCacheKey(string $image, string $imgkey, int $width, int $height, int $quality): string
|
|
{
|
|
$identity = $image;
|
|
|
|
if ($image !== '' && $image[0] !== '@') {
|
|
$path = $image[0] === '*' ? \substr($image, 1) : $image;
|
|
if (\is_file($path)) {
|
|
$mtime = \filemtime($path);
|
|
$size = \filesize($path);
|
|
if ($mtime !== false && $size !== false) {
|
|
$identity = $image . ':' . $mtime . ':' . $size;
|
|
}
|
|
}
|
|
}
|
|
|
|
// When the identity is just the raw image string the key is identical
|
|
// to the one already computed by the caller, so reuse it and avoid a
|
|
// second md5/base64 round.
|
|
$key = $identity === $image ? $imgkey : $this->getKey($identity, $width, $height, $quality);
|
|
|
|
return self::CACHE_PREFIX . $key;
|
|
}
|
|
|
|
/**
|
|
* Produce a persistable snapshot of the imported image data.
|
|
*
|
|
* Drops the original raw bytes (not used at output time) to shrink the
|
|
* stored payload. PDF object numbers and the output flag are intentionally
|
|
* left as imported (all zero / unset), so a loaded entry is ready for a
|
|
* fresh document without any reset.
|
|
*
|
|
* @param ImageRawData $data Imported image data.
|
|
*
|
|
* @return ImageRawData Persistable image data.
|
|
*/
|
|
private function getPersistableData(array $data): array
|
|
{
|
|
$data['raw'] = '';
|
|
|
|
if (isset($data['mask'])) {
|
|
$data['mask']['raw'] = '';
|
|
}
|
|
|
|
if (isset($data['plain'])) {
|
|
$data['plain']['raw'] = '';
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Resolve target image dimensions preserving aspect ratio when one side is omitted.
|
|
*
|
|
* @param ImageRawData $data Image raw data.
|
|
* @param ?int $width Target width in pixels (null = unspecified, derive from aspect ratio).
|
|
* @param ?int $height Target height in pixels (null = unspecified, derive from aspect ratio).
|
|
*
|
|
* @return array{0: int, 1: int}
|
|
*/
|
|
private function resolveDimensions(array $data, ?int $width, ?int $height): array
|
|
{
|
|
return $this->scaleToAspect($data['width'], $data['height'], $width, $height);
|
|
}
|
|
|
|
/**
|
|
* Scale target dimensions against a source size, preserving the aspect ratio.
|
|
*
|
|
* - If both $width and $height are null (unspecified), the source dimensions
|
|
* are returned.
|
|
* - If only one side is specified, the other is derived from the source aspect
|
|
* ratio.
|
|
* - If both sides are specified, they are returned as-is, unless $fit is true,
|
|
* in which case the result is scaled to fit within the $width x $height box
|
|
* while preserving the source aspect ratio.
|
|
*
|
|
* An explicit zero (as opposed to null) is kept as-is so that callers can
|
|
* detect and reject invalid target dimensions downstream.
|
|
*
|
|
* @param int $srcwidth Source width in pixels.
|
|
* @param int $srcheight Source height in pixels.
|
|
* @param ?int $width Target width in pixels (null = unspecified).
|
|
* @param ?int $height Target height in pixels (null = unspecified).
|
|
* @param bool $fit Scale to fit within the box when both sides are given.
|
|
*
|
|
* @return array{0: int, 1: int}
|
|
*/
|
|
private function scaleToAspect(int $srcwidth, int $srcheight, ?int $width, ?int $height, bool $fit = false): array
|
|
{
|
|
if ($width === null && $height === null) {
|
|
return [\max(0, $srcwidth), \max(0, $srcheight)];
|
|
}
|
|
|
|
if ($fit && $width !== null && $width > 0 && $height !== null && $height > 0) {
|
|
return $this->fitToBox($srcwidth, $srcheight, $width, $height);
|
|
}
|
|
|
|
if ($width !== null && $height === null && $srcwidth > 0) {
|
|
$height = $this->scaleSide($width, $srcheight, $srcwidth);
|
|
}
|
|
|
|
if ($height !== null && $width === null && $srcheight > 0) {
|
|
$width = $this->scaleSide($height, $srcwidth, $srcheight);
|
|
}
|
|
|
|
return [\max(0, $width ?? $srcwidth), \max(0, $height ?? $srcheight)];
|
|
}
|
|
|
|
/**
|
|
* Scale a single dimension proportionally: round($value * $numerator / $denominator).
|
|
*
|
|
* @param int $value The known side to scale from (e.g. the specified width or height).
|
|
* @param int $numerator Source size of the side being computed.
|
|
* @param int $denominator Source size of the side $value corresponds to.
|
|
*
|
|
* @return int Scaled size of the computed side, in pixels.
|
|
*/
|
|
private function scaleSide(int $value, int $numerator, int $denominator): int
|
|
{
|
|
return (int) \round(((float) $value * (float) $numerator) / (float) $denominator);
|
|
}
|
|
|
|
/**
|
|
* Scale the source size to fit within the $width x $height box, preserving the aspect ratio.
|
|
*
|
|
* @param int $srcwidth Source width in pixels.
|
|
* @param int $srcheight Source height in pixels.
|
|
* @param int $width Bounding box width in pixels.
|
|
* @param int $height Bounding box height in pixels.
|
|
*
|
|
* @return array{0: int, 1: int}
|
|
*/
|
|
private function fitToBox(int $srcwidth, int $srcheight, int $width, int $height): array
|
|
{
|
|
if ($srcwidth <= 0 || $srcheight <= 0) {
|
|
return [\max(0, $width), \max(0, $height)];
|
|
}
|
|
|
|
$scale = \min((float) $width / (float) $srcwidth, (float) $height / (float) $srcheight);
|
|
return [
|
|
\max(0, (int) \round((float) $srcwidth * $scale)),
|
|
\max(0, (int) \round((float) $srcheight * $scale)),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Add plain/mask variants when required by mask mode or alpha splitting.
|
|
*
|
|
* @param ImageRawData $data Image raw data.
|
|
* @param int $width Width in pixels.
|
|
* @param int $height Height in pixels.
|
|
* @param int $quality Quality for JPEG files.
|
|
* @param bool $ismask True if the image is a transparency mask.
|
|
*
|
|
* @return ImageRawData
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the image cannot be imported.
|
|
*/
|
|
private function enrichMaskData(array $data, int $width, int $height, int $quality, bool $ismask): array
|
|
{
|
|
$basedata = $this->getBaseData($data);
|
|
|
|
if ($ismask) {
|
|
$data['mask'] = $basedata;
|
|
return $data;
|
|
}
|
|
|
|
if (!$data['splitalpha']) {
|
|
return $data;
|
|
}
|
|
|
|
// create 2 separate images: plain + mask
|
|
$data['plain'] = $this->createPlainImage($basedata, $width, $height, $quality);
|
|
$data['mask'] = $this->createMaskImage($basedata, $width, $height, $quality);
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* @param ImageBaseData $basedata Image base data.
|
|
* @param int $width Width in pixels.
|
|
* @param int $height Height in pixels.
|
|
* @param int $quality Quality for JPEG files.
|
|
*
|
|
* @return ImageBaseData
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the image cannot be imported.
|
|
*/
|
|
private function createPlainImage(array $basedata, int $width, int $height, int $quality): array
|
|
{
|
|
$plain = $this->getResizedRawData($basedata, $width, $height, false, $quality);
|
|
$plain = $this->getData($plain, $width, $height, $quality);
|
|
|
|
return $this->getBaseData($plain);
|
|
}
|
|
|
|
/**
|
|
* @param ImageBaseData $basedata Image base data.
|
|
* @param int $width Width in pixels.
|
|
* @param int $height Height in pixels.
|
|
* @param int $quality Quality for JPEG files.
|
|
*
|
|
* @return ImageBaseData
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the image cannot be imported.
|
|
*/
|
|
private function createMaskImage(array $basedata, int $width, int $height, int $quality): array
|
|
{
|
|
$mask = $this->getAlphaChannelRawData($basedata);
|
|
$mask = $this->getData($mask, $width, $height, $quality);
|
|
$mask = $this->getBaseData($mask);
|
|
$mask['colspace'] = 'DeviceGray';
|
|
|
|
return $mask;
|
|
}
|
|
|
|
/**
|
|
* Extract the relevant data from the image.
|
|
*
|
|
* @param ImageRawData $data Image raw data.
|
|
* @param int $width Width in pixels.
|
|
* @param int $height Height in pixels.
|
|
* @param int $quality Quality for JPEG files.
|
|
*
|
|
* @return ImageRawData Image raw data array.
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the image cannot be imported.
|
|
*/
|
|
protected function getData(array $data, int $width, int $height, int $quality): array
|
|
{
|
|
if (!$data['native']) {
|
|
throw new ImageException('Unable to import image');
|
|
}
|
|
|
|
$imageImport = $this->createImportImage($data);
|
|
$data = $imageImport->getData($this->getBaseData($data));
|
|
|
|
if ($data['recode']) {
|
|
// re-encode the image as it was not possible to decode it
|
|
$data = $this->getResizedRawData($this->getBaseData($data), $width, $height, true, $quality);
|
|
$data = $imageImport->getData($this->getBaseData($data));
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* @param array{'type': int}|ImageRawData $data Image raw data.
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the image type is unknown.
|
|
*/
|
|
private function createImportImage(array $data): ImageImportInterface
|
|
{
|
|
if (!isset(self::NATIVE[$data['type']])) {
|
|
throw new ImageException('Unable to import image: unknown type');
|
|
}
|
|
|
|
return match (self::NATIVE[$data['type']]) {
|
|
'Png' => new \Com\Tecnick\Pdf\Image\Import\Png(),
|
|
'Jpeg' => new \Com\Tecnick\Pdf\Image\Import\Jpeg(),
|
|
default => throw new ImageException('Unable to import image: unknown type'),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Normalize raw image data to its base shape.
|
|
*
|
|
* @param ImageRawData $data Image raw data.
|
|
*
|
|
* @return ImageBaseData Image base data.
|
|
*/
|
|
private function getBaseData(array $data): array
|
|
{
|
|
return [
|
|
'bits' => $data['bits'],
|
|
'channels' => $data['channels'],
|
|
'colspace' => $data['colspace'],
|
|
'data' => $data['data'],
|
|
'exturl' => $data['exturl'],
|
|
'file' => $data['file'],
|
|
'filter' => $data['filter'],
|
|
'height' => $data['height'],
|
|
'icc' => $data['icc'],
|
|
'ismask' => $data['ismask'],
|
|
'key' => $data['key'],
|
|
'mapto' => $data['mapto'],
|
|
'native' => $data['native'],
|
|
'obj' => $data['obj'],
|
|
'obj_alt' => $data['obj_alt'],
|
|
'obj_icc' => $data['obj_icc'],
|
|
'obj_pal' => $data['obj_pal'],
|
|
'pal' => $data['pal'],
|
|
'parms' => $data['parms'],
|
|
'raw' => $data['raw'],
|
|
'recode' => $data['recode'],
|
|
'recoded' => $data['recoded'],
|
|
'splitalpha' => $data['splitalpha'],
|
|
'trns' => $data['trns'],
|
|
'type' => $data['type'],
|
|
'width' => $data['width'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get the original image raw data.
|
|
*
|
|
* @param string $image Image file name, URL or a '@' character followed by the image data string.
|
|
* To link an image without embedding it on the document, set an asterisk character
|
|
* before the URL (i.e.: '*http://www.example.com/image.jpg').
|
|
*
|
|
* @return ImageRawData Image data array.
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the image cannot be read.
|
|
* @throws \Com\Tecnick\File\Exception If reading image content fails.
|
|
*/
|
|
protected function getRawData(string $image): array
|
|
{
|
|
// default data to return
|
|
$data = [
|
|
'bits' => 8, // number of bits per channel
|
|
'channels' => 3, // number of channels
|
|
'colspace' => 'DeviceRGB', // color space
|
|
'data' => '', // PDF image data
|
|
'exturl' => false, // true if the image is an exernal URL that should not be embedded
|
|
'file' => '', // source file name or URL
|
|
'filter' => 'FlateDecode', // decoding filter
|
|
'height' => 0, // image height in pixels
|
|
'icc' => '', // ICC profile
|
|
'ismask' => false, // true if the image is a transparency mask
|
|
'key' => '', // image key
|
|
'mapto' => IMAGETYPE_PNG, // type to convert to
|
|
'native' => false, // true if the image is PNG or JPEG
|
|
'obj' => 0, // PDF object number
|
|
'obj_alt' => 0,
|
|
'obj_icc' => 0,
|
|
'obj_pal' => 0,
|
|
'pal' => '', // colour palette
|
|
'parms' => '', // additional PDF decoding parameters
|
|
'raw' => '', // raw image data
|
|
'recode' => false,
|
|
'recoded' => false,
|
|
'splitalpha' => false,
|
|
'trns' => [], // colour key masking
|
|
'type' => 0, // image type constant: IMAGETYPE_XXX
|
|
'width' => 0, // image width in pixels
|
|
];
|
|
|
|
if ($image === '' || ($image[0] === '@' || $image[0] === '*') && \strlen($image) === 1) {
|
|
throw new ImageException('Empty image');
|
|
}
|
|
|
|
if ($image[0] === '@') { // image from string
|
|
$data['raw'] = \substr($image, 1);
|
|
return $this->getMetaData($data);
|
|
}
|
|
|
|
if ($image[0] === '*') { // not-embedded external URL
|
|
$data['exturl'] = true;
|
|
$image = \substr($image, 1);
|
|
}
|
|
|
|
$data['file'] = $image;
|
|
$raw = $this->fileHelper->getFileData($image);
|
|
if ($raw === false) {
|
|
throw new ImageException('Unable to read image file: ' . $image);
|
|
}
|
|
|
|
$data['raw'] = $raw;
|
|
|
|
return $this->getMetaData($data);
|
|
}
|
|
|
|
/**
|
|
* Get the image meta data.
|
|
*
|
|
* @param ImageBaseData $data Image raw data.
|
|
*
|
|
* @return ImageRawData Image raw data array.
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the image format is invalid.
|
|
*/
|
|
protected function getMetaData(array $data): array
|
|
{
|
|
// getimagesizefromstring() does not throw: it emits warnings (silenced
|
|
// below) and returns false on failure, which is handled right after.
|
|
\set_error_handler(static fn(): bool => true);
|
|
try {
|
|
$meta = \getimagesizefromstring($data['raw']);
|
|
} finally {
|
|
\restore_error_handler();
|
|
}
|
|
|
|
if ($meta === false) {
|
|
throw new ImageException('Invalid image format');
|
|
}
|
|
|
|
$data['width'] = $meta[0];
|
|
$data['height'] = $meta[1];
|
|
$data['type'] = $meta[2];
|
|
$data['native'] = isset(self::NATIVE[$data['type']]);
|
|
$data['mapto'] = \in_array($data['type'], self::LOSSLESS, true) ? IMAGETYPE_PNG : IMAGETYPE_JPEG;
|
|
/** @var array{'bits'?: int, 'channels'?: int} $meta */
|
|
if (isset($meta['bits'])) {
|
|
$data['bits'] = $meta['bits'];
|
|
}
|
|
|
|
if (isset($meta['channels']) && $meta['channels'] !== 0) {
|
|
$data['channels'] = (int) $meta['channels'];
|
|
}
|
|
|
|
if (isset(self::COLSPACEMAP[$data['channels']])) {
|
|
$data['colspace'] = self::COLSPACEMAP[$data['channels']];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Get the resized image raw data
|
|
* (always convert the image type to a native format: PNG or JPEG).
|
|
*
|
|
* @param ImageBaseData $data Image raw data as returned by getRawData.
|
|
* @param int $width New width in pixels.
|
|
* @param int $height New height in pixels.
|
|
* @param bool $alpha If true save the alpha channel information,
|
|
* if false merge the alpha channel (PNG mode).
|
|
* @param int $quality Quality for JPEG files
|
|
* (0 = max compression; 100 = best quality, bigger file).
|
|
*
|
|
* @return ImageRawData Image raw data array.
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the image cannot be resized.
|
|
* @SuppressWarnings("PHPMD.CyclomaticComplexity")
|
|
* @SuppressWarnings("PHPMD.NPathComplexity")
|
|
*/
|
|
protected function getResizedRawData(
|
|
array $data,
|
|
int $width,
|
|
int $height,
|
|
bool $alpha = true,
|
|
int $quality = 100,
|
|
): array {
|
|
if ($width <= 0 || $height <= 0) {
|
|
throw new ImageException('Image width and/or height are empty');
|
|
}
|
|
|
|
$img = \imagecreatefromstring($data['raw']);
|
|
if ($img === false) {
|
|
throw new ImageException('Unable to create new image from string');
|
|
}
|
|
|
|
$newimg = \imagecreatetruecolor($width, $height);
|
|
if ($newimg === false) {
|
|
throw new ImageException('Unable to create new resized image');
|
|
}
|
|
|
|
\imageinterlace($newimg, false);
|
|
|
|
if ($alpha) {
|
|
$trid = \imagecolorallocate($newimg, 0, 0, 0);
|
|
if ($trid === false) {
|
|
throw new ImageException('Unable to allocate alpha color for transparency');
|
|
}
|
|
\imagecolortransparent($newimg, $trid);
|
|
} else {
|
|
$tid = \imagecolortransparent($img);
|
|
$palsize = \imagecolorstotal($img);
|
|
if ($tid >= 0 && $palsize > 0 && $tid < $palsize) {
|
|
// set transparency for Indexed image
|
|
/** @var array{'red': int, 'green': int, 'blue': int, 'alpha': int} $tcol */
|
|
$tcol = \imagecolorsforindex($img, $tid);
|
|
$trid = \imagecolorallocate($newimg, $tcol['red'], $tcol['green'], $tcol['blue']);
|
|
if ($trid === false) {
|
|
throw new ImageException('Unable to allocate color for transparency');
|
|
}
|
|
\imagefill($newimg, 0, 0, $trid);
|
|
\imagecolortransparent($newimg, $trid);
|
|
}
|
|
}
|
|
|
|
\imagealphablending($newimg, !$alpha);
|
|
\imagesavealpha($newimg, $alpha);
|
|
|
|
\imagecopyresampled($newimg, $img, 0, 0, 0, 0, $width, $height, $data['width'], $data['height']);
|
|
|
|
\ob_start();
|
|
if ($data['mapto'] === IMAGETYPE_PNG) {
|
|
\imagepng($newimg, null, 9, PNG_ALL_FILTERS);
|
|
} else {
|
|
\imagejpeg($newimg, null, $quality);
|
|
}
|
|
$ogc = \ob_get_clean();
|
|
if ($ogc === false) {
|
|
throw new ImageException('Unable to extract alpha channel');
|
|
}
|
|
|
|
$data['raw'] = $ogc;
|
|
$data['exturl'] = false;
|
|
$data['recoded'] = true;
|
|
return $this->getMetaData($data);
|
|
}
|
|
|
|
/**
|
|
* Extract the alpha channel as separate image to be used as a mask.
|
|
*
|
|
* @param ImageBaseData $data Image raw data as returned by getRawData.
|
|
*
|
|
* @return ImageRawData Image raw data array.
|
|
*
|
|
* @throws \Com\Tecnick\Pdf\Image\Exception If the alpha channel cannot be extracted.
|
|
*/
|
|
protected function getAlphaChannelRawData(array $data): array
|
|
{
|
|
$img = \imagecreatefromstring($data['raw']);
|
|
if ($img === false) {
|
|
throw new ImageException('Unable to create alpha channel image from string');
|
|
}
|
|
|
|
$newimg = \imagecreate(\max(1, $data['width']), \max(1, $data['height']));
|
|
if ($newimg === false) {
|
|
throw new ImageException('Unable to create new empty alpha channel image');
|
|
}
|
|
|
|
\imageinterlace($newimg, false);
|
|
|
|
// generate gray scale palette (0 -> 255)
|
|
for ($col = 0; $col < 256; ++$col) {
|
|
\imagecolorallocate($newimg, $col, $col, $col);
|
|
}
|
|
|
|
// Precompute the gamma-corrected output for each of the 128 possible
|
|
// GD alpha values (7-bit: 0 -> 127), so the expensive pow() runs 128
|
|
// times instead of once per pixel.
|
|
$alphamap = [];
|
|
for ($alf = 0; $alf < 128; ++$alf) {
|
|
// GD alpha is only 7 bit (0 -> 127); 2.2 is the gamma value
|
|
$alphamap[$alf] = (int) ((((float) (127 - $alf) / 127) ** 2.2) * 255);
|
|
}
|
|
|
|
$truecolor = \imageistruecolor($img);
|
|
|
|
// extract alpha channel
|
|
for ($xpx = 0; $xpx < $data['width']; ++$xpx) {
|
|
for ($ypx = 0; $ypx < $data['height']; ++$ypx) {
|
|
$colindex = \imagecolorat($img, $xpx, $ypx);
|
|
if ($colindex === false) {
|
|
throw new ImageException('Unable to extract alpha channel color index');
|
|
}
|
|
|
|
// get and correct gamma color (keys 0-127 are always present)
|
|
if ($truecolor) {
|
|
// For truecolor images the alpha is packed in bits 24-30 of
|
|
// the pixel value, so it can be read directly instead of
|
|
// allocating a colour-component array per pixel.
|
|
$alpha = $alphamap[($colindex >> 24) & 0x7F] ?? 0;
|
|
} else {
|
|
/** @var array{'red': int, 'green': int, 'blue': int, 'alpha': int} $color */
|
|
$color = \imagecolorsforindex($img, $colindex);
|
|
$alpha = $alphamap[$color['alpha']] ?? 0;
|
|
}
|
|
|
|
\imagesetpixel($newimg, $xpx, $ypx, $alpha);
|
|
}
|
|
}
|
|
|
|
\ob_start();
|
|
\imagepng($newimg, null, 9, PNG_ALL_FILTERS);
|
|
$ogc = \ob_get_clean();
|
|
if ($ogc === false) {
|
|
throw new ImageException('Unable to extract alpha channel');
|
|
}
|
|
|
|
$data['raw'] = $ogc;
|
|
$data['channels'] = 1;
|
|
$data['colspace'] = 'DeviceGray';
|
|
$data['exturl'] = false;
|
|
$data['recoded'] = true;
|
|
$data['ismask'] = true;
|
|
return $this->getMetaData($data);
|
|
}
|
|
}
|