jkuchar/PdfResponse

View on GitHub
src/PdfResponse.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

/**
 * PdfResponse
 * -----------
 * Wrapper of mPDF.
 * Generate PDF from Nette Framework in one line.
 *
 * @author     Jan Kuchař
 * @copyright  Copyright (c) 2010 Jan Kuchař (http://mujserver.net)
 * @license    LGPL
 * @link       http://addons.nettephp.com/cs/pdfresponse
 */

namespace PdfResponse;

use Mpdf\Mpdf;
use Nette\Http\IRequest;
use Nette\Http\IResponse;
use Nette\SmartObject;
use Nette\Utils\Strings;
use PHPHtmlParser\Dom;

/**
 * @property-read Mpdf $mPDF
 */
class PdfResponse implements \Nette\Application\IResponse {

    use SmartObject;

    /**
     * Source data
     * @var mixed
     */
    private $source;

    /** @var array */
    public $domOptions = [];


    /**
     * Callback - create mPDF object
     * @var callable
     */
    public $createMPDF = null;


    /**
     * Portrait page orientation
     */
    const ORIENTATION_PORTRAIT  = 'P';

    /**
     * Landscape page orientation
     */
    const ORIENTATION_LANDSCAPE = 'L';

    /**
     * Specifies page orientation.
     *
     * You can use constants:
     * <ul>
     *   <li>PdfResponse::ORIENTATION_PORTRAIT (default)
     *   <li>PdfResponse::ORIENTATION_LANDSCAPE
     * </ul>
     *
     * <b>In some usages this may not work.</b>
     * If this setting does not work for you,
     * you can specify page orientation by setting
     * page size and orientation in style sheet of
     * source document.
     * <pre>
     * @page { sheet-size: A4-L; }
     * </pre>
     *
     * @var string
     */
    public $pageOrientation = self::ORIENTATION_PORTRAIT;


    /**
     * Specifies format of the document<br>
     * <br>
     * Allowed values: (Values are case-<b>in</b>sensitive)
     * <ul>
     *   <li>A0 - A10
     *   <li>B0 - B10
     *   <li>C0 - C10
     *   <li>4A0
     *   <li>2A0
     *   <li>RA0 - RA4
     *   <li>SRA0 - SRA4
     *   <li>Letter
     *   <li>Legal
     *   <li>Executive
     *   <li>Folio
     *   <li>Demy
     *   <li>Royal
     *   <li>A<i> (Type A paperback 111x178mm)</i>
     *   <li>B<i> (Type B paperback 128x198mm)</i>
     * </ul>
     *
     * @var string
     */
    public $pageFormat = 'A4';

    /**
     * Margins in this order:
     * <ol>
     *   <li>top
     *   <li>right
     *   <li>bottom
     *   <li>left
     *   <li>header
     *   <li>footer
     * </ol>
     *
     * Please use values <b>higer than 0</b>. In some PDF browser zero values may
     * cause problems!
     *
     * @var string
     */
    public $pageMargins = '16,15,16,15,9,9';

    /**
     * Author of the document
     * @var string
     */
    public $documentAuthor = 'Nette Framework - Pdf response';

    /**
     * Title of the document
     * @var string
     */
    public $documentTitle = 'Unnamed document';

    /**
     * This parameter specifies the magnification (zoom) of the display when the document is opened.<br>
     * Values (case-<b>sensitive</b>)
     * <ul>
     *   <li><b>fullpage</b>: Fit a whole page in the screen
     *   <li><b>fullwidth</b>: Fit the width of the page in the screen
     *   <li><b>real</b>: Display at real size
     *   <li><b>default</b>: User's default setting in Adobe Reader
     *   <li><i>integer</i>: Display at a percentage zoom (e.g. 90 will display at 90% zoom)
     * </ul>
     *
     * @var string|int
     */
    public $displayZoom = 'default';

    /**
     * Specify the page layout to be used when the document is opened.<br>
     * Values (case-<b>sensitive</b>)
     * <ul>
     *   <li><b>single</b>: Display one page at a time
     *   <li><b>continuous</b>: Display the pages in one column
     *   <li><b>two</b>: Display the pages in two columns
     *   <li><b>default</b>: User's default setting in Adobe Reader
     * </ul>
     *
     * @var string
     */
    public $displayLayout = 'continuous';

    /**
     * This parameter specifie the directory to be used as a temp dir when generating PDF content.<br/>
     * If it is empty, mPDF default value is used.
     *
     * @var string|null
     */
    public $tempDir = null;

    /**
     * Before document output starts
     * @var callable|null
     */
    public $onBeforeComplete = null;

    /**
     * Before document write starts
     * @var callable|null
     */
    public $onBeforeWrite = null;

    /**
     * Multi-language document?
     * @var bool
     */
    public $multiLanguage = false;

    /**
     * Additional stylesheet as a <b>string</b>
     * @var string
     */
    public $styles = '';

    /**
     * <b>Ignore</b> styles in HTML document
     * When using this feature, you MUST also install SimpleHTMLDom to your application!
     * @var bool
     */
    public $ignoreStylesInHTMLDocument = false;

    /**
     * mPDF instance
     * @var Mpdf
     */
    private $mPDF = null;

    /**
     * Document name on output
     * @var string
     */
    public $outputName = null;

    /**
     * send the file inline to the browser. The plug-in is used if available. The name given by filename is used when one selects the "Save as" option on the link generating the PDF.
     */
    const OUTPUT_INLINE = 'I';

    /**
     * send to the browser and force a file download with the name given by filename.
     */
    const OUTPUT_DOWNLOAD = 'D';

    /**
     * save to a local file with the name given by filename (may include a path).
     */
    const OUTPUT_FILE = 'F';

    /**
     * return the document as a string. filename is ignored.
     */
    const OUTPUT_STRING = 'S';

