<?php

/**
 * MetaInfo.php
 *
 * @since     2002-08-03
 * @category  Library
 * @package   Pdf
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD
 * @license   https://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.
 *
 * @phpcs:disable Generic.Files.LineLength
 */

namespace Com\Tecnick\Pdf;

use Com\Tecnick\Pdf\Exception as PdfException;

/**
 * Com\Tecnick\Pdf\MetaInfo
 *
 * Meta Informaton PDF class
 *
 * @since     2002-08-03
 * @category  Library
 * @package   Pdf
 * @author    Nicola Asuni <info@tecnick.com>
 * @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD
 * @license   https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE.TXT)
 * @link      https://github.com/tecnickcom/tc-lib-pdf
 *
 * @phpstan-import-type TViewerPref from Base
 *
 * @SuppressWarnings("PHPMD.DepthOfInheritance")
 */
abstract class MetaInfo extends \Com\Tecnick\Pdf\HTML
{
    /**
     * Valid document zoom modes
     *
     * @var array<string>
     */
    protected const VALIDZOOM = ['fullpage', 'fullwidth', 'real', 'default'];

    /**
     * Return the program version.
     */
    public function getVersion(): string
    {
        return $this->version;
    }

    /**
     * Set a field value only if it is not empty.
     *
     * @param string $field Field name
     * @param string $value Value to set
     */
    private function setNonEmptyFieldValue(string $field, string $value): static
    {
        if ($value !== '') {
            $this->$field = $value;
        }

        return $this;
    }

    /**
     * Set the value of an existing array key if it is not empty.
     *
     * @param string $field Field array name
     * @param string $key Key name
     * @param string $value Value to set
     */
    private function setNonEmptyArrayFieldValue(string $field, string $key, string $value): static
    {
        if (
            isset($this->{$field})
            && \is_array($this->{$field})
            && ($key !== '')
            && isset($this->{$field}[$key])
            && ($value !== '')
        ) {
            $this->{$field}[$key] = $value;
        }

        return $this;
    }

    /**
     * Defines the creator of the document.
     * This is typically the name of the application that generates the PDF.
     *
     * @param string $creator The name of the creator.
     */
    public function setCreator(string $creator): static
    {
        return $this->setNonEmptyFieldValue('creator', $creator);
    }

    /**
     * Defines the author of the document.
     *
     * @param string $author The name of the author.
     */
    public function setAuthor(string $author): static
    {
        return $this->setNonEmptyFieldValue('author', $author);
    }

    /**
     * Defines the subject of the document.
     *
     * @param string $subject The subject.
     */
    public function setSubject(string $subject): static
    {
        return $this->setNonEmptyFieldValue('subject', $subject);
    }

    /**
     * Defines the title of the document.
     *
     * @param string $title The title.
     */
    public function setTitle(string $title): static
    {
        return $this->setNonEmptyFieldValue('title', $title);
    }

    /**
     * Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'.
     *
     * @param string $keywords Space-separated list of keywords.
     */
    public function setKeywords(string $keywords): static
    {
        return $this->setNonEmptyFieldValue('keywords', $keywords);
    }

    /**
     * Set the PDF version (check PDF reference for valid values).
     *
     * @param string $version PDF document version.
     *
     * @throw PdfException in case of error.
     */
    public function setPDFVersion(string $version = '1.7'): static
    {
        if ($this->pdfa == 1) { // PDF/A 1 mode
            $this->pdfver = '1.4';
            return $this;
        }

        $isvalid = \preg_match('/^[1-9]+[.]\d+$/', $version);
        if ($isvalid === false) {
            throw new PdfException('Invalid PDF version format');
        }

        $this->pdfver = $version;
        return $this;
    }

    /**
     * Set the sRGB mode.
     *
     * @param bool $enabled Set to true to add the default sRGB ICC color profile
     */
    public function setSRGB(bool $enabled): static
    {
        $this->sRGB = $enabled;
        return $this;
    }

    /**
     * Returns a formatted date for meta information.
     * (ref. Chapter 7.9.4 Dates of PDF32000_2008.pdf).
     *
     * @param int $time Time in seconds.
     *
     * @return string date-time string.
     */
    protected function getFormattedDate(int $time): string
    {
        $date = \date('YmdHisp', $time);
        return \str_ends_with($date, 'Z') ? $date : \substr_replace($date, "'", -3, 1) . "'";
    }

