<?php

class HTML2PDF {
    private 
string $node;
    private 
string $script;
    private 
string $baseHref;
    private 
bool $enableBackgrounds false;
    private 
string $orientation self::PORTRAIT;
    private 
string $format self::LETTER;
    private 
string $header '';
    private 
string $footer '';
    private array 
$pageMargins = [
        
'top' => '0.125in'
        
'bottom' => '0.125in'
        
'left' => '0.25in'
        
'right' => '0.25in'
    
];

    const 
PORTRAIT 'portrait';
    const 
LANDSCAPE 'landscape';

    const 
LETTER 'Letter';
    const 
A4 'A4';

    public function 
__construct(string $node 'node'string $script 'main.js'string $baseHref null){
        
$this->node $node;
        
$this->script $script;
        if (
$baseHref === null){
            
$baseHref sprintf('%s://%s/'
                
, ($_SERVER['HTTPS'] ?? 'off') === 'on' 'https' 'http'
                
$_SERVER['HTTP_HOST'] ?? 'localhost'
            
);
        }
        
$this->baseHref $baseHref;
    }

    public function 
setHeaderHTML($html) : self{
        
$this->header $html;

        return 
$this;
    }

    public function 
setFooterHTML($html) : self{
        
$this->footer $html;

        return 
$this;
    }

    
/**
     * Set page margins using CSS style dimensions, including units.  Ex: 0.25in
     *
     * @param ?string $top
     * @param ?string $right
     * @param ?string $bottom
     * @param ?string $left
     *
     * @return void
     */
    
public function setPageMargins(?string $top null, ?string $right null, ?string $bottom null, ?string $left null) : self{
        
$this->pageMargins = [
            
'top' => $top ?? $this->pageMargins['top']
            , 
'bottom' => $bottom ?? $this->pageMargins['bottom']
            , 
'left' => $left ?? $this->pageMargins['left']
            , 
'right' => $right ?? $this->pageMargins['right']
        ];

        return 
$this;
    }

    public function 
setOrientation(string $orientation) : self{
        
$this->orientation $orientation;

        return 
$this;
    }

    public function 
setFormat(string $format) : self{
        
$this->format $format;

        return 
$this;
    }

    public function 
setEnableBackgrounds(bool $value) : self{
        
$this->enableBackgrounds $value;

        return 
$this;
    }

    
/**
     * Generate the PDF and return it as a binary string.
     *
     * @param string $html
     *
     * @return string
     * @throws \RuntimeException
     */
    
public function generatePdf(string $html) : string{
        
$source $destination $destinationPdf $pdf null;
        
$html $this->fixupHtml($html);

        try {
            [
$source$destination$destinationPdf] = $this->createTemporaryFiles();
            
file_put_contents($source$html);

            
$cmd $this->createCommandLine($source$destinationPdf);
            
exec($cmd$output$ret);
            if (
$ret !== 0){
                throw new 
\RuntimeException('Failed to generate PDF with command [' $cmd '] Output: ' implode(PHP_EOL$output));
            }

            
$pdf file_get_contents($destinationPdf);
        } finally {
            
$this->cleanupTemporaryFiles($source$destination$destinationPdf);
        }

        return 
$pdf;
    }

    
/**
     * Generate the PDF and output it to the browser.
     *
     * @param string $html HTML to convert
     * @param string $name Filename to use for the PDF
     * @param string $mode Disposition mode (attachment or inline)
     *
     * @throws Exception
     */
    
public function outputPdf(string $htmlstring $name 'page.pdf'string $mode 'inline') : void{
        
$type 'application/pdf';
        
$content $this->generatePdf($html);

        
header('Content-type: ' $type);
        
header('Content-length: ' strlen($content));
        
header('Cache-control: private max-age=30 must-revalidate');
        
header(sprintf('Content-disposition: %s; filename="%s"'$mode$name));
        echo 
$content;
    }

    public function 
saveTo(string $htmlstring $file) : bool{
        return 
file_put_contents($file$this->generatePdf($html)) !== false;
    }

    private function 
createTemporaryFiles() : array{
        
$source tempnam(sys_get_temp_dir(), 'htmlpdf');
        
$destination tempnam(sys_get_temp_dir(), 'htmlpdf');
        if (
$source === false || $destination === false){
            
$this->cleanupTemporaryFiles($source$destination);
            throw new 
\RuntimeException('Could not generate temporary files.');
        }

        
$destinationPdf $destination '.pdf';
        
$fp fopen($destinationPdf'x+');
        if (!
$fp){
            
$this->cleanupTemporaryFiles($source$destination$destinationPdf);
            throw new 
\RuntimeException('Could not generate destination pdf file.');
        }

        return [
$source$destination$destinationPdf];
    }

    private function 
createCommandLine(string $sourcestring $destination) : string{
        
$options json_encode([
            
'landscape' => $this->orientation === 'landscape'
            
'path' => $destination
            
'printBackground' => $this->enableBackgrounds
            
'format' => $this->format
            
'headerTemplate' => $this->header
            
'footerTemplate' => $this->footer
            
'displayHeaderFooter' => ($this->header || $this->footer)
            , 
'margin' => $this->pageMargins
        
]);

        
$cmd sprintf("\"%s\" %s %s %s 2>&1"
            
$this->node
            
$this->escape($this->script)
            , 
$this->escape($source)
            , 
$this->escape($options)
        );

        return 
$cmd;
    }

    private function 
cleanupTemporaryFiles(string $sourcestring $destination, ?string $destinationPdf null) : void{
        
$source && unlink($source);
        
$destination && unlink($destination);
        
$destinationPdf && unlink($destinationPdf);
    }

    private function 
escape($value) : string{
        if (
PHP_OS === 'WINNT'){
            
$value str_replace(['"'], ['""'], $value);
            
$value '"' $value '"';
        } else {
            
$value escapeshellarg($value);
        }

        return 
$value;
    }

    private function 
fixupHtml(string $html) : string{
        
libxml_use_internal_errors(true);
        
$dom = new \DOMDocument('1.0''utf-8');
        
$dom->loadHTML($html);
        
libxml_use_internal_errors(false);

        
$this->setupBaseHref($dom);
        
$this->setupClasses($dom);

        return 
$dom->saveHTML();
    }

    private function 
setupBaseHref(DOMDocument $dom) : void{
        
$baseList $dom->getElementsByTagName('base');
        
$hasBaseHref false;
        for (
$idx 0$idx $baseList->length$idx++){
            
/** @var DOMElement $base */
            
$base $baseList->item($idx);
            if (
$base->hasAttribute('href')){
                
$hasBaseHref true;
            }
        }

        if (!
$hasBaseHref){
            
$base $dom->createElement('base');
            
$base->setAttribute('href'$this->baseHref);
            
$head $dom->getElementsByTagName('head')->item(0);
            
$head->insertBefore($base$head->firstChild);
        }
    }

    private function 
setupClasses(DOMDocument $dom) : void{
        
$html $dom->documentElement;
        
$classList preg_split('/\s+/'$html->getAttribute('class') ?? '');
        
$classList[] = 'o-' strtolower($this->orientation);

        
$html->setAttribute('class'implode(' '$classList));
    }
}