Source of file PDFService.php

Size: 14,967 Bytes - Last Modified: 2023-11-16T22:56:03+01:00

/home/websites/teicee/packagist/site/phpdoc/conf/../vendor/teicee/dpdf-bundle/src/Base/PDFService.php

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
<?php
namespace TIC\DpdfBundle\Base;

use Twig\Environment;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;

/**
 * Classe parent pour les services de génération de PDF
 * (à partir d'un contenu HMTL ou d'une vue Twig).
 */
abstract class PDFService
{
	protected $twig;
	protected $encore;
	protected $temp_files = array();
	protected $debug = false;

	/**
	 * Types mimes & extensions de fichiers disponibles
	 */
	const MIME_TYPES = array(
		'PDF' => [ ".pdf", "application/pdf" ],
		'PNG' => [ ".png", "image/png"       ],
#		'JPG' => [ ".jpg", "image/jpeg"      ],
	);

	/**
	 * Valeurs par défaut (options & chemins) pour les générations Twig|HTML vers PDF|PNG
	 */
	protected $confs = array(
		'debug'             => false,           // dump des options, debug CSS et fichier de log
		'generator'         => "auto",          // auto|pdf = PDFLib|CPDF|wkhtmltopdf ; image|png = GD|wkhtmltoimage
		'pdflib_license'    => null,            // [dompdf] si usage de PDFLib avec une licence
		'media_type'        => "screen",        // CSS3 media type: screen | print
		'paper_size'        => "A4",            // page format: A4, letter...
		'orientation'       => "portrait",      // page orientation: portrait | landscape
		'default_font'      => "DejaVu Sans",   // [dompdf] helvetica, times-roman, courier...
		'font_ratio'        => 1.1,             // [dompdf] ratio sur la hauteur (pour rendu proche des navigateurs)
		'dpi'               => 96,              // DPI setting (images & fonts)
		'default_twigview'  => "layout_print",  // nom de la vue twig par défault (sans l'extension ".html.twig")
		'default_filename'  => "document",      // nom du fichier de sortie (sans l'extension ".pdf")
		'base_url'          => null,            // URL complète absolue pour résoudre les liens relatifs
	);
	protected $paths = array(
		'root_dir'          => null,            // racine des fichiers accessibles en chargement (chroot|allow)
		'path_source'       => null,            // dossier parent par défaut pour les fichiers chargés (HTML)
		'path_output'       => null,            // dossier parent par défaut pour les fichiers générés (PDF|PNG)
		'path_logdir'       => null,            // dossier parent pour les fichiers de log (debug)
		'path_tmpdir'       => null,            // dossier parent pour les fichiers temporaires (défaut sys_get_temp_dir)
		'wkhtml_pdf'        => __DIR__ . '../../../../bin/wkhtmltopdf-amd64',    // [wkhtml]
		'wkhtml_image'      => __DIR__ . '../../../../bin/wkhtmltoimage-amd64',  // [wkhtml]
	);

	/**
	 * Construction du service en chargeant la configuration des parameters.
	 *
	 * @param array                     $confs          Liste des options pour le paramétrage du service
	 * @param array                     $paths          Liste des chemins pour le paramétrage du service
	 * @param Environment               $twig           Instance du moteur de rendu pour générer les vues HTML
	 * @param TranslatorInterface       $translator     Service d'internationalisation pour changer la locale
	 * @param EntrypointLookupInterface $encore         Accès à la réinitialisation de Webpack Encore
	 */
	public function __construct(?array $confs = [], ?array $paths = [], ?Environment $twig = null, ?TranslatorInterface $translator = null, ?EntrypointLookupInterface $encore = null)
	{
		$this->twig   = $twig;
		$this->trans  = $translator;
		$this->encore = $encore;
		$this->confs  = \array_merge($this->confs, $confs);
		$this->paths  = \array_merge($this->paths, $paths);
		if (isset($this->confs['debug'])) $this->debug = (bool)$this->confs['debug'];
#		\register_shutdown_function([$this, 'purgeTempFiles']); // ne fonctionne pas et empêche le destruct (qui marche sinon) !
	}

	public function __destruct()
	{
		$this->purgeTempFiles();
	}

	/**
	 * Suppression des fichiers temporaires (sauf en mode debug).
	 */
	protected function purgeTempFiles()
	{
		if ($this->debug) return;
		
		foreach ($this->temp_files as $file_path) {
			if (\file_exists($file_path)) \unlink($file_path);
		}
		$this->temp_files = array();
	}