    /**
     * Returns a formatted date for XMP meta information.
     *
     * @param int $time Time in seconds.
     *
     * @return string date-time string.
     */
    protected function getXMPFormattedDate(int $time): string
    {
        return \date('Y-m-d\TH:i:sp', $time);
    }

    /**
     * Returns the producer string.
     */
    protected function getProducer(): string
    {
        return "\x54\x43\x50\x44\x46\x20"
        . $this->version
        . "\x20\x28\x68\x74\x74\x70\x73\x3a\x2f\x2f"
        . "\x74\x63\x70\x64\x66\x2e\x6f\x72\x67\x29";
    }

    /**
     * Returns a formatted date for meta information.
     *
     * @param int $time Time in seconds.
     * @param int $oid  Current PDF object number.
     *
     * @return string escaped date-time string.
     */
    protected function getOutDateTimeString(int $time, int $oid): string
    {
        if ($time === 0) {
            $time = $this->doctime;
        }

        return $this->encrypt->escapeDataString('D:' . $this->getFormattedDate($time), $oid);
    }

    /**
     * Get the PDF output string for the Document Information Dictionary.
     * (ref. Chapter 14.3.3 Document Information Dictionary of PDF32000_2008.pdf).
     */
    protected function getOutMetaInfo(): string
    {
        $oid = ++$this->pon;
        $this->objid['info'] = $oid;
        return $oid . ' 0 obj' . "\n"
        . '<<'
        . ' /Creator ' . $this->getOutTextString($this->creator, $oid, true)
        . ' /Author ' . $this->getOutTextString($this->author, $oid, true)
        . ' /Subject ' . $this->getOutTextString($this->subject, $oid, true)
        . ' /Title ' . $this->getOutTextString($this->title, $oid, true)
        . ' /Keywords ' . $this->getOutTextString($this->keywords, $oid, true)
        . ' /Producer ' . $this->getOutTextString($this->getProducer(), $oid, true)
        . ' /CreationDate ' . $this->getOutDateTimeString($this->doctime, $oid)
        . ' /ModDate ' . $this->getOutDateTimeString($this->docmodtime, $oid)
        . ' /Trapped /False'
        . ' >>' . "\n"
        . 'endobj' . "\n";
    }

    /**
     * Escape some special characters (&lt; &gt; &amp;) for XML output.
     *
     * @param string $str Input string to escape.
     */
    protected function getEscapedXML(string $str): string
    {
        return \strtr($str, [
            "\0" => '',
            '&' => '&amp;',
            '<' => '&lt;',
            '>' => '&gt;',
        ]);
    }

    /**
     * Set additional custom XMP data to be appended just before the end of the tag indicated by the key.
     *
     * IMPORTANT:
     * This data is added as-is without controls, so you have to validate your data before using this method.
     *
     * @param string $key Key for the custom XMP data. Valid keys are:
     *                    - 'x:xmpmeta'
     *                    - 'x:xmpmeta.rdf:RDF'
     *                    - 'x:xmpmeta.rdf:RDF.rdf:Description'
     *                    - 'x:xmpmeta.rdf:RDF.rdf:Description.pdfaExtension:schemas'
     *                    - 'x:xmpmeta.rdf:RDF.rdf:Description.pdfaExtension:schemas.rdf:Bag'
     * @param string $xmp Custom XMP data.
     */
    public function setCustomXMP(string $key, string $xmp): static
    {
        return $this->setNonEmptyArrayFieldValue('custom_xmp', $key, $xmp);
    }