    /**
     * Output destination
     * @var string
     */
    public $outputDestination = self::OUTPUT_INLINE;

    /**
     * Getts margins as <b>array</b>
     * @return array
     */
    function getMargins() {
        $margins = explode(',', $this->pageMargins);
        if(count($margins) !== 6) {
            throw new \Nette\InvalidStateException('You must specify all margins! For example: 16,15,16,15,9,9');
        }

        $dictionary = array(
            0 => 'top',
            1 => 'right',
            2 => 'bottom',
            3 => 'left',
            4 => 'header',
            5 => 'footer'
        );

        $marginsOut = array();
        foreach($margins AS $key => $val) {
            $val = (int)$val;
            if($val < 0) {
                throw new \Nette\InvalidArgumentException('Margin must not be negative number!');
            }
            $marginsOut[$dictionary[$key]] = $val;
        }

        return $marginsOut;
    }

    /**
     * PdfResponse constructor.
     * @param $source
     */
    public function __construct($source) {
        $this->createMPDF = array($this, 'createMPDF');
        $this->source = $source;
    }


    function openPrintDialog() {
        $this->getMPDF()->SetJS('print()');
    }

    /**
     * Getts source document html
     * @return string
     * @throws \Nette\InvalidStateException
     */
    public function getSource() {
        $source = $this->getRawSource();

        // String given
        if(is_string($source)) {
            return $source;
        };

        // Nette template given
        if ($source instanceof \Nette\Application\UI\ITemplate ) {
            $source->pdfResponse = $this;
            $source->mPDF = $this->getMPDF();
            return (string) $source;

        };

        // Other case - not supported
        throw new \Nette\InvalidStateException('Source is not supported! (type: ' .
            (is_object($source) ? ('object of class ' . get_class($source)) : gettype($source)).
            ')');
    }

    public function getRawSource() {
        if(!$this->source) {
            throw new \Nette\InvalidStateException('Source is not defined!');
        }

        return $this->source;
    }



    /**
     * Sends response to output.
     */
    public function send(IRequest $httpRequest, IResponse $httpResponse): void {
        // Throws exception if sources can not be processed
        $html = $this->getSource();

        // Fix: $html can't be empty (mPDF generates Fatal error)
        if(empty($html)) {
            $html = '<html><body></body></html>';
        }

        $mpdf = $this->getMPDF();
        $mpdf->biDirectional = $this->multiLanguage;
        $mpdf->SetAuthor($this->documentAuthor);
        $mpdf->SetTitle($this->documentTitle);
        $mpdf->SetDisplayMode($this->displayZoom, $this->displayLayout);

        // @see: http://mpdf1.com/manual/index.php?tid=121&searchstring=writeHTML
        if($this->ignoreStylesInHTMLDocument) {

            // copied from mPDF -> removes comments
            $html = preg_replace('/<!--mpdf/i','',$html);
            $html = preg_replace('/mpdf-->/i','',$html);
            $html = preg_replace('/<\!\-\-.*?\-\->/s','',$html);

            // deletes all <style> tags

            $parsedHtml =  $this->createDom();
            $parsedHtml->loadStr($html);
            foreach($parsedHtml->find('style') AS $el) {
                $el->outertext = '';
            }
            $html = $parsedHtml->__toString();

            $mode = 2; // If <body> tags are found, all html outside these tags are discarded, and the rest is parsed as content for the document. If no <body> tags are found, all html is parsed as content. Prior to mPDF 4.2 the default CSS was not parsed when using mode #2
        }else {
            $mode = 0; // Parse all: HTML + CSS
        }

        Utils::tryCall($this->onBeforeWrite);

        // Add content
        $mpdf->WriteHTML(
            $html,
            $mode
        );

        // Add styles
        if(!empty($this->styles)) {
            $mpdf->WriteHTML(
                $this->styles,
                1
            );
        }

        Utils::tryCall($this->onBeforeComplete);

        if(!$this->outputName) {
            $this->outputName = Strings::webalize($this->documentTitle). '.pdf';
        }

        $mpdf->Output($this->outputName,$this->outputDestination);
    }


    /**
     * Returns mPDF object
     * @return Mpdf
     */
    public function getMPDF() {
        if(!$this->mPDF instanceof Mpdf) {
            if(\is_callable($this->createMPDF)) {
                $factory = $this->createMPDF;
                $mpdf = $factory();
                if(!$mpdf instanceof Mpdf) {
                    throw new \Nette\InvalidStateException('Callback function createMPDF must return mPDF object!');
                }
                $this->mPDF = $mpdf;
            }else
                throw new \Nette\InvalidStateException('Callback createMPDF is not callable!');
        }
        return $this->mPDF;
    }


    /**
     * Creates and returns mPDF object
     * @return Mpdf
     */
    public function createMPDF() {
        $margins = $this->getMargins();
        $config = [
            'mode' => 'utf-8',
            'format' => $this->pageFormat,
            'default_font_size' => '',
            'default_font' => '',
            'margin_left' => $margins['left'],
            'margin_right' => $margins['right'],
            'margin_top' => $margins['top'],
            'margin_bottom' => $margins['bottom'],
            'margin_header' => $margins['header'],
            'margin_footer' => $margins['footer'],
            'orientation' => $this->pageOrientation,
        ];
        if ($this->tempDir !== null) {
            $config['tempDir'] = $this->tempDir;
        }

        return new Mpdf($config);
    }


    /**
     * Creates and returns Dom object
     * @return Dom
     */
    private function createDom() {
        $options = $this->domOptions;

        if (empty($options))
        {
            $options = [
                'removeStyles' => FALSE
            ];
        }

        $dom = new Dom();
        $dom->setOptions($options);

        return $dom;
    }
}