Files
EpiWebview/vendor/tecnickcom/tc-lib-pdf/src/Tcpdf.php

1277 lines
45 KiB
PHP

<?php
/**
* Tcpdf.php
*
* @since 2002-08-03
* @category Library
* @package Pdf
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2002-2025 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
*
* This file is part of tc-lib-pdf software library.
*/
namespace Com\Tecnick\Pdf;
use Com\Tecnick\Barcode\Exception as BarcodeException;
use Com\Tecnick\Pdf\Encrypt\Encrypt as ObjEncrypt;
use Com\Tecnick\Pdf\Exception as PdfException;
/**
* Com\Tecnick\Pdf\Tcpdf
*
* Tcpdf PDF class
*
* @since 2002-08-03
* @category Library
* @package Pdf
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2002-2025 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
*
* @phpstan-import-type StyleDataOpt from \Com\Tecnick\Pdf\Graph\Base
* @phpstan-import-type PageData from \Com\Tecnick\Pdf\Page\Box
* @phpstan-import-type PageInputData from \Com\Tecnick\Pdf\Page\Box
* @phpstan-import-type TFontMetric from \Com\Tecnick\Pdf\Font\Stack
*
* @phpstan-import-type TAnnotOpts from Output
* @phpstan-import-type TSignature from Output
* @phpstan-import-type TSignTimeStamp from Output
* @phpstan-import-type TGTransparency from Output
* @phpstan-import-type TUserRights from Output
* @phpstan-import-type TXOBject from Output
*
* @SuppressWarnings("PHPMD.DepthOfInheritance")
*/
class Tcpdf extends \Com\Tecnick\Pdf\ClassObjects
{
/**
* Initialize a new PDF object.
*
* @param string $unit Unit of measure ('pt', 'mm', 'cm', 'in').
* @param bool $isunicode True if the document is in Unicode mode.
* @param bool $subsetfont If true subset the embedded fonts to remove the unused characters.
* @param bool $compress Set to false to disable stream compression.
* @param string $mode PDF mode: "pdfa1", "pdfa2", "pdfa3", "pdfx" or empty.
* @param ?ObjEncrypt $objEncrypt Encryption object.
*/
public function __construct(
string $unit = 'mm',
bool $isunicode = true,
bool $subsetfont = false,
bool $compress = true,
string $mode = '',
?ObjEncrypt $objEncrypt = null
) {
$this->setDecimalSeparator();
$this->doctime = time();
$this->docmodtime = $this->doctime;
$seed = new \Com\Tecnick\Pdf\Encrypt\Type\Seed();
$this->fileid = md5($seed->encrypt('TCPDF'));
$this->setPDFFilename($this->fileid . '.pdf');
$this->unit = $unit;
$this->setUnicodeMode($isunicode);
$this->subsetfont = $subsetfont;
$this->setPDFMode($mode);
$this->setCompressMode($compress);
$this->setPDFVersion();
$this->initClassObjects($objEncrypt);
}
/**
* Set the pdf mode.
*
* @param string $mode Input PDFA mode.
*/
protected function setPDFMode(string $mode): void
{
$this->pdfx = ($mode == 'pdfx');
$this->pdfa = 0;
$matches = ['', '0'];
if (preg_match('/^pdfa([1-3])$/', $mode, $matches) === 1) {
$this->pdfa = (int) $matches[1];
}
}
/**
* Set the compression mode.
*
* @param bool $compress Set to false to disable stream compression.
*/
protected function setCompressMode(bool $compress): void
{
$this->compress = (($compress) && ($this->pdfa != 3));
}
/**
* Set the decimal separator.
*
* @throw PdfException in case of error.
*/
protected function setDecimalSeparator(): void
{
// check for locale-related bug
if (1.1 == 1) { /* @phpstan-ignore-line */
throw new PdfException("Don't alter the locale before including class file");
}
// check for decimal separator
if (sprintf('%.1F', 1.0) != '1.0') {
setlocale(LC_NUMERIC, 'C');
}
}
/**
* Set the decimal separator.
*
* @param bool $isunicode True when using Unicode mode.
*/
protected function setUnicodeMode(bool $isunicode): void
{
$this->isunicode = $isunicode;
// check if PCRE Unicode support is enabled
if ($this->isunicode && (@preg_match('/\pL/u', 'a') == 1)) {
$this->setSpaceRegexp('/(?!\xa0)[\s\p{Z}]/u');
return;
}
// PCRE unicode support is turned OFF
$this->setSpaceRegexp('/[^\S\xa0]/');
}
/**
* Set the pdf document base file name.
* If the file extension is present, it must be '.pdf' or '.PDF'.
*
* @param string $name File name.
*/
public function setPDFFilename(string $name): void
{
$bname = basename($name);
if (preg_match('/^[\w,\s-]+(\.pdf)?$/i', $bname) === 1) {
$this->pdffilename = $bname;
$this->encpdffilename = rawurlencode($bname);
}
}
/**
* Set regular expression to detect withespaces or word separators.
* The pattern delimiter must be the forward-slash character "/".
* Some example patterns are:
* <pre>
* Non-Unicode or missing PCRE unicode support: "/[^\S\xa0]/"
* Unicode and PCRE unicode support: "/(?!\xa0)[\s\p{Z}]/u"
* Unicode and PCRE unicode support in Chinese mode: "/(?!\xa0)[\s\p{Z}\p{Lo}]/u"
* if PCRE unicode support is turned ON ("\P" is the negate class of "\p"):
* \s : any whitespace character
* \p{Z} : any separator
* \p{Lo} : Unicode letter or ideograph that does not have lowercase and uppercase variants.
* \xa0 : Unicode Character 'NO-BREAK SPACE' (U+00A0)
* </pre>
*
* @param string $regexp regular expression (leave empty for default).
*/
public function setSpaceRegexp(string $regexp = '/[^\S\xa0]/'): void
{
$parts = explode('/', $regexp);
$this->spaceregexp = [
'r' => $regexp,
'p' => (empty($parts[1]) ? '[\s]' : $parts[1]),
'm' => (empty($parts[2]) ? '' : $parts[2]),
];
}
/**
* Defines the way the document is to be displayed by the viewer.
*
* @param int|string $zoom The zoom to use.
* It can be one of the following string values or a number indicating the
* zooming factor to use.
* * fullpage: displays the entire page on screen * fullwidth: uses
* maximum width of window
* * real: uses real size (equivalent to 100% zoom) * default: uses
* viewer default mode
* @param string $layout The page layout. Possible values are:
* * SinglePage Display one page at a time
* * OneColumn Display the pages in one column
* * TwoColumnLeft Display the pages in two columns,
* with odd-numbered pages on the left
* * TwoColumnRight Display the pages in
* two columns, with odd-numbered pages
* on the right
* * TwoPageLeft Display the pages two at a time,
* with odd-numbered pages on the left
* * TwoPageRight Display the pages two at a time,
* with odd-numbered pages on the right
* @param string $mode A name object specifying how the document should be displayed when opened:
* * UseNone Neither document outline nor thumbnail images visible
* * UseOutlines Document outline visible
* * UseThumbs Thumbnail images visible
* * FullScreen Full screen, with no menu bar, window controls,
* or any other window visible
* * UseOC (PDF 1.5) Optional content group panel visible
* * UseAttachments (PDF 1.6) Attachments panel visible
*/
public function setDisplayMode(
int|string $zoom = 'default',
string $layout = 'SinglePage',
string $mode = 'UseNone'
): static {
$this->display['zoom'] = (is_numeric($zoom) || in_array($zoom, $this::VALIDZOOM)) ? $zoom : 'default';
$this->display['layout'] = $this->page->getLayout($layout);
$this->display['page'] = $this->page->getDisplay($mode);
return $this;
}
// ===| BARCODE |=======================================================
/**
* Get a barcode PDF code.
*
* @param string $type Barcode type.
* @param string $code Barcode content.
* @param float $posx Abscissa of upper-left corner.
* @param float $posy Ordinate of upper-left corner.
* @param int $width Barcode width in user units (excluding padding).
* A negative value indicates the multiplication
* factor for each column.
* @param int $height Barcode height in user units (excluding padding).
* A negative value indicates the multiplication
* factor for each row.
* @param array{int, int, int, int} $padding Additional padding to add around the barcode
* (top, right, bottom, left) in user units. A
* negative value indicates the multiplication
* factor for each row or column.
* @param StyleDataOpt $style Array of style options.
*
* @throws BarcodeException in case of error
*/
public function getBarcode(
string $type,
string $code,
float $posx = 0,
float $posy = 0,
int $width = -1,
int $height = -1,
array $padding = [0, 0, 0, 0],
array $style = []
): string {
$model = $this->barcode->getBarcodeObj($type, $code, $width, $height, 'black', $padding);
$bars = $model->getBarsArrayXYWH();
$out = '';
$out .= $this->graph->getStartTransform();
$out .= $this->graph->getStyleCmd($style);
foreach ($bars as $bar) {
$out .= $this->graph->getBasicRect(($posx + $bar[0]), ($posy + $bar[1]), $bar[2], $bar[3], 'f');
}
return $out . $this->graph->getStopTransform();
}
/**
* Add an embedded file.
* If a file with the same name already exists, it will be ignored.
*
* @param string $file File name (absolute or relative path).
*
* @throws PdfException in case of error.
*/
public function addEmbeddedFile(string $file): void
{
if (($this->pdfa == 1) || ($this->pdfa == 2)) {
throw new PdfException('Embedded files are not allowed in PDF/A mode version 1 and 2');
}
if (empty($file)) {
throw new PdfException('Empty file name');
}
$filekey = basename((string) $file);
if (
! empty($filekey)
&& empty($this->embeddedfiles[$filekey])
) {
$this->embeddedfiles[$filekey] = [
'a' => 0,
'f' => ++$this->pon,
'n' => ++$this->pon,
'file' => (string) $file,
'content' => '',
];
}
}
/**
* Add string content as an embedded file.
* If a file with the same name already exists, it will be ignored.
*
* @param string $file File name to be used a key for the embedded file.
* @param string $content Content of the embedded file.
*
* @throws PdfException in case of error.
*/
public function addContentAsEmbeddedFile(string $file, string $content): void
{
if (($this->pdfa == 1) || ($this->pdfa == 2)) {
throw new PdfException('Embedded files are not allowed in PDF/A mode version 1 and 2');
}
if (empty($file) || empty($content)) {
throw new PdfException('Empty file name or content');
}
if (empty($this->embeddedfiles[$file])) {
$this->embeddedfiles[$file] = [
'a' => 0,
'f' => ++$this->pon,
'n' => ++$this->pon,
'file' => $file,
'content' => $content,
];
}
}
// ===| ANNOTATION |====================================================
/**
* Add an annotation and returns the object id.
*
* @param float $posx Abscissa of upper-left corner.
* @param float $posy Ordinate of upper-left corner.
* @param float $width Width.
* @param float $height Height.
* @param string $txt Annotation text or alternate content.
* @param TAnnotOpts $opt Array of options (Annotation Types) - all lowercase.
*
* @return int Object ID.
*/
public function setAnnotation(
float $posx,
float $posy,
float $width,
float $height,
string $txt,
array $opt = [
'subtype' => 'text',
]
): int {
if (!empty($this->xobjtid)) {
// Store annotationparameters for later use on a XObject template.
$this->xobjects[$this->xobjtid]['annotations'][] = [
'n' => 0,
'x' => $posx,
'y' => $posy,
'w' => $width,
'h' => $height,
'txt' => $txt,
'opt' => $opt,
];
return 0;
}
$oid = ++$this->pon;
$this->annotation[$oid] = [
'n' => $oid,
'x' => $posx,
'y' => $posy,
'w' => $width,
'h' => $height,
'txt' => $txt,
'opt' => $opt,
];
switch (strtolower($opt['subtype'])) {
case 'fileattachment':
case 'sound':
$this->addEmbeddedFile($opt['fs']);
}
// Add widgets annotation's icons
if (isset($opt['mk']['i']) && is_string($opt['mk']['i'])) {
$this->image->add($opt['mk']['i']);
}
if (isset($opt['mk']['ri']) && is_string($opt['mk']['ri'])) {
$this->image->add($opt['mk']['ri']);
}
if (isset($opt['mk']['ix']) && is_string($opt['mk']['ix'])) {
$this->image->add($opt['mk']['ix']);
}
return $oid;
}
/**
* Creates a link in the specified area.
* A link annotation represents either a hypertext link to a destination elsewhere in the document.
*
* @param float $posx Abscissa of upper-left corner.
* @param float $posy Ordinate of upper-left corner.
* @param float $width Width.
* @param float $height Height.
* @param string $link URL to open when the link is clicked or an identifier returned by addInternalLink().
* A single character prefix may be used to specify the link action:
* - '#' = internal destination
* - '%' = embedded PDF file
* - '*' = embedded generic file
*
* @return int Object ID (Add to a page via: $pdf->page->addAnnotRef($aoid);).
*/
public function setLink(
float $posx,
float $posy,
float $width,
float $height,
string $link,
): int {
return $this->setAnnotation(
$posx,
$posy,
$width,
$height,
$link,
['subtype' => 'Link']
);
}
/**
* Defines the page and vertical position an internal link points to.
*
* @param int $page Page number.
* @param float $posy Vertical position.
*
* @return string Internal link identifier to be used with setLink().
*
*/
public function addInternalLink(int $page = -1, float $posy = 0): string
{
$lnkid = '@' . (count($this->links) + 1);
$this->links[$lnkid] = [
'p' => ($page < 0) ? $this->page->getPageID() : $page,
'y' => $posy,
];
return $lnkid;
}
/**
* Add a named destination.
*
* @param string $name Named destination (must be unique).
* @param int $page Page number.
* @param float $posx Abscissa of upper-left corner.
* @param float $posy Ordinate of upper-left corner.
*
* @return string Destination name.
*/
public function setNamedDestination(
string $name,
int $page = -1,
float $posx = 0,
float $posy = 0,
): string {
$ename = $this->encrypt->encodeNameObject($name);
$this->dests[$ename] = [
'p' => ($page < 0) ? $this->page->getPageID() : $page,
'x' => $posx,
'y' => $posy,
];
return '#' . $ename;
}
/**
* Add a bookmark entry.
*
* @param string $name Bookmark description that will be printed in the TOC.
* @param string $link (Optional) URL to open when the link is clicked
* or an identifier returned by addInternalLink().
* A single character prefix may be used to specify the link action:
* - '#' = internal destination
* - '%' = embedded PDF file
* - '*' = embedded generic file
* @param int $level Bookmark level (minimum 0).
*
* @param int $page Page number.
* @param float $posx Abscissa of upper-left corner.
* @param float $posy Ordinate of upper-left corner.
* @param string $fstyle Font style.
* Possible values are (case insensitive):
* - regular (default)
* - B: bold
* - I: italic
* - U: underline
* - D: strikeout (linethrough)
* - O: overline
* @param string $color Color name.
*/
public function setBookmark(
string $name,
string $link = '',
int $level = 0,
int $page = -1,
float $posx = 0,
float $posy = 0,
string $fstyle = '',
string $color = '',
): void {
$maxlevel = ((count($this->outlines) > 0) ? (end($this->outlines)['l'] + 1) : 0);
$this->outlines[] = [
't' => $name,
'u' => $link,
'l' => (($level < 0) ? 0 : ($level > $maxlevel ? $maxlevel : $level)),
'p' => (($page < 0) ? $this->page->getPageID() : $page),
'x' => $posx,
'y' => $posy,
's' => strtoupper($fstyle),
'c' => $color,
'parent' => 0,
'first' => -1,
'last' => -1,
'next' => -1,
'prev' => -1,
];
}
// ===| SIGNATURE |=====================================================
/**
* Set User's Rights for the PDF Reader.
* WARNING: This is experimental and currently doesn't work because requires a private key.
* Check the PDF Reference 8.7.1 Transform Methods,
* Table 8.105 Entries in the UR transform parameters dictionary.
*
* @param TUserRights $rights User rights:
* - annots (string) Names specifying additional annotation-related usage rights for the document.
* Valid names in PDF 1.5 and later are /Create/Delete/Modify/Copy/Import/Export, which permit
* the user to perform the named operation on annotations.
* - document (string) Names specifying additional document-wide usage rights for the document.
* The only defined value is "/FullSave", which permits a user to save the document along with
* modified form and/or annotation data.
* - ef (string) Names specifying additional usage rights for named embedded files in the document.
* Valid names are /Create/Delete/Modify/Import, which permit the user to perform the named
* operation on named embedded files Names specifying additional embedded-files-related usage
* rights for the document.
* - enabled (bool) If true enable user's rights on PDF reader.
* - form (string) Names specifying additional form-field-related usage rights for the document.
* Valid names are: /Add/Delete/FillIn/Import/Export/SubmitStandalone/SpawnTemplate.
* - formex (string) Names specifying additional form-field-related usage rights. The only valid
* name is BarcodePlaintext, which permits text form field data to be encoded as a plaintext
* two-dimensional barcode.
* - signature (string) Names specifying additional signature-related usage rights for the document.
* The only defined value is /Modify, which permits a user to apply a digital signature to an
* existing signature form field or clear a signed signature form field.
*/
public function setUserRights(array $rights): void
{
$this->userrights = array_merge($this->userrights, $rights);
}
/**
* Enable document signature (requires the OpenSSL Library).
* The digital signature improve document authenticity and integrity and allows
* to enable extra features on PDF Reader.
*
* To create self-signed signature:
* openssl req -x509 -nodes -days 365000 -newkey rsa:1024 -keyout tcpdf.crt -out tcpdf.crt
* To export crt to p12:
* openssl pkcs12 -export -in tcpdf.crt -out tcpdf.p12
* To convert pfx certificate to pem:
* openssl pkcs12 -in tcpdf.pfx -out tcpdf.crt -nodes
*
* @param TSignature $data Signature data:
* - appearance (array) Signature appearance.
* - empty (bool) Array of empty signatures:
* - objid (int) Object id.
* - name (string) Name of the signature field.
* - page (int) Page number.
* - rect (array) Rectangle of the signature field.
* - name (string) Name of the signature field.
* - page (int) Page number.
* - rect (array) Rectangle of the signature field.
* - approval (bool) Enable approval signature eg. for PDF incremental update.
* - cert_type (int) The access permissions granted for this document. Valid values shall be:
* 1 = No changes to the document shall be permitted;
* any change to the document shall invalidate the signature;
* 2 = Permitted changes shall be filling in forms, instantiating page templates, and signing;
* other changes shall invalidate the signature;
* 3 = Permitted changes shall be the same as for 2, as well as annotation creation,
* deletion, and modification;
* other changes shall invalidate the signature.
* - extracerts (string) Specifies the name of a file containing a bunch of extra certificates
* to include in the signature
* which can for example be used to help the recipient to verify the certificate that you used.
* - info (array) Optional information.
* - ContactInfo (string)
* - Location (string)
* - Name (string)
* - Reason (string)
* - password (string)
* - privkey (string) Private key (string or filename prefixed with 'file://').
* - signcert (string) Signing certificate (string or filename prefixed with 'file://').
*/
public function setSignature(array $data): void
{
$this->signature = array_merge($this->signature, $data);
if (empty($this->signature['signcert'])) {
throw new PdfException('Invalid signing certificate (signcert)');
}
if (empty($this->signature['privkey'])) {
$this->signature['privkey'] = $this->signature['signcert'];
}
++$this->pon;
$this->objid['signature'] = $this->pon; // Signature widget annotation object id.
++$this->pon; // Signature appearance object id ($this->objid['signature'] + 1).
$this->setSignAnnotRefs();
$this->sign = true;
}
/**
* Enable or disable the the Signature Approval
*
* @param bool $enabled It true enable the Signature Approval
*/
protected function enableSignatureApproval(bool $enabled = true): static
{
$this->sigapp = $enabled;
$this->page->enableSignatureApproval($this->sigapp);
return $this;
}
/**
* Set the signature timestamp.
*
* @param TSignTimeStamp $data Signature timestamp data:
* - enabled (bool) If true enable timestamp signature.
* - host (string) Time Stamping Authority (TSA) server (prefixed with 'https://')
* - username (string) TSA username or authorization PEM file.
* - password (string) TSA password.
* - cert (string) cURL optional location of TSA certificate for authorization.
*/
public function setSignTimeStamp(array $data): void
{
$this->sigtimestamp = array_merge($this->sigtimestamp, $data);
if ($this->sigtimestamp['enabled'] && empty($this->sigtimestamp['host'])) {
throw new PdfException('Invalid TSA host');
}
}
/**
* Get a signature appearance (page and rectangle coordinates).
*
* @param float $posx Abscissa of the upper-left corner.
* @param float $posy Ordinate of the upper-left corner.
* @param float $width Width of the signature area.
* @param float $heigth Height of the signature area.
* @param int $page Page number (pid).
* @param string $name Name of the signature.
*
* @return array{
* 'name': string,
* 'page': int,
* 'rect': string,
* } Array defining page and rectangle coordinates of signature appearance.
*/
protected function getSignatureAppearanceArray(
float $posx = 0,
float $posy = 0,
float $width = 0,
float $heigth = 0,
int $page = -1,
string $name = ''
): array {
$sigapp = [];
$sigapp['page'] = ($page < 0) ? $this->page->getPageID() : $page;
$sigapp['name'] = (empty($name)) ? 'Signature' : $name;
$pntx = $this->toPoints($posx);
$pnty = $this->toYUnit(($posy + $heigth), $this->page->getPage($sigapp['page'])['pheight']);
$pntw = $this->toPoints($width);
$pnth = $this->toPoints($heigth);
$sigapp['rect'] = sprintf('%F %F %F %F', $pntx, $pnty, ($pntx + $pntw), ($pnty + $pnth));
return $sigapp;
}
/**
* Set the digital signature appearance (a cliccable rectangle area to get signature properties).
*
* @param float $posx Abscissa of the upper-left corner.
* @param float $posy Ordinate of the upper-left corner.
* @param float $width Width of the signature area.
* @param float $heigth Height of the signature area.
* @param int $page option page number (if < 0 the current page is used).
* @param string $name Name of the signature.
*/
public function setSignatureAppearance(
float $posx = 0,
float $posy = 0,
float $width = 0,
float $heigth = 0,
int $page = -1,
string $name = ''
): void {
$data = $this->getSignatureAppearanceArray($posx, $posy, $width, $heigth, $page, $name);
$this->signature['appearance']['page'] = $data['page'];
$this->signature['appearance']['name'] = $data['name'];
$this->signature['appearance']['rect'] = $data['rect'];
$this->setSignAnnotRefs();
}
/**
* Add an empty digital signature appearance (a cliccable rectangle area to get signature properties).
*
* @param float $posx Abscissa of the upper-left corner.
* @param float $posy Ordinate of the upper-left corner.
* @param float $width Width of the signature area.
* @param float $heigth Height of the signature area.
* @param int $page option page number (if < 0 the current page is used).
* @param string $name Name of the signature.
*/
public function addEmptySignatureAppearance(
float $posx = 0,
float $posy = 0,
float $width = 0,
float $heigth = 0,
int $page = -1,
string $name = ''
): void {
++$this->pon;
$data = $this->getSignatureAppearanceArray($posx, $posy, $width, $heigth, $page, $name);
$this->signature['appearance']['empty'][] = [
'objid' => $this->pon,
'name' => $data['name'],
'page' => $data['page'],
'rect' => $data['rect'],
];
$this->setSignAnnotRefs();
}
/*
* Set the signature annotation references.
*/
protected function setSignAnnotRefs(): void
{
if (empty($this->objid['signature'])) {
return;
}
if (!empty($this->signature['appearance']['page'])) {
$this->page->addAnnotRef($this->objid['signature'], $this->signature['appearance']['page']);
}
if (empty($this->signature['appearance']['empty'])) {
return;
}
foreach ($this->signature['appearance']['empty'] as $esa) {
$this->page->addAnnotRef($esa['objid'], $esa['page']);
}
}
// ===| XOBJECT |=======================================================
/**
* Create a new XObject template and return the object id.
*
* An XObject Template is a PDF block that is a self-contained description
* of any sequence of graphics objects (including path objects, text objects,
* and sampled images). An XObject Template may be painted multiple times,
* either on several pages or at several locations on the same page and
* produces the same results each time, subject only to the graphics state
* at the time it is invoked.
*
* @param float $width Width of the XObject.
* @param float $heigth Height of the XObject.
* @param ?TGTransparency $transpgroup Optional group attributes.
*
* @return string XObject template object ID.
*/
public function newXObjectTemplate(
float $width = 0,
float $heigth = 0,
?array $transpgroup = null,
): string {
$oid = ++$this->pon;
$tid = 'XT' . $oid;
$this->xobjtid = $tid;
$region = $this->page->getRegion();
if (empty($width) || $width < 0) {
$width = $region['RW'];
}
if (empty($heigth) || $heigth < 0) {
$heigth = $region['RH'];
}
$this->xobjects[$tid] = [
'spot_colors' => [],
'extgstate' => [],
'gradient' => [],
'font' => [],
'image' => [],
'xobject' => [],
'annotations' => [],
'id' => $tid,
'n' => $oid,
'x' => 0,
'y' => 0,
'w' => $width,
'h' => $heigth,
'outdata' => '',
'transparency' => $transpgroup,
];
return $tid;
}
/**
* Exit from the XObject template mode.
*
* See: newXObjectTemplate.
*/
public function exitXObjectTemplate(): void
{
$this->xobjtid = '';
}
/**
* Returns the PDF code to render the specified XObject template.
*
* See: newXObjectTemplate.
*
* @param string $tid The XObject Template object as returned by the newXObjectTemplate method.
* @param float $posx Abscissa of upper-left corner.
* @param float $posy Ordinate of upper-left corner.
* @param float $width Width.
* @param float $height Height.
* @param string $valign Vertical alignment inside the specified box: T=top; C=center; B=bottom.
* @param string $halign Horizontal alignment inside the specified box: L=left; C=center; R=right.
*
* @return string The PDF code to render the specified XObject template.
*/
public function getXObjectTemplate(
string $tid,
float $posx = 0,
float $posy = 0,
float $width = 0,
float $height = 0,
string $valign = 'T',
string $halign = 'L',
): string {
$this->xobjtid = '';
$region = $this->page->getRegion();
if (empty($this->xobjects[$tid])) {
return '';
}
$xobj = $this->xobjects[$tid];
if (empty($width) || $width < 0) {
$width = min($xobj['w'], $region['RW']);
}
if (empty($height) || $height < 0) {
$height = min($xobj['h'], $region['RH']);
}
$tplx = $this->cellHPos($posx, $width, $halign, $this->defcell);
$tply = $this->cellVPos($posy, $height, $valign, $this->defcell);
$this->bbox[] = [
'x' => $tplx,
'y' => $tply,
'w' => $width,
'h' => $height,
];
$ctm = [
0 => ($width / $xobj['w']),
1 => 0,
2 => 0,
3 => ($height / $xobj['h']),
4 => $this->toPoints($tplx),
5 => $this->toYPoints($tply + $height),
];
$out = $this->graph->getStartTransform();
$out .= $this->graph->getTransformation($ctm);
$out .= '/' . $xobj['id'] . ' Do' . "\n";
$out .= $this->graph->getStopTransform();
if (!empty($xobj['annotations'])) {
foreach ($xobj['annotations'] as $annot) {
// transform original coordinates
$clt = $this->graph->getCtmProduct(
$ctm,
array(
1,
0,
0,
1,
$this->toPoints($annot['x']),
$this->toPoints(-$annot['y']),
),
);
$anx = $this->toUnit($clt[4]);
$any = $this->toYUnit($clt[5] + $this->toUnit($height));
$crb = $this->graph->getCtmProduct(
$ctm,
array(
1,
0,
0,
1,
$this->toPoints(($annot['x'] + $annot['w'])),
$this->toPoints((-$annot['y'] - $annot['h'])),
),
);
$anw = $this->toUnit($crb[4]) - $anx;
$anh = $this->toYUnit($crb[5] + $this->toUnit($height)) - $any;
$out .= $this->setAnnotation(
$anx,
$any,
$anw,
$anh,
$annot['txt'],
$annot['opt']
);
}
}
return $out;
}
/**
* Add the specified raw PDF content to the XObject template.
*
* @param string $tid The XObject Template object as returned by the newXObjectTemplate method.
* @param string $data The raw PDF content data to add.
*/
public function addXObjectContent(string $tid, string $data): void
{
$this->xobjects[$tid]['outdata'] .= $data;
}
/**
* Add the specified XObject ID to the XObject template.
*
* @param string $tid The XObject Template object as returned by the newXObjectTemplate method.
* @param string $key The XObject key to add.
*/
public function addXObjectXObjectID(string $tid, string $key): void
{
$this->xobjects[$tid]['xobject'][] = $key;
}
/**
* Add the specified Image ID to the XObject template.
*
* @param string $tid The XObject Template object as returned by the newXObjectTemplate method.
* @param int $key TheImage key to add.
*/
public function addXObjectImageID(string $tid, int $key): void
{
$this->xobjects[$tid]['image'][] = $key;
}
/**
* Add the specified Font ID to the XObject template.
*
* @param string $tid The XObject Template object as returned by the newXObjectTemplate method.
* @param string $key The Font key to add.
*/
public function addXObjectFontID(string $tid, string $key): void
{
$this->xobjects[$tid]['font'][] = $key;
}
/**
* Add the specified Gradient ID to the XObject template.
*
* @param string $tid The XObject Template object as returned by the newXObjectTemplate method.
* @param int $key The Gradient key to add.
*/
public function addXObjectGradientID(string $tid, int $key): void
{
$this->xobjects[$tid]['gradient'][] = $key;
}
/**
* Add the specified ExtGState ID to the XObject template.
*
* @param string $tid The XObject Template object as returned by the newXObjectTemplate method.
* @param int $key The ExtGState key to add.
*/
public function addXObjectExtGStateID(string $tid, int $key): void
{
$this->xobjects[$tid]['extgstate'][] = $key;
}
/**
* Add the specified SpotColor ID to the XObject template.
*
* @param string $tid The XObject Template object as returned by the newXObjectTemplate method.
* @param string $key The SpotColor key to add.
*/
public function addXObjectSpotColorID(string $tid, string $key): void
{
$this->xobjects[$tid]['spot_colors'][] = $key;
}
// ===| LAYERS |========================================================
/**
* Creates and return a new PDF Layer.
*
* @param string $name Layer name (only a-z letters and numbers). Leave empty for automatic name.
* @param array{'view'?: bool, 'design'?: bool} $intent intended use of the graphics in the layer.
* @param bool $print Set the printability of the layer.
* @param bool $view Set the visibility of the layer.
* @param bool $lock Set the lock state of the layer.
*
* @return string
*/
public function newLayer(
string $name = '',
array $intent = [],
bool $print = true,
bool $view = true,
bool $lock = true,
): string {
$layer = sprintf('LYR%03d', (count($this->pdflayer) + 1));
$name = preg_replace('/[^a-zA-Z0-9_\-]/', '', $name);
if (empty($name)) {
$name = $layer;
}
$intarr = [];
if (!empty($intent['view'])) {
$intarr[] = '/View';
}
if (!empty($intent['design'])) {
$intarr[] = '/Design';
}
$this->pdflayer[] = array(
'layer' => $layer,
'name' => $name,
'intent' => implode(' ', $intarr),
'print' => $print,
'view' => $view,
'lock' => $lock,
'objid' => 0,
);
return ' /OC /' . $layer . ' BDC' . "\n";
}
public function closeLayer(): string
{
return 'EMC' . "\n";
}
// ===| TOC |===========================================================
/**
* Add a Table of Contents (TOC) to the document.
* The bookmars are created via the setBookmark() method.
*
* @param int $page Page number.
* @param float $posx Abscissa of the upper-left corner.
* @param float $posy Ordinate of the upper-left corner.
* @param float $width Width of the signature area.
* @param bool $rtl Right-To-Left - If true prints the TOC in RTL mode.
* @param StyleDataOpt $linestyle Line style for the space filler.
*
* @return void
*/
public function addTOC(
int $page = -1,
float $posx = 0,
float $posy = 0,
float $width = 0,
bool $rtl = false,
array $linestyle = [
'lineWidth' => 0.3,
'lineCap' => 'butt',
'lineJoin' => 'miter',
'dashArray' => [1,1],
'dashPhase' => 0,
'lineColor' => 'gray',
'fillColor' => '',
],
): void {
if (empty($width) || $width < 0) {
$width = $this->page->getRegion()['RW'];
}
$curfont = $this->font->getCurrentFont();
// width to accomodate the number (max 9 digits space).
$chrw = $this->toUnit($curfont['cw'][48] ?? $curfont['dw']); // 48 ASCII = '0'.
$indent = 2 * $chrw; // each level is indented by 2 characters.
$numwidth = 9 * $chrw; // maximum 9 digits to print the page number.
$txtwidth = ($width - $numwidth);
$cellSpaceT = $this->toUnit(
$this->defcell['margin']['T'] +
$this->defcell['padding']['T']
);
$cellSpaceB = $this->toUnit(
$this->defcell['margin']['B'] +
$this->defcell['padding']['B']
);
$cellSpaceH = $chrw + $this->toUnit(
$this->defcell['margin']['L'] +
$this->defcell['margin']['L'] +
$this->defcell['padding']['R'] +
$this->defcell['padding']['R']
);
$aligntext = 'L';
$alignnum = 'R';
$txt_posx = $posx;
$num_posx = $posx + $txtwidth;
if ($rtl) {
$aligntext = 'R';
$alignnum = 'L';
$txt_posx = $posx + $numwidth;
$num_posx = $posx;
}
$pid = ($page < 0) ? $this->page->getPageID() : $page;
foreach ($this->outlines as $bmrk) {
$font = $this->font->cloneFont(
$this->pon,
$curfont['idx'],
$bmrk['s'] . (($bmrk['l'] == 0) ? 'B' : ''),
(int) round($curfont['size'] - $bmrk['l']),
$curfont['spacing'],
$curfont['stretching'],
);
$region = $this->page->getRegion($pid);
if (($posy + $cellSpaceT + $cellSpaceB + $font['height']) > $region['RH']) {
$this->page->getNextRegion($pid);
$curpid = $this->page->getPageId();
if ($curpid > $pid) {
$pid = $curpid;
$this->setPageContext($pid);
}
$region = $this->page->getRegion($pid);
$posy = 0; // $region['RY'];
}
$this->page->addContent($this->graph->getStartTransform(), $pid);
$this->page->addContent($font['out'], $pid);
if (! empty($bmrk['c'])) {
$col = $this->color->getPdfColor($bmrk['c']);
$this->page->addContent($col, $pid);
}
if (empty($bmrk['u'])) {
$bmrk['u'] = $this->addInternalLink($bmrk['p'], $bmrk['y']);
}
$offset = ($indent * $bmrk['l']);
// add bookmark text
$this->addTextCell(
$bmrk['t'],
$pid,
$txt_posx,
$posy,
$txtwidth,
0,
$offset,
0,
'T',
$aligntext,
);
$bbox = $this->getLastBBox();
$wtxt = $bbox['w'];
$pageid = $this->page->getPageID();
if ($pageid > $pid) {
$this->page->addContent($this->graph->getStopTransform(), $pid);
$lnkid = $this->setLink(
$posx,
$posy,
$width,
($region['RH'] - $posy),
$bmrk['u'],
);
$this->page->addAnnotRef($lnkid, $pid);
$pid = $pageid;
$this->page->addContent($this->graph->getStartTransform(), $pid);
$this->page->addContent($font['out'], $pid);
}
$posy = $bbox['y'] - $cellSpaceT; // align number with the last line of the text
// add page number
$this->addTextCell(
(string) ($bmrk['p'] + 1),
$pid,
$num_posx,
$posy,
$numwidth,
0,
0,
0,
'T',
$alignnum,
);
$bbox = $this->getLastBBox();
$wnum = $bbox['w'];
// add line to fill the gap between text and number
$line_posx = ($cellSpaceH + $offset + $posx + ($rtl ? $wnum : $wtxt));
$line_posy = $bbox['y'] + $this->toUnit($font['ascent']);
$line = $this->graph->getLine(
$line_posx,
$line_posy,
$line_posx + ($width - $wtxt - $wnum - (2 * $cellSpaceH) - $offset),
$line_posy,
$linestyle,
);
$this->page->addContent($line, $pid);
$lnkid = $this->setLink(
$posx,
$bbox['y'],
$width,
$bbox['h'],
$bmrk['u'],
);
$this->page->addAnnotRef($lnkid, $pid);
$this->page->addContent($this->graph->getStopTransform(), $pid);
// Move to the next line.
$posy = $bbox['y'] + $bbox['h'] + $cellSpaceB;
}
}
}