    /**
     * Get the PDF output string for the XMP data object
     *
     * @SuppressWarnings("PHPMD.ExcessiveMethodLength")
     */
    protected function getOutXMP(): string
    {
        $uuid = 'uuid:' . \substr($this->fileid, 0, 8)
        . '-' . \substr($this->fileid, 8, 4)
        . '-' . \substr($this->fileid, 12, 4)
        . '-' . \substr($this->fileid, 16, 4)
        . '-' . \substr($this->fileid, 20, 12);

        // @codingStandardsIgnoreStart
        $xmp = '<?xpacket begin="' . $this->uniconv->chr(0xfeff) . '" id="W5M0MpCehiHzreSzNTczkc9d"?>' . "\n"
        . '<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 4.2.1-c043 52.372728, 2009/01/18-15:08:04">' . "\n"
        . "\t" . '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' . "\n"
        . "\t\t" . '<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">' . "\n"
        . "\t\t\t" . '<dc:format>application/pdf</dc:format>' . "\n"
        . "\t\t\t" . '<dc:title>' . "\n"
        . "\t\t\t\t" . '<rdf:Alt>' . "\n"
        . "\t\t\t\t\t" . '<rdf:li xml:lang="x-default">' . $this->getEscapedXML($this->title) . '</rdf:li>' . "\n"
        . "\t\t\t\t" . '</rdf:Alt>' . "\n"
        . "\t\t\t" . '</dc:title>' . "\n"
        . "\t\t\t" . '<dc:creator>' . "\n"
        . "\t\t\t\t" . '<rdf:Seq>' . "\n"
        . "\t\t\t\t\t" . '<rdf:li>' . $this->getEscapedXML($this->author) . '</rdf:li>' . "\n"
        . "\t\t\t\t" . '</rdf:Seq>' . "\n"
        . "\t\t\t" . '</dc:creator>' . "\n"
        . "\t\t\t" . '<dc:description>' . "\n"
        . "\t\t\t\t" . '<rdf:Alt>' . "\n"
        . "\t\t\t\t\t" . '<rdf:li xml:lang="x-default">' . $this->getEscapedXML($this->subject) . '</rdf:li>' . "\n"
        . "\t\t\t\t" . '</rdf:Alt>' . "\n"
        . "\t\t\t" . '</dc:description>' . "\n"
        . "\t\t\t" . '<dc:subject>' . "\n"
        . "\t\t\t\t" . '<rdf:Bag>' . "\n"
        . "\t\t\t\t\t" . '<rdf:li>' . $this->getEscapedXML($this->keywords) . '</rdf:li>' . "\n"
        . "\t\t\t\t" . '</rdf:Bag>' . "\n"
        . "\t\t\t" . '</dc:subject>' . "\n"
        . "\t\t" . '</rdf:Description>' . "\n"
        . "\t\t" . '<rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">' . "\n"
        . "\t\t\t" . '<xmp:CreateDate>' . $this->getXMPFormattedDate($this->doctime) . '</xmp:CreateDate>' . "\n"
        . "\t\t\t" . '<xmp:CreatorTool>' . $this->getEscapedXML($this->creator) . '</xmp:CreatorTool>' . "\n"
        . "\t\t\t" . '<xmp:ModifyDate>' . $this->getXMPFormattedDate($this->docmodtime) . '</xmp:ModifyDate>' . "\n"
        . "\t\t\t" . '<xmp:MetadataDate>' . $this->getXMPFormattedDate($this->doctime) . '</xmp:MetadataDate>' . "\n"
        . "\t\t" . '</rdf:Description>' . "\n"
        . "\t\t" . '<rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">' . "\n"
        . "\t\t\t" . '<pdf:Keywords>' . $this->getEscapedXML($this->keywords) . '</pdf:Keywords>' . "\n"
        . "\t\t\t" . '<pdf:Producer>' . $this->getEscapedXML($this->getProducer()) . '</pdf:Producer>' . "\n"
        . "\t\t" . '</rdf:Description>' . "\n"
        . "\t\t" . '<rdf:Description rdf:about="" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/">' . "\n"
        . "\t\t\t" . '<xmpMM:DocumentID>' . $uuid . '</xmpMM:DocumentID>' . "\n"
        . "\t\t\t" . '<xmpMM:InstanceID>' . $uuid . '</xmpMM:InstanceID>' . "\n"
        . "\t\t" . '</rdf:Description>' . "\n";

        if ($this->pdfa !== 0) {
            $xmp .= '		<rdf:Description rdf:about="" xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">' . "\n"
            . "\t\t\t" . '<pdfaid:part>' . $this->pdfa . '</pdfaid:part>' . "\n"
            . "\t\t\t" . '<pdfaid:conformance>B</pdfaid:conformance>' . "\n"
            . "\t\t" . '</rdf:Description>' . "\n";
        }

        // XMP extension schemas
        $xmp .= "\t\t" . '<rdf:Description rdf:about="" xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/" xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#" xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#">' . "\n"
        . "\t\t\t" . '<pdfaExtension:schemas>' . "\n"
        . "\t\t\t\t" . '<rdf:Bag>' . "\n"
        . "\t\t\t\t\t" . '<rdf:li rdf:parseType="Resource">' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:namespaceURI>http://ns.adobe.com/pdf/1.3/</pdfaSchema:namespaceURI>' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:prefix>pdf</pdfaSchema:prefix>' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:schema>Adobe PDF Schema</pdfaSchema:schema>' . "\n"
        . "\t\t\t\t\t" . '</rdf:li>' . "\n"
        . "\t\t\t\t\t" . '<rdf:li rdf:parseType="Resource">' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:namespaceURI>http://ns.adobe.com/xap/1.0/mm/</pdfaSchema:namespaceURI>' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:prefix>xmpMM</pdfaSchema:prefix>' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:schema>XMP Media Management Schema</pdfaSchema:schema>' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:property>' . "\n"
        . "\t\t\t\t\t\t\t" . '<rdf:Seq>' . "\n"
        . "\t\t\t\t\t\t\t\t" . '<rdf:li rdf:parseType="Resource">' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:category>internal</pdfaProperty:category>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:description>UUID based identifier for specific incarnation of a document</pdfaProperty:description>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:name>InstanceID</pdfaProperty:name>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:valueType>URI</pdfaProperty:valueType>' . "\n"
        . "\t\t\t\t\t\t\t\t" . '</rdf:li>' . "\n"
        . "\t\t\t\t\t\t\t" . '</rdf:Seq>' . "\n"
        . "\t\t\t\t\t\t" . '</pdfaSchema:property>' . "\n"
        . "\t\t\t\t\t" . '</rdf:li>' . "\n"
        . "\t\t\t\t\t" . '<rdf:li rdf:parseType="Resource">' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:namespaceURI>http://www.aiim.org/pdfa/ns/id/</pdfaSchema:namespaceURI>' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:prefix>pdfaid</pdfaSchema:prefix>' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:schema>PDF/A ID Schema</pdfaSchema:schema>' . "\n"
        . "\t\t\t\t\t\t" . '<pdfaSchema:property>' . "\n"
        . "\t\t\t\t\t\t\t" . '<rdf:Seq>' . "\n"
        . "\t\t\t\t\t\t\t\t" . '<rdf:li rdf:parseType="Resource">' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:category>internal</pdfaProperty:category>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:description>Part of PDF/A standard</pdfaProperty:description>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:name>part</pdfaProperty:name>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:valueType>Integer</pdfaProperty:valueType>' . "\n"
        . "\t\t\t\t\t\t\t\t" . '</rdf:li>' . "\n"
        . "\t\t\t\t\t\t\t\t" . '<rdf:li rdf:parseType="Resource">' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:category>internal</pdfaProperty:category>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:description>Amendment of PDF/A standard</pdfaProperty:description>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:name>amd</pdfaProperty:name>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:valueType>Text</pdfaProperty:valueType>' . "\n"
        . "\t\t\t\t\t\t\t\t" . '</rdf:li>' . "\n"
        . "\t\t\t\t\t\t\t\t" . '<rdf:li rdf:parseType="Resource">' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:category>internal</pdfaProperty:category>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:description>Conformance level of PDF/A standard</pdfaProperty:description>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:name>conformance</pdfaProperty:name>' . "\n"
        . "\t\t\t\t\t\t\t\t\t" . '<pdfaProperty:valueType>Text</pdfaProperty:valueType>' . "\n"
        . "\t\t\t\t\t\t\t\t" . '</rdf:li>' . "\n"
        . "\t\t\t\t\t\t\t" . '</rdf:Seq>' . "\n"
        . "\t\t\t\t\t\t" . '</pdfaSchema:property>' . "\n"
        . "\t\t\t\t\t" . '</rdf:li>' . "\n"
        . $this->custom_xmp['x:xmpmeta.rdf:RDF.rdf:Description.pdfaExtension:schemas.rdf:Bag'] . "\n"
        . "\t\t\t\t" . '</rdf:Bag>' . "\n"
        . $this->custom_xmp['x:xmpmeta.rdf:RDF.rdf:Description.pdfaExtension:schemas'] . "\n"
        . "\t\t\t" . '</pdfaExtension:schemas>' . "\n"
        . $this->custom_xmp['x:xmpmeta.rdf:RDF.rdf:Description'] . "\n"
        . "\t\t" . '</rdf:Description>' . "\n"
        . $this->custom_xmp['x:xmpmeta.rdf:RDF'] . "\n"
        . "\t" . '</rdf:RDF>' . "\n"
        . $this->custom_xmp['x:xmpmeta'] . "\n"
        . '</x:xmpmeta>' . "\n"
        . '<?xpacket end="w"?>';
        // @codingStandardsIgnoreEnd

        $oid = ++$this->pon;
        $this->objid['xmp'] = $oid;

        return $oid . ' 0 obj' . "\n"
        . '<<'
        . ' /Type /Metadata'
        . ' /Subtype /XML'
        . ' /Length ' . \strlen($xmp)
        . ' >> stream' . "\n"
        . $xmp . "\n"
        . 'endstream' . "\n"
        . 'endobj' . "\n";
    }

