generated from jric11/baseProject
Initial commit
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Exception.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;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Image\Exception
|
||||
*
|
||||
* Custom Exception class
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
class Exception extends \Exception {}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* ImageCacheInterface.php
|
||||
*
|
||||
* @since 2026-06-16
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Image\ImageCacheInterface
|
||||
*
|
||||
* Optional external cache used to persist processed image data across
|
||||
* Import instances and PHP processes, avoiding recomputation of images that
|
||||
* have already been imported, resized and encoded.
|
||||
*
|
||||
* Implementations are a thin bridge to any backend (filesystem, APCu, Redis,
|
||||
* a PSR-16 cache, ...). They only need to store and retrieve plain arrays;
|
||||
* (de)serialization and eviction are the backend's responsibility.
|
||||
*
|
||||
* The cached value is the import-time snapshot of an image: it never contains
|
||||
* PDF object numbers (those are assigned per document at output time). Both
|
||||
* methods MUST be best-effort and MUST NOT throw on a backend miss or
|
||||
* transient backend failure, so that a cache problem never breaks PDF
|
||||
* generation.
|
||||
*
|
||||
* Security: the cache store is a trust boundary. The stored arrays (image
|
||||
* data, palette and ICC bytes) are embedded verbatim into generated PDFs, so
|
||||
* anyone able to write to the backend can influence document output. Use a
|
||||
* store only your application can write to, and when an implementation
|
||||
* deserializes data it MUST disable object restoration
|
||||
* (e.g. unserialize($s, ['allowed_classes' => false])).
|
||||
*
|
||||
* @since 2026-06-16
|
||||
* @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-import-type ImageRawData from \Com\Tecnick\Pdf\Image\Import
|
||||
*/
|
||||
interface ImageCacheInterface
|
||||
{
|
||||
/**
|
||||
* Retrieve a previously stored image data array.
|
||||
*
|
||||
* @param string $key Image cache key.
|
||||
*
|
||||
* @return ImageRawData|null Stored image data array, or null on a miss.
|
||||
*/
|
||||
public function get(string $key): ?array;
|
||||
|
||||
/**
|
||||
* Store an image data array.
|
||||
*
|
||||
* @param string $key Image cache key.
|
||||
* @param ImageRawData $data Image data array to store.
|
||||
*/
|
||||
public function set(string $key, array $data): void;
|
||||
}
|
||||
+930
@@ -0,0 +1,930 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* ImageImportInterface.php
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package PdfImage
|
||||
* @author jmleroux <jmleroux.pro@gmail.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\Import;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Image\Import\ImageImportInterface
|
||||
*
|
||||
* @since 2011-05-23
|
||||
* @category Library
|
||||
* @package PdfImage
|
||||
* @author jmleroux <jmleroux.pro@gmail.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-import-type ImageBaseData from \Com\Tecnick\Pdf\Image\Import
|
||||
*/
|
||||
interface ImageImportInterface
|
||||
{
|
||||
/**
|
||||
* Extract data from an image.
|
||||
*
|
||||
* @param ImageBaseData $data Image raw data.
|
||||
*
|
||||
* @return ImageBaseData Image raw data array.
|
||||
*/
|
||||
public function getData(array $data): array;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Jpeg.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\Import;
|
||||
|
||||
use Com\Tecnick\File\Byte;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Image\Import\Jpeg
|
||||
*
|
||||
* @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-import-type ImageBaseData from \Com\Tecnick\Pdf\Image\Import
|
||||
*/
|
||||
class Jpeg implements ImageImportInterface
|
||||
{
|
||||
/**
|
||||
* Extract data from a JPEG image.
|
||||
*
|
||||
* @param ImageBaseData $data Image raw data.
|
||||
*
|
||||
* @return ImageBaseData Image raw data array.
|
||||
*
|
||||
* @throws \RangeException If the byte offset is out of range.
|
||||
*/
|
||||
public function getData(array $data): array
|
||||
{
|
||||
$data['filter'] = 'DCTDecode';
|
||||
$data['data'] = $data['raw'];
|
||||
$byte = new Byte($data['raw']);
|
||||
// extract embedded ICC profile (if any)
|
||||
$icc = [];
|
||||
$offset = 0;
|
||||
// Length in bytes of the "ICC_PROFILE" marker string, used as the
|
||||
// minimum forward step so a malformed segment can never stall the scan.
|
||||
$markerlen = 11;
|
||||
while (($pos = \strpos($data['raw'], 'ICC_PROFILE', $offset)) !== false) {
|
||||
if ($pos < 2) {
|
||||
// Not enough bytes before the marker to read the APP2 length.
|
||||
$offset = $pos + $markerlen;
|
||||
continue;
|
||||
}
|
||||
|
||||
// get ICC sequence length
|
||||
$length = $byte->getUShort($pos - 2) - 16;
|
||||
if ($length <= 0) {
|
||||
// Malformed or spurious marker: skip past it so $offset always
|
||||
// advances and the loop cannot get stuck re-finding this match.
|
||||
$offset = $pos + $markerlen;
|
||||
continue;
|
||||
}
|
||||
|
||||
// marker sequence number
|
||||
$msn = \max(1, \ord($data['raw'][$pos + 12] ?? "\x00"));
|
||||
// number of markers (total of APP2 used)
|
||||
//$nom = \max(1, \ord($data['raw'][($pos + 13)]));
|
||||
// get sequence segment
|
||||
$icc[$msn - 1] = \substr($data['raw'], $pos + 14, $length);
|
||||
// move forward to next sequence
|
||||
$offset = $pos + 14 + $length;
|
||||
}
|
||||
|
||||
// order and compact ICC segments
|
||||
if ($icc !== []) {
|
||||
\ksort($icc);
|
||||
$icc = \implode('', $icc);
|
||||
if (\substr($icc, 36, 4) === 'acsp') {
|
||||
// valid ICC profile
|
||||
$data['icc'] = $icc;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Png.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\Import;
|
||||
|
||||
use Com\Tecnick\File\Byte;
|
||||
use Com\Tecnick\Pdf\Image\Exception as ImageException;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Image\Import\Png
|
||||
*
|
||||
* @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-import-type ImageBaseData from \Com\Tecnick\Pdf\Image\Import
|
||||
*/
|
||||
class Png implements ImageImportInterface
|
||||
{
|
||||
/**
|
||||
* Extract data from a PNG image.
|
||||
*
|
||||
* @param ImageBaseData $data Image raw data.
|
||||
*
|
||||
* @return ImageBaseData Image raw data array.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Image\Exception If the image is invalid.
|
||||
* @throws \RangeException If the byte offset is out of range.
|
||||
*/
|
||||
public function getData(array $data): array
|
||||
{
|
||||
$data['filter'] = 'FlateDecode';
|
||||
$byte = new Byte($data['raw']);
|
||||
|
||||
$offset = 0;
|
||||
// check signature
|
||||
if (\substr($data['raw'], $offset, 8) !== \chr(137) . 'PNG' . \chr(13) . \chr(10) . \chr(26) . \chr(10)) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new ImageException('Not a PNG image');
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$offset += 8;
|
||||
$offset += 4;
|
||||
|
||||
$data = $this->getIhdrChunk($byte, $data, $offset);
|
||||
|
||||
$offset += 3;
|
||||
|
||||
// check compression, filter and interlacing settings
|
||||
if (
|
||||
$byte->getByte($offset - 3) !== 0
|
||||
|| $byte->getByte($offset - 2) !== 0
|
||||
|| $byte->getByte($offset - 1) !== 0
|
||||
) {
|
||||
if ($data['recoded']) {
|
||||
// this image has been already re-encoded
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new ImageException('Unsupported feature');
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
// re-encode PNG
|
||||
$data['recode'] = true;
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (\str_contains($data['colspace'], '+Alpha')) {
|
||||
// alpha channel: split images (plain + alpha)
|
||||
$data['splitalpha'] = true;
|
||||
$data['colspace'] = \substr($data['colspace'], 0, -6);
|
||||
return $data;
|
||||
}
|
||||
|
||||
$data['parms'] =
|
||||
'/DecodeParms << /Predictor 15 /Colors '
|
||||
. $data['channels']
|
||||
. ' /BitsPerComponent '
|
||||
. $data['bits']
|
||||
. ' /Columns '
|
||||
. $data['width']
|
||||
. ' >>';
|
||||
|
||||
$offset += 4;
|
||||
return $this->getChunks($byte, $data, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the IHDR chunk data.
|
||||
*
|
||||
* The header chunk (IHDR) contains basic information about the image data and must appear as the first chunk,
|
||||
* and there must only be one header chunk in a PNG data stream.
|
||||
*
|
||||
* @param Byte $byte Byte class object wrapping the raw image data.
|
||||
* @param ImageBaseData $data Image raw data.
|
||||
* @param int $offset Current byte offset.
|
||||
*
|
||||
* @return ImageBaseData Image raw data array.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Image\Exception If the image is invalid.
|
||||
* @throws \RangeException If the byte offset is out of range.
|
||||
*/
|
||||
protected function getIhdrChunk(Byte $byte, array $data, int &$offset): array
|
||||
{
|
||||
if (\substr($data['raw'], $offset, 4) !== 'IHDR') {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new ImageException('Invalid PNG image');
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$offset += 4;
|
||||
$data['width'] = $byte->getULong($offset);
|
||||
$offset += 4;
|
||||
$data['height'] = $byte->getULong($offset);
|
||||
$offset += 4;
|
||||
$data['bits'] = $byte->getByte($offset);
|
||||
++$offset;
|
||||
$chc = $byte->getByte($offset); // channels code
|
||||
++$offset;
|
||||
$data['channels'] = $chc === 2 ? 3 : 1;
|
||||
$chcmap = [
|
||||
0 => 'DeviceGray',
|
||||
2 => 'DeviceRGB',
|
||||
3 => 'Indexed',
|
||||
4 => 'DeviceGray+Alpha',
|
||||
6 => 'DeviceRGB+Alpha',
|
||||
];
|
||||
if (isset($chcmap[$chc])) {
|
||||
$data['colspace'] = $chcmap[$chc];
|
||||
} else {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new ImageException('Unknown color mode');
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract chunks data from a PNG image.
|
||||
*
|
||||
* @param Byte $byte Byte class object wrapping the raw image data.
|
||||
* @param ImageBaseData $data Image raw data.
|
||||
* @param int $offset Current byte offset.
|
||||
*
|
||||
* @return ImageBaseData Image raw data array.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Image\Exception If the image is invalid.
|
||||
* @throws \RangeException If the byte offset is out of range.
|
||||
*/
|
||||
protected function getChunks(Byte $byte, array $data, int $offset): array
|
||||
{
|
||||
// getULong() is unsigned so a "len >= 0" guard would always be true; the
|
||||
// loop instead ends on the IEND chunk, or on the RangeException raised
|
||||
// when a (truncated) stream is read past its end.
|
||||
while (true) {
|
||||
$len = $byte->getULong($offset);
|
||||
$offset += 4;
|
||||
$type = \substr($data['raw'], $offset, 4);
|
||||
$offset += 4;
|
||||
if ($type === 'PLTE') {
|
||||
$data = $this->getPlteChunk($data, $offset, $len);
|
||||
} elseif ($type === 'tRNS') {
|
||||
$data = $this->getTrnsChunk($data, $offset, $len);
|
||||
} elseif ($type === 'IDAT') {
|
||||
$data = $this->getIdatChunk($data, $offset, $len);
|
||||
} elseif ($type === 'iCCP') {
|
||||
$data = $this->getIccpChunk($byte, $data, $offset, $len);
|
||||
} elseif ($type === 'IEND') {
|
||||
// The image trailer chunk (IEND) must be the final chunk
|
||||
// and marks the end of the PNG file or data stream.
|
||||
break;
|
||||
} else {
|
||||
$offset += $len;
|
||||
$offset += 4;
|
||||
}
|
||||
}
|
||||
|
||||
if ($data['colspace'] === 'Indexed' && $data['pal'] === '') {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new ImageException('The color palette is missing');
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the PLTE chunk data.
|
||||
*
|
||||
* The palette chunk (PLTE) stores the colormap data associated with the image data.
|
||||
* This chunk is present only if the image data uses a color palette and must appear before the image data chunk.
|
||||
*
|
||||
* @param ImageBaseData $data Image raw data.
|
||||
* @param int $offset Current byte offset.
|
||||
* @param int $len Number of bytes in this chunk.
|
||||
*
|
||||
* @return ImageBaseData Image raw data array.
|
||||
*/
|
||||
protected function getPlteChunk(array $data, int &$offset, int $len): array
|
||||
{
|
||||
$data['pal'] = \substr($data['raw'], $offset, $len);
|
||||
$offset += $len;
|
||||
$offset += 4;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the tRNS chunk data.
|
||||
*
|
||||
* @param ImageBaseData $data Image raw data.
|
||||
* @param int $offset Current byte offset.
|
||||
* @param int $len Number of bytes in this chunk.
|
||||
*
|
||||
* @return ImageBaseData Image raw data array.
|
||||
*/
|
||||
protected function getTrnsChunk(array $data, int &$offset, int $len): array
|
||||
{
|
||||
// read transparency info
|
||||
$trns = \substr($data['raw'], $offset, $len);
|
||||
$offset += $len;
|
||||
if ($data['colspace'] === 'DeviceGray') {
|
||||
// DeviceGray
|
||||
$data['trns'][] = \ord($trns[1]);
|
||||
} elseif ($data['colspace'] === 'DeviceRGB') {
|
||||
// DeviceRGB
|
||||
$data['trns'][] = \ord($trns[1]);
|
||||
$data['trns'][] = \ord($trns[3]);
|
||||
$data['trns'][] = \ord($trns[5]);
|
||||
} else {
|
||||
// Indexed
|
||||
$data['trns'] = \array_map('ord', \str_split($trns));
|
||||
}
|
||||
|
||||
$offset += 4;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the IDAT chunk data.
|
||||
*
|
||||
* The image data chunk (IDAT) stores the actual image data,
|
||||
* and multiple image data chunks may occur in a data stream and must be stored in contiguous order.
|
||||
*
|
||||
* @param ImageBaseData $data Image raw data.
|
||||
* @param int $offset Current byte offset.
|
||||
* @param int $len Number of bytes in this chunk.
|
||||
*
|
||||
* @return ImageBaseData Image raw data array.
|
||||
*/
|
||||
protected function getIdatChunk(array $data, int &$offset, int $len): array
|
||||
{
|
||||
$data['data'] .= \substr($data['raw'], $offset, $len);
|
||||
$offset += $len;
|
||||
$offset += 4;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the iCCP chunk data.
|
||||
*
|
||||
* @param Byte $byte Byte class object.
|
||||
* @param ImageBaseData $data Image raw data.
|
||||
* @param int $offset Current byte offset.
|
||||
* @param int $len Number of bytes in this chunk.
|
||||
*
|
||||
* @return ImageBaseData Image raw data array.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Image\Exception If the filter method is unknown.
|
||||
* @throws \RangeException If attempting to read beyond available bytes.
|
||||
*/
|
||||
protected function getIccpChunk(Byte $byte, array $data, int &$offset, int $len): array
|
||||
{
|
||||
// skip profile name
|
||||
$pos = 0;
|
||||
while ($byte->getByte($offset++) !== 0 && $pos < 80) {
|
||||
++$pos;
|
||||
}
|
||||
|
||||
// get compression method
|
||||
if ($byte->getByte($offset++) !== 0) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new ImageException('Unknown filter method');
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
// read ICC Color Profile
|
||||
$len -= $pos + 2;
|
||||
$icc = \gzuncompress(\substr($data['raw'], $offset, $len));
|
||||
if ($icc !== false) {
|
||||
$data['icc'] = $icc;
|
||||
} else {
|
||||
throw new ImageException('Error while decompressing ICC profile');
|
||||
}
|
||||
|
||||
$offset += $len;
|
||||
$offset += 4;
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
+578
@@ -0,0 +1,578 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Output.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\File\File as ObjFile;
|
||||
use Com\Tecnick\Pdf\Encrypt\Encrypt;
|
||||
use Com\Tecnick\Pdf\Image\Exception as ImageException;
|
||||
|
||||
/**
|
||||
* Com\Tecnick\Pdf\Image\Output
|
||||
*
|
||||
* @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 TFileOptions array{
|
||||
* allowedHosts?: array<string>,
|
||||
* maxRemoteSize?: int,
|
||||
* curlopts?: array<int, bool|int|string>,
|
||||
* defaultCurlOpts?: array<int, bool|int|string>,
|
||||
* fixedCurlOpts?: array<int, bool|int|string>
|
||||
* }
|
||||
*
|
||||
* @phpstan-import-type ImageBaseData from \Com\Tecnick\Pdf\Image\Import
|
||||
* @phpstan-import-type ImageRawData from \Com\Tecnick\Pdf\Image\Import
|
||||
*
|
||||
* @SuppressWarnings("PHPMD.ExcessiveClassComplexity")
|
||||
*/
|
||||
abstract class Output
|
||||
{
|
||||
/**
|
||||
* Current PDF object number.
|
||||
*/
|
||||
protected int $pon;
|
||||
|
||||
/**
|
||||
* Store image object IDs for the XObject Dictionary.
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
protected array $xobjdict = [];
|
||||
|
||||
/**
|
||||
* Stack of added images.
|
||||
*
|
||||
* @var array<int, array{
|
||||
* 'iid': int,
|
||||
* 'key': string,
|
||||
* 'width': int,
|
||||
* 'height': int,
|
||||
* 'defprint': bool,
|
||||
* 'altimgs'?: array<int, int>,
|
||||
* }>
|
||||
*/
|
||||
protected array $image = [];
|
||||
|
||||
/**
|
||||
* Cache used to store imported image data.
|
||||
* The same image data can be reused multiple times.
|
||||
*
|
||||
* @var array<string, ImageRawData>
|
||||
*/
|
||||
protected array $cache = [];
|
||||
|
||||
/**
|
||||
* Initialize images data.
|
||||
*
|
||||
* @param float $kunit Unit of measure conversion ratio.
|
||||
* @param Encrypt $encrypt Encrypt object.
|
||||
* @param ObjFile $fileHelper File helper for image loading and writing.
|
||||
* @param bool $pdfa True if we are in PDF/A mode.
|
||||
* @param bool $compress Set to false to disable stream compression.
|
||||
* @param ?ImageCacheInterface $imageCache Optional external cache to persist processed image
|
||||
* data across instances and processes (null = disabled).
|
||||
*/
|
||||
public function __construct(
|
||||
protected float $kunit,
|
||||
protected Encrypt $encrypt,
|
||||
protected ObjFile $fileHelper,
|
||||
protected bool $pdfa = false,
|
||||
protected bool $compress = true,
|
||||
protected ?ImageCacheInterface $imageCache = null,
|
||||
) {
|
||||
$this->fileHelper = $fileHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current PDF object number.
|
||||
*/
|
||||
public function getObjectNumber(): int
|
||||
{
|
||||
return $this->pon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback with a temporary file helper.
|
||||
*
|
||||
* @template TResult
|
||||
*
|
||||
* @param ObjFile $fileHelper Temporary file helper to use while the callback executes.
|
||||
* @param callable(): TResult $callback Callback executed with the temporary file helper.
|
||||
*
|
||||
* @return TResult
|
||||
*/
|
||||
public function withFileHelper(ObjFile $fileHelper, callable $callback): mixed
|
||||
{
|
||||
$previousFileHelper = $this->fileHelper;
|
||||
$this->fileHelper = $fileHelper;
|
||||
|
||||
try {
|
||||
return $callback();
|
||||
} finally {
|
||||
$this->fileHelper = $previousFileHelper;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string to print the specified image ID.
|
||||
*
|
||||
* @param int $iid Image ID.
|
||||
* @param float $xpos Abscissa (X coordinate) of the upper-left Image corner in user units.
|
||||
* @param float $ypos Ordinate (Y coordinate) of the upper-left Image corner in user units.
|
||||
* @param float $width Image width in user units.
|
||||
* @param float $height Image height in user units.
|
||||
* @param float $pageheight Page height in user units.
|
||||
*
|
||||
* @return string Image PDF page content.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Image\Exception If the image ID is not found.
|
||||
*/
|
||||
public function getSetImage(
|
||||
int $iid,
|
||||
float $xpos,
|
||||
float $ypos,
|
||||
float $width,
|
||||
float $height,
|
||||
float $pageheight,
|
||||
): string {
|
||||
$img = $this->image[$iid] ?? null;
|
||||
if ($img === null) {
|
||||
throw new ImageException('Unknown image ID: ' . $iid);
|
||||
}
|
||||
|
||||
$out = 'q';
|
||||
$out .= \sprintf(
|
||||
' %F 0 0 %F %F %F cm',
|
||||
$width * $this->kunit,
|
||||
$height * $this->kunit,
|
||||
$xpos * $this->kunit,
|
||||
($pageheight - $ypos - $height) * $this->kunit, // reverse coordinate
|
||||
);
|
||||
|
||||
if (!isset($this->cache[$img['key']]['mask'])) {
|
||||
return $out . ' /IMG' . $iid . ' Do Q' . "\n";
|
||||
}
|
||||
|
||||
if (!isset($this->cache[$img['key']]['plain'])) {
|
||||
return $out . ' /IMGmask' . $iid . ' Do Q' . "\n";
|
||||
}
|
||||
|
||||
return $out . ' /IMGplain' . $iid . ' Do Q' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for Images.
|
||||
*
|
||||
* @param int $pon Current PDF Object Number.
|
||||
*
|
||||
* @return string PDF code for the images block.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Encrypt\Exception If image object encryption fails.
|
||||
*/
|
||||
public function getOutImagesBlock(int $pon): string
|
||||
{
|
||||
$this->pon = $pon;
|
||||
$out = '';
|
||||
foreach ($this->image as $iid => $img) {
|
||||
if (!isset($this->cache[$img['key']]['out'])) {
|
||||
if (isset($this->cache[$img['key']]['mask'])) {
|
||||
/** @var ImageRawData $mask */
|
||||
$mask = &$this->cache[$img['key']]['mask'];
|
||||
$out .= $this->getOutImage($img, $mask, 'mask');
|
||||
if (isset($this->cache[$img['key']]['plain'])) {
|
||||
/** @var ImageRawData $plain */
|
||||
$plain = &$this->cache[$img['key']]['plain'];
|
||||
$out .= $this->getOutImage($img, $plain, 'plain');
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($this->cache[$img['key']]['mask'])) {
|
||||
$out .= $this->getOutImage($img, $this->cache[$img['key']]);
|
||||
}
|
||||
|
||||
unset($mask, $plain);
|
||||
|
||||
$this->image[$iid] = $img;
|
||||
}
|
||||
|
||||
if (($this->cache[$img['key']]['mask']['obj'] ?? 0) === 0) {
|
||||
$this->xobjdict['IMG' . $img['iid']] = $this->cache[$img['key']]['obj'];
|
||||
} else {
|
||||
$maskData = $this->cache[$img['key']]['mask'] ?? null;
|
||||
if ($maskData === null) {
|
||||
$this->xobjdict['IMG' . $img['iid']] = $this->cache[$img['key']]['obj'];
|
||||
continue;
|
||||
}
|
||||
|
||||
$plainData = $this->cache[$img['key']]['plain'] ?? null;
|
||||
if (($plainData['obj'] ?? 0) === 0) {
|
||||
$this->xobjdict['IMGmask' . $img['iid']] = (int) $maskData['obj'];
|
||||
} elseif ($plainData !== null) {
|
||||
$this->xobjdict['IMGplain' . $img['iid']] = (int) $plainData['obj'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for Image object.
|
||||
*
|
||||
* @param array{
|
||||
* 'iid': int,
|
||||
* 'key': string,
|
||||
* 'width': int,
|
||||
* 'height': int,
|
||||
* 'defprint': bool,
|
||||
* 'altimgs'?: array<int, int>,
|
||||
* } $img Image reference.
|
||||
* @param ImageRawData $data Image raw data.
|
||||
* @param string $sub Sub image ('mask', 'plain' or empty string).
|
||||
*
|
||||
* @return string PDF Image object.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Encrypt\Exception If image data encryption fails.
|
||||
*/
|
||||
protected function getOutImage(array &$img, array &$data, string $sub = ''): string
|
||||
{
|
||||
$out = $this->getOutIcc($data) . $this->getOutPalette($data) . $this->getOutAltImages($img, $data, $sub);
|
||||
|
||||
$data['obj'] = ++$this->pon;
|
||||
|
||||
$out .=
|
||||
$data['obj']
|
||||
. ' 0 obj'
|
||||
. "\n"
|
||||
. '<<'
|
||||
. ' /Type /XObject'
|
||||
. ' /Subtype /Image'
|
||||
. ' /Width '
|
||||
. $data['width']
|
||||
. ' /Height '
|
||||
. $data['height']
|
||||
. $this->getOutColorInfo($data);
|
||||
|
||||
if ($data['exturl']) {
|
||||
// external stream
|
||||
$out .=
|
||||
' /Length 0 /F'
|
||||
. ' <<'
|
||||
. ' /FS /URL /F '
|
||||
. $this->encrypt->escapeDataString('true', $this->pon)
|
||||
. ' >>';
|
||||
if ($data['filter'] !== '') {
|
||||
$out .= ' /FFilter /' . $data['filter'];
|
||||
}
|
||||
|
||||
$out .= ' >>' . "\n";
|
||||
} else {
|
||||
if ($data['filter'] !== '') {
|
||||
$out .= ' /Filter /' . $data['filter'];
|
||||
}
|
||||
|
||||
if ($data['parms'] !== '') {
|
||||
$out .= ' ' . $data['parms'];
|
||||
}
|
||||
|
||||
// Colour Key Masking
|
||||
if ($data['trns'] !== []) {
|
||||
$trns = $this->getOutTransparency($data);
|
||||
if ($trns !== '') {
|
||||
$out .= ' /Mask [ ' . $trns . ']';
|
||||
}
|
||||
}
|
||||
|
||||
$stream = $this->encrypt->encryptString($data['data'], $this->pon);
|
||||
$out .= ' /Length ' . \strlen($stream) . '>> stream' . "\n" . $stream . "\n" . 'endstream' . "\n";
|
||||
}
|
||||
|
||||
$out .= 'endobj' . "\n";
|
||||
|
||||
$this->cache[$img['key']]['out'] = true; // mark this as done
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return XObjects Dictionary portion for the images.
|
||||
*/
|
||||
public function getXobjectDict(): string
|
||||
{
|
||||
$out = '';
|
||||
foreach ($this->xobjdict as $iid => $objid) {
|
||||
$out .= ' /' . $iid . ' ' . $objid . ' 0 R';
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return XObjects Dictionary.
|
||||
*
|
||||
* @param array<int> $keys Image IDs.
|
||||
*/
|
||||
public function getXobjectDictByKeys(array $keys): string
|
||||
{
|
||||
if ($keys === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$out = '';
|
||||
|
||||
foreach ($keys as $iid) {
|
||||
$key = 'IMG' . $iid;
|
||||
if (isset($this->xobjdict[$key])) {
|
||||
$out .= ' /' . $key . ' ' . $this->xobjdict[$key] . ' 0 R';
|
||||
continue;
|
||||
}
|
||||
$key = 'IMGplain' . $iid;
|
||||
if (isset($this->xobjdict[$key])) {
|
||||
$out .= ' /' . $key . ' ' . $this->xobjdict[$key] . ' 0 R';
|
||||
continue;
|
||||
}
|
||||
$key = 'IMGmask' . $iid;
|
||||
if (isset($this->xobjdict[$key])) {
|
||||
$out .= ' /' . $key . ' ' . $this->xobjdict[$key] . ' 0 R';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for ICC object.
|
||||
*
|
||||
* @param ImageRawData $data Image raw data.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Encrypt\Exception If ICC profile encryption fails.
|
||||
*/
|
||||
protected function getOutIcc(array &$data): string
|
||||
{
|
||||
if ($data['icc'] === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data['obj_icc'] = ++$this->pon;
|
||||
$out =
|
||||
$data['obj_icc']
|
||||
. ' 0 obj'
|
||||
. "\n"
|
||||
. '<<'
|
||||
. ' /N '
|
||||
. $data['channels']
|
||||
. ' /Alternate /'
|
||||
. $data['colspace'];
|
||||
$icc = $data['icc'];
|
||||
if ($this->compress) {
|
||||
$out .= ' /Filter /FlateDecode';
|
||||
$cicc = \gzcompress($icc);
|
||||
if ($cicc !== false) {
|
||||
$icc = $cicc;
|
||||
}
|
||||
}
|
||||
|
||||
$stream = $this->encrypt->encryptString($icc, $this->pon);
|
||||
return (
|
||||
$out . (
|
||||
' /Length '
|
||||
. \strlen($stream)
|
||||
. ' >>'
|
||||
. ' stream'
|
||||
. "\n"
|
||||
. $stream
|
||||
. "\n"
|
||||
. 'endstream'
|
||||
. "\n"
|
||||
. 'endobj'
|
||||
. "\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for Indexed palette object.
|
||||
*
|
||||
* @param ImageRawData $data Image raw data.
|
||||
*
|
||||
* @throws \Com\Tecnick\Pdf\Encrypt\Exception If palette encryption fails.
|
||||
*/
|
||||
protected function getOutPalette(array &$data): string
|
||||
{
|
||||
if ($data['colspace'] !== 'Indexed') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data['obj_pal'] = ++$this->pon;
|
||||
$out = $data['obj_pal'] . ' 0 obj' . "\n" . '<<';
|
||||
$pal = $data['pal'];
|
||||
if ($this->compress) {
|
||||
$out .= '/Filter /FlateDecode';
|
||||
$cpal = \gzcompress($pal);
|
||||
if ($cpal !== false) {
|
||||
$pal = $cpal;
|
||||
}
|
||||
}
|
||||
|
||||
$stream = $this->encrypt->encryptString($pal, $this->pon);
|
||||
return (
|
||||
$out . (
|
||||
' /Length '
|
||||
. \strlen($stream)
|
||||
. '>>'
|
||||
. ' stream'
|
||||
. "\n"
|
||||
. $stream
|
||||
. "\n"
|
||||
. 'endstream'
|
||||
. "\n"
|
||||
. 'endobj'
|
||||
. "\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for color and mask information.
|
||||
*
|
||||
* @param ImageRawData $data Image raw data.
|
||||
*/
|
||||
protected function getOutColorInfo(array $data): string
|
||||
{
|
||||
$out = '';
|
||||
// set color space
|
||||
if ($data['obj_icc'] !== 0) {
|
||||
// ICC Colour Space
|
||||
$out .= ' /ColorSpace [/ICCBased ' . $data['obj_icc'] . ' 0 R]';
|
||||
} elseif ($data['obj_pal'] !== 0) {
|
||||
// Indexed Colour Space
|
||||
$out .=
|
||||
' /ColorSpace [/Indexed /DeviceRGB '
|
||||
. ((\strlen($data['pal']) / 3) - 1)
|
||||
. ' '
|
||||
. $data['obj_pal']
|
||||
. ' 0 R]';
|
||||
} else {
|
||||
// Device Colour Space
|
||||
$out .= ' /ColorSpace /' . $data['colspace'];
|
||||
}
|
||||
|
||||
if ($data['colspace'] === 'DeviceCMYK') {
|
||||
$out .= ' /Decode [1 0 1 0 1 0 1 0]';
|
||||
}
|
||||
|
||||
$out .= ' /BitsPerComponent ' . $data['bits'];
|
||||
|
||||
$maskobj = (int) ($this->cache[$data['key']]['mask']['obj'] ?? 0);
|
||||
if (!$data['ismask'] && $maskobj > 0) {
|
||||
$out .= ' /SMask ' . $maskobj . ' 0 R';
|
||||
}
|
||||
|
||||
if ($data['obj_alt'] !== 0) {
|
||||
// reference to alternate images dictionary
|
||||
$out .= ' /Alternates ' . $data['obj_alt'] . ' 0 R';
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for Alternate images object.
|
||||
*
|
||||
* @param array{
|
||||
* 'iid': int,
|
||||
* 'key': string,
|
||||
* 'width': int,
|
||||
* 'height': int,
|
||||
* 'defprint': bool,
|
||||
* 'altimgs'?: array<int, int>,
|
||||
* } $img Image reference.
|
||||
* @param ImageRawData $data Image raw data.
|
||||
* @param string $sub Sub image ('mask', 'plain' or empty string).
|
||||
*/
|
||||
protected function getOutAltImages(array $img, array &$data, string $sub = ''): string
|
||||
{
|
||||
if ($this->pdfa || !isset($img['altimgs']) || $img['altimgs'] === [] || $sub === 'mask') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data['obj_alt'] = ++$this->pon;
|
||||
|
||||
$out = $this->pon . ' 0 obj' . "\n" . '[';
|
||||
foreach ($img['altimgs'] as $iid) {
|
||||
$altimg = $this->image[$iid] ?? null;
|
||||
if ($altimg === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$altobj = (int) ($this->cache[$altimg['key']]['obj'] ?? 0);
|
||||
if ($altobj === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$out .=
|
||||
' << /Image '
|
||||
. $altobj
|
||||
. ' 0 R'
|
||||
. ' /DefaultForPrinting '
|
||||
. ($altimg['defprint'] ? 'true' : 'false')
|
||||
. ' >>';
|
||||
}
|
||||
|
||||
return $out . (' ]' . "\n" . 'endobj' . "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PDF output string for color transparency.
|
||||
*
|
||||
* @param ImageRawData $data Image raw data.
|
||||
*/
|
||||
protected function getOutTransparency(array $data): string
|
||||
{
|
||||
$trns = '';
|
||||
|
||||
if ($data['colspace'] === 'Indexed') {
|
||||
// Indexed: trns holds one alpha byte per palette entry; mask the
|
||||
// fully-transparent entries (alpha 0) by their palette index.
|
||||
foreach ($data['trns'] as $idx => $val) {
|
||||
if ($val !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$trns .= $idx . ' ' . $idx . ' ';
|
||||
}
|
||||
|
||||
return $trns;
|
||||
}
|
||||
|
||||
// DeviceRGB / DeviceGray: trns holds the transparent colour sample
|
||||
// values themselves, emitted as single-value colour-key ranges.
|
||||
foreach ($data['trns'] as $val) {
|
||||
$trns .= $val . ' ' . $val . ' ';
|
||||
}
|
||||
|
||||
return $trns;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user