Feature: Labelprint für Kistenetiketten hinzugefügt

This commit is contained in:
2025-10-27 12:14:44 +01:00
parent 43bc416554
commit 14bae6c9ef
1068 changed files with 229014 additions and 1807 deletions
+34
View File
@@ -0,0 +1,34 @@
<?php
/**
* Exception.php
*
* @since 2011-05-23
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-Image
*/
class Exception extends \Exception
{
}
+583
View File
@@ -0,0 +1,583 @@
<?php
/**
* Import.php
*
* @since 2011-05-23
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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;
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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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,
* '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;
/**
* 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.
*/
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 {
return strtr(
rtrim(
base64_encode(
pack('H*', md5($image . $width . $height . $quality))
),
'='
),
'+/',
'-_'
);
}
/**
* Get an imported image by key.
*
* @param string $key Image key.
*
* @return ImageRawData Image raw data array.
*/
public function getImageDataByKey(string $key): array
{
if (empty($this->cache[$key])) {
throw new ImageException('Unknown key');
}
return $this->cache[$key];
}
/**
* 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
*/
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);
if (isset($this->cache[$imgkey])) {
return $this->cache[$imgkey];
}
$data = $this->getRawData($image);
$data['key'] = $imgkey;
if ($width === null) {
$width = $data['width'];
}
$width = max(0, (int) $width);
if ($height === null) {
$height = $data['height'];
}
$height = max(0, (int) $height);
if ((! $data['native']) || ($width != $data['width']) || ($height != $data['height'])) {
$data = $this->getResizedRawData($data, $width, $height, true, $quality);
}
$data = $this->getData($data, $width, $height, $quality);
if ($ismask) {
$data['mask'] = $data;
} elseif (! empty($data['splitalpha'])) {
// create 2 separate images: plain + mask
$rawdata = $data;
$data['plain'] = $this->getResizedRawData($rawdata, $width, $height, false, $quality);
$data['plain'] = $this->getData($data['plain'], $width, $height, $quality);
$data['mask'] = $this->getAlphaChannelRawData($rawdata);
$data['mask'] = $this->getData($data['mask'], $width, $height, $quality);
$data['mask']['colspace'] = 'DeviceGray';
}
// store data in cache
$this->cache[$imgkey] = $data;
return $data;
}
/**
* 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.
*/
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($data);
if (! empty($data['recode'])) {
// re-encode the image as it was not possible to decode it
$data = $this->getResizedRawData($data, $width, $height, true, $quality);
$data = $imageImport->getData($data);
}
return $data;
}
/**
* @param array{
* 'type': int,
* } $data Image raw data.
*/
private function createImportImage(array $data): ImageImportInterface
{
$class = '\\Com\\Tecnick\\Pdf\\Image\\Import\\' . self::NATIVE[$data['type']];
return new $class();
}
/**
* 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.
*/
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;
$file = new File();
$raw = $file->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.
*/
protected function getMetaData(array $data): array
{
try {
$meta = @getimagesizefromstring($data['raw']);
} catch (\Exception $exception) {
throw new ImageException('Invalid image format: ' . $exception);
}
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) ? IMAGETYPE_PNG : IMAGETYPE_JPEG);
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 getImageRawData.
* @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.
*
* @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
$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 getImageRawData.
*
* @return ImageRawData Image raw data array.
*/
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);
}
// 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
$color = imagecolorsforindex($img, $colindex);
// GD alpha is only 7 bit (0 -> 127); 2.2 is the gamma value
$alpha = (int) (((float) (127 - $color['alpha']) / 127) ** 2.2 * 255);
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,42 @@
<?php
/**
* ImageImportInterface.php
*
* @since 2011-05-23
* @category Library
* @package PdfImage
* @author jmleroux <jmleroux.pro@gmail.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\Jpeg
*
* @since 2011-05-23
* @category Library
* @package PdfImage
* @author jmleroux <jmleroux.pro@gmail.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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;
}
Binary file not shown.
+301
View File
@@ -0,0 +1,301 @@
<?php
/**
* Png.php
*
* @since 2011-05-23
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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.
*/
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($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 (! empty($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($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 ImageBaseData $data Image raw data.
* @param int $offset Current byte offset.
*
* @return ImageBaseData Image raw data array.
*/
protected function getIhdrChunk(array $data, int &$offset): array
{
$byte = new Byte($data['raw']);
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 ImageBaseData $data Image raw data.
* @param int $offset Current byte offset.
*
* @return ImageBaseData Image raw data array.
*/
protected function getChunks(array $data, int $offset): array
{
$byte = new Byte($data['raw']);
while (($len = $byte->getULong($offset)) >= 0) {
$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') && (empty($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 presentonly 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.
*/
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;
}
}
+472
View File
@@ -0,0 +1,472 @@
<?php
/**
* Output.php
*
* @since 2011-05-23
* @category Library
* @package PdfImage
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @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\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-2024 Nicola Asuni - Tecnick.com LTD
* @license http://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
* @link https://github.com/tecnickcom/tc-lib-pdf-image
*
* @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 bool $pdfa True if we are in PDF/A mode.
* @param bool $compress Set to false to disable stream compression.
*/
public function __construct(
protected float $kunit,
/**
* Encrypt object.
*/
protected Encrypt $encrypt,
protected bool $pdfa = false,
protected bool $compress = true
) {
}
/**
* Returns current PDF object number.
*/
public function getObjectNumber(): int
{
return $this->pon;
}
/**
* 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.
*/
public function getSetImage(
int $iid,
float $xpos,
float $ypos,
float $width,
float $height,
float $pageheight
): string {
if (empty($this->image[$iid])) {
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 (empty($this->cache[$this->image[$iid]['key']]['mask'])) {
return $out . ' /IMG' . $iid . ' Do Q' . "\n";
}
if (empty($this->cache[$this->image[$iid]['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.
*/
public function getOutImagesBlock(int $pon): string
{
$this->pon = $pon;
$out = '';
foreach ($this->image as $iid => $img) {
if (empty($this->cache[$img['key']]['out'])) {
if (empty($this->cache[$img['key']]['mask'])) {
$out .= $this->getOutImage($img, $this->cache[$img['key']]);
} else {
$out .= $this->getOutImage($img, $this->cache[$img['key']]['mask'], 'mask');
if (!empty($this->cache[$img['key']]['plain'])) {
$out .= $this->getOutImage($img, $this->cache[$img['key']]['plain'], 'plain');
}
}
$this->image[$iid] = $img;
}
if (empty($this->cache[$img['key']]['mask']['obj'])) {
$this->xobjdict['IMG' . $img['iid']] = $this->cache[$img['key']]['obj'];
} else {
if (empty($this->cache[$img['key']]['plain']['obj'])) {
$this->xobjdict['IMGmask' . $img['iid']] = $this->cache[$img['key']]['mask']['obj'];
} else {
$this->xobjdict['IMGplain' . $img['iid']] = $this->cache[$img['key']]['plain']['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 ImageBaseData $data Image raw data.
* @param string $sub Sub image ('mask', 'plain' or empty string).
*
* @return string PDF Image object.
*/
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 (! empty($data['exturl'])) {
// external stream
$out .= ' /Length 0 /F'
. ' <<'
. ' /FS /URL /F '
. $this->encrypt->escapeDataString(empty($data['exturl']) ? 'false' : 'true', $this->pon)
. ' >>';
if (! empty($data['filter'])) {
$out .= ' /FFilter /' . $data['filter'];
}
$out .= ' >>' . "\n";
} else {
if (! empty($data['filter'])) {
$out .= ' /Filter /' . $data['filter'];
}
if (! empty($data['parms'])) {
$out .= ' ' . $data['parms'];
}
// Colour Key Masking
if (! empty($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 (empty($keys)) {
return '';
}
$out = '';
foreach ($keys as $iid) {
$key = 'IMG' . $iid;
if (!empty($this->xobjdict[$key])) {
$out .= ' /' . $key . ' ' . $this->xobjdict[$key] . ' 0 R';
continue;
}
$key = 'IMGplain' . $iid;
if (!empty($this->xobjdict[$key])) {
$out .= ' /' . $key . ' ' . $this->xobjdict[$key] . ' 0 R';
continue;
}
$key = 'IMGmask' . $iid;
if (!empty($this->xobjdict[$key])) {
$out .= ' /' . $key . ' ' . $this->xobjdict[$key] . ' 0 R';
continue;
}
}
return $out;
}
/**
* Get the PDF output string for ICC object.
*
* @param ImageBaseData $data Image raw data.
*/
protected function getOutIcc(array &$data): string
{
if (empty($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 ImageBaseData $data Image raw data.
*/
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 ImageBaseData $data Image raw data.
*/
protected function getOutColorInfo(array $data): string
{
$out = '';
// set color space
if (! empty($data['obj_icc'])) {
// ICC Colour Space
$out .= ' /ColorSpace [/ICCBased ' . $data['obj_icc'] . ' 0 R]';
} elseif (! empty($data['obj_pal'])) {
// 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'];
if (! $data['ismask'] && ! empty($this->cache[$data['key']]['mask']['obj'])) {
$out .= ' /SMask ' . $this->cache[$data['key']]['mask']['obj'] . ' 0 R';
}
if (! empty($data['obj_alt'])) {
// 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 ImageBaseData $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 || empty($img['altimgs']) || ($sub == 'mask')) {
return '';
}
$data['obj_alt'] = ++$this->pon;
$out = $this->pon . ' 0 obj' . "\n"
. '[';
foreach ($img['altimgs'] as $iid) {
if (! empty($this->cache[$this->image[$iid]['key']]['obj'])) {
$out .= ' << /Image ' . $this->cache[$this->image[$iid]['key']]['obj'] . ' 0 R'
. ' /DefaultForPrinting ' . (empty($this->image[$iid]['defprint']) ? 'false' : 'true')
. ' >>';
}
}
return $out . (' ]' . "\n"
. 'endobj' . "\n");
}
/**
* Get the PDF output string for color and mask information.
*
* @param array{
* 'trns': array<int, int>,
* } $data Image raw data.
*/
protected function getOutTransparency(array $data): string
{
$trns = '';
foreach ($data['trns'] as $idx => $val) {
if ($val == 0) {
$trns .= $idx . ' ' . $idx . ' ';
}
}
return $trns;
}
}