    /**
     * Set the viewer preferences dictionary
     * controlling the way the document is to be presented on the screen or in print.
     *
     * @param TViewerPref $pref Array of options (see PDF reference "Viewer Preferences").
     */
    public function setViewerPreferences(array $pref): static
    {
        $this->viewerpref = $pref;
        return $this;
    }

    /**
     * Sanitize the page box name and return the default 'CropBox' in case of error.
     *
     * @param string $name Entry name.
     */
    protected function getPageBoxName(string $name): string
    {
        $box = 'CropBox';
        if (isset($this->viewerpref[$name])) {
            $val = $this->viewerpref[$name];
            if (
                isset($this->page->{$box}[$val]) // @phpstan-ignore offsetAccess.nonOffsetAccessible
                && \is_string($this->page->{$box}[$val])
            ) {
                $box = $this->page->{$box}[$val];
            }
        }

        return ' /' . $name . ' /' . $box;
    }

    /**
     * Sanitize the page box name and return the default 'CropBox' in case of error.
     */
    protected function getPagePrintScaling(): string
    {
        $mode = 'AppDefault';
        if (isset($this->viewerpref['PrintScaling'])) {
            $name = \strtolower($this->viewerpref['PrintScaling']);
            $valid = [
                'none' => 'None',
                'appdefault' => 'AppDefault',
            ];
            if (isset($valid[$name])) {
                $mode = $valid[$name];
            }
        }

        return ' /PrintScaling /' . $mode;
    }