	/**
	 * Création d'un fichier temporaire avec le contenu donné.
	 *
	 * @param   string  $prefix         Chaine pour le début du nom du fichier (sous-dossier si commence par un /)
	 * @param   string  $suffix         Chaine à utiliser comme extension du nom du fichier
	 * @param   string  $file_data      Contenu à placer dans le fichier à générer
	 * @return  string                  Chemin absolu du fichier temporaire (généré/à générer)
	 */
	protected function createTempFile(string $prefix = "", ?string $suffix = null, string $file_data = null): string
	{
		$folder = "";
		if (\strlen($prefix) && (\substr($prefix,0,1) === "/")) {
			$folder = $prefix;
			$prefix = "";
		}
		
		$tmpdir = \sys_get_temp_dir();
		if (! empty($this->paths['path_tmpdir'])) {
			if (! \is_dir($this->paths['path_tmpdir'].$folder)) @\mkdir($this->paths['path_tmpdir'].$folder, 0750, true);
			$tmpdir = $this->paths['path_tmpdir'].$folder;
		}
		
		if (! \is_writable($tmpdir))
			throw new RuntimeException(\sprintf("Unable to write in directory: %s\n", $tmpdir));
		
		$file_path = $tmpdir . "/" . \uniqid($prefix, true);
		if (null !== $suffix) $file_path.= "." . $suffix;
		
		if (null !== $file_data) \file_put_contents($file_path, $file_data);
		$this->temp_files[] = $file_path;
		return $file_path;
	}

// ------------------------------------------------------------------------------------------ CONFIG

	/**
	 * Initialisation du moteur html2pdf avec ses options.
	 *
	 * @param   array   $pdf_options    Surcharge des options du moteur html2pdf
	 * @return  PDFService
	 */
	abstract public function initEngine(array $pdf_options = []): PDFService;

// ------------------------------------------------------------------------------------------ LOADER

	/**
	 * Initialisation du moteur html2pdf à partir d'un fichier HTML.
	 *
	 * @param   string  $html_file      Chemin du fichier HTML à charger
	 * @param   array   $pdf_options    Surcharge des options du moteur html2pdf
	 * @return  PDFService
	 */
	abstract public function loadHtmlFile(string $html_file, array $pdf_options = []): PDFService;

	/**
	 * Initialisation du moteur html2pdf à partir d'un contenu HTML.
	 *
	 * @param   string  $html_data      Contenu du document HTML à charger
	 * @param   array   $pdf_options    Surcharge des options du moteur html2pdf
	 * @return  PDFService
	 */
	abstract public function loadHtmlData(string $html_data, array $pdf_options = []): PDFService;

	/**
	 * Initialisation du moteur html2pdf à partir d'une vue Twig à générer.
	 * NOTE: avec gestion de la locale et reset de webpack encore (en cas d'usages multiples)
	 *
	 * @param   string  $twig_view      Chemin du template twig à générer
	 * @param   array   $view_params    Variables à injecter dans le template (peut contenir 'locale')
	 * @param   array   $pdf_options    Surcharge des options du moteur html2pdf
	 * @return  PDFService
	 */
	public function loadTwigView(?string $twig_view = null, array $view_params = [], array $pdf_options = []): PDFService
	{
		if (! $this->twig) throw new \Exception("Twig environment not found!");
		if ($this->encore) $this->encore->reset();  // @see symfony/webpack-encore-bundle#33
		
		if ($this->trans && isset($view_params['locale']) && \strlen($view_params['locale'])) {
			$orig_locale = $this->trans->getLocale();
			$this->trans->setLocale($view_params['locale']);
		}
		$html_data = $this->twig->render($this->fixTwigView($twig_view), $view_params);
		if (isset($orig_locale)) $this->trans->setLocale($orig_locale);
		
		return $this->loadHtmlData($html_data, $pdf_options);
	}

// ------------------------------------------------------------------------------------------ RENDER

	/**
	 * Returns the PDF data as a string.
	 *
	 * @param   bool    $nocompress     Désactivation de la compression PDF
	 * @return  string                  Données binaires du document PDF
	 */
	abstract public function renderData(bool $nocompress = false): string;

	/**
	 * Returns the PDF in a local file.
	 *
	 * @param   string  $filepath       Chemin du fichier PDF à enregistrer
	 * @param   bool    $nocompress     Désactivation de la compression PDF
	 * @return  string                  Chemin du fichier PDF généré
	 */
	public function renderFile(?string $filepath = null, bool $nocompress = false): string
	{
		$filepath = $this->fixFilepath($filepath);
		\file_put_contents($filepath, $this->renderData($nocompress));
		return $filepath;
	}

	/**
	 * Returns the PDF in a symfony Response object (clean integration).
	 *
	 * @param   string  $filename       Nom du document PDF à télécharger
	 * @param   bool    $inline         Affichage du PDF sans forcer son enregistrement
	 * @param   bool    $nocompress     Désactivation de la compression PDF
	 * @return  Response                Objet à retourner par le controlleur
	 */
	public function renderResponse(?string $filename = null, bool $inline = false, bool $nocompress = false): Response
	{
		return new Response(
			$this->renderData($nocompress),
			200,
			$this->getHttpHeaders($filename, $inline)
		);
	}

	/**
	 * Returns the PDF in a symfony StreamedResponse object (less memory usage).
	 *
	 * @param   string  $filename       Nom du document PDF à télécharger
	 * @param   bool    $inline         Affichage du PDF sans forcer son enregistrement
	 * @param   bool    $nocompress     Désactivation de la compression PDF
	 * @return  StreamedResponse        Objet à retourner par le controlleur
	 */
	public function renderStream(?string $filename = null, bool $inline = false, bool $nocompress = false): StreamedResponse
	{
		return new StreamedResponse(
			function() use ($nocompress) { echo $this->renderData($nocompress); },
			200,
			$this->getHttpHeaders($filename, $inline)
		);
	}

// ------------------------------------------------------------------------------------------ COMBOS