    /**
     * Returns the Duplex mode for the Viewer Preferences
     */
    protected function getDuplexMode(): string
    {
        if (isset($this->viewerpref['Duplex'])) {
            $name = \strtolower($this->viewerpref['Duplex']);
            $valid = [
                'simplex' => 'Simplex',
                'duplexflipshortedge' => 'DuplexFlipShortEdge',
                'duplexfliplongedge' => 'DuplexFlipLongEdge',
            ];
            if (isset($valid[$name])) {
                return ' /Duplex /' . $valid[$name];
            }
        }

        return '';
    }

    /**
     * Returns the Viewer Preference boolean entry.
     *
     * @param string $name Entry name.
     */
    protected function getBooleanMode(string $name): string
    {
        if (isset($this->viewerpref[$name])) {
            return ' /' . $name . ' ' . ($this->viewerpref[$name] === true ? 'true' : 'false');
        }

        return '';
    }

    /**
     * Returns the PDF viewer preferences for the catalog section
     */
    protected function getOutViewerPref(): string
    {
        $vpr = $this->viewerpref;
        $out = ' /ViewerPreferences <<';
        if ($this->rtl) {
            $out .= ' /Direction /R2L';
        } else {
            $out .= ' /Direction /L2R';
        }

        $out .= $this->getBooleanMode('HideToolbar');
        $out .= $this->getBooleanMode('HideMenubar');
        $out .= $this->getBooleanMode('HideWindowUI');
        $out .= $this->getBooleanMode('FitWindow');
        $out .= $this->getBooleanMode('CenterWindow');
        $out .= $this->getBooleanMode('DisplayDocTitle');
        if (isset($vpr['NonFullScreenPageMode'])) {
            $out .= ' /NonFullScreenPageMode /' . $this->page->getDisplay($vpr['NonFullScreenPageMode']);
        }

        $out .= $this->getPageBoxName('ViewArea');
        $out .= $this->getPageBoxName('ViewClip');
        $out .= $this->getPageBoxName('PrintArea');
        $out .= $this->getPageBoxName('PrintClip');
        $out .= $this->getPagePrintScaling();
        $out .= $this->getDuplexMode();
        $out .= $this->getBooleanMode('PickTrayByPDFSize');
        if (isset($vpr['PrintPageRange'])) {
            $PrintPageRangeNum = '';
            foreach ($vpr['PrintPageRange'] as $pnum) {
                $PrintPageRangeNum .= ' ' . ($pnum - 1) . '';
            }

            $out .= ' /PrintPageRange [' . $PrintPageRangeNum . ' ]';
        }

        if (isset($vpr['NumCopies'])) {
            $out .= ' /NumCopies ' . (int) $vpr['NumCopies'];
        }

        return $out . ' >>';
    }
}