	/**
	 * Initialisation à partir d'une vue Twig pour générer un PDF retourné dans une variable.
	 *
	 * @param   string  $twig_view      Chemin du template twig à générer
	 * @param   array   $view_params    Variables à injecter dans le template (peut contenir 'locale')
	 * @param   array   $pdf_options    Surcharge des options du moteur html2pdf
	 * @return  string                  Données binaires du document PDF
	 */
	public function view2data(?string $twig_view = null, array $view_params = [], array $pdf_options = []): string
	{
		return $this->loadTwigView($twig_view, $view_params, $pdf_options)->renderData();
	}

	/**
	 * Initialisation à partir d'une vue Twig pour générer un PDF retourné dans un fichier local.
	 *
	 * @param   string  $filepath       Chemin du fichier PDF à enregistrer
	 * @param   string  $twig_view      Chemin du template twig à générer
	 * @param   array   $view_params    Variables à injecter dans le template (peut contenir 'locale')
	 * @param   array   $pdf_options    Surcharge des options du moteur html2pdf
	 * @return  string                  Chemin du fichier PDF généré
	 */
	public function view2file(?string $filepath, ?string $twig_view = null, array $view_params = [], array $pdf_options = []): string
	{
		return $this->loadTwigView($twig_view, $view_params, $pdf_options)->renderFile($filepath);
	}

	/**
	 * Initialisation à partir d'une vue Twig pour générer un PDF retourné dans une StreamedResponse.
	 *
	 * @param   string  $filename       Nom du document PDF à télécharger
	 * @param   string  $twig_view      Chemin du template twig à générer
	 * @param   array   $view_params    Variables à injecter dans le template (peut contenir 'locale')
	 * @param   array   $pdf_options    Surcharge des options du moteur html2pdf
	 * @return  StreamedResponse        Objet à retourner par le controlleur
	 */
	public function view2http(?string $filename, ?string $twig_view = null, array $view_params = [], array $pdf_options = []): StreamedResponse
	{
		return $this->loadTwigView($twig_view, $view_params, $pdf_options)->renderStream($filename);
	}

// ------------------------------------------------------------------------------------------ CHECKS

	protected function fixTwigView(?string $twig_view = null): string
	{
		if ($twig_view === null) $twig_view = $this->confs['default_twigview'];
		return \preg_match('/[^\/]+\.[^\/]+$/', $twig_view) ? $twig_view : $twig_view . ".html.twig";
	}

	protected function fixHtmlFile(string $html_file): string
	{
		// si relatif : utilisation de l'option "path_source" pour ajouter le dossier parent par défaut
		return (\substr($html_file,0,1) !== '/') ? $this->paths['path_source'] . '/' . $html_file : $html_file;
	}

	protected function fixFilepath(?string $filepath = null): string
	{
		list($extension) = $this->getFileType();
		
		// récupération de l'éventuelle partie "dossier" du chemin $filepath
		$folder = \is_string($filepath) ? \dirname($filepath . "X") : ".";
		// si relatif : utilisation de l'option "path_output" pour ajouter le dossier parent par défaut
		if (\substr($folder,0,1) !== '/') $folder = $this->paths['path_output'] . '/' . $folder;
		// si dossier non existant : création automatique (récursive)
		if (! \is_dir($folder)) @\mkdir($folder, 0750, true);
		
		// récupération de l'éventuelle partie "fichier" du chemin $filepath
		$filename = (\is_string($filepath) && \substr($filepath,-1) !== '/') ? \trim(\basename($filepath, $extension)) : "";
		// si vide : génération automatique d'un nom de fichier (datetime + random)
		if (! \strlen($filename)) $filename = \sprintf('%s-%s', \date('Ymd_His'), \bin2hex(\random_bytes(8)));
		// retourne le chemin complet (absolu) avec extension automatique
		return \str_replace('/./', '/', $folder) . '/' . $filename . $extension;
	}

	protected function fixFilename(?string $filename = null, ?string $extension = null): string
	{
		if ($extension === null) list($extension) = $this->getFileType();
		$filename = \is_string($filename) ? \trim(\basename($filename, $extension)) : "";
		if (! \strlen($filename)) $filename = \trim(\basename($this->confs['default_filename'], $extension));
		return \str_replace('"', '', $filename) . $extension;
	}

	protected function getHttpHeaders(?string $filename = null, bool $inline = false): array
	{
		list($extension, $mime_type) = $this->getFileType();
		return array(
			'Content-Type'         => $mime_type,
			'Content-Disposition'  => \sprintf('%s; filename="%s"',
				($inline) ? "inline" : "attachment",
				$this->fixFilename($filename, $extension)
			),
		);
	}

	protected function getFileType(string $type = "PDF"): ?array
	{
		if (! isset(self::MIME_TYPES[$type])) $type = "PDF";
		return self::MIME_TYPES[$type];
	}

}