Linux SRV-16 6.18.44-paas #1 SMP PREEMPT_DYNAMIC Thu Aug 13 10:08:12 UTC 2026 x86_64
/
proc
/
212
/
root
/
proc
/
212
/
task
/
212
/
root
/
proc
/
70528
/
cwd
/
src
/
Service
/
//proc/212/root/proc/212/task/212/root/proc/70528/cwd/src/Service/MockupPdfRendererService.php
<?php namespace App\Service; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\File\File; /** * Rend une description structurée de maquette (document, pages, éléments) * en PDF d'impression : couleurs CMJN, fond perdu, TrimBox et traits de coupe. */ class MockupPdfRendererService { public $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function render(array $layout, ?File $visualFile, string $outputPath): void { $document = $layout['document'] ?? []; $width = (float) ($document['width_mm'] ?? 210); $height = (float) ($document['height_mm'] ?? 297); $bleed = (float) ($document['bleed_mm'] ?? 2); $pageWidth = $width + 2 * $bleed; $pageHeight = $height + 2 * $bleed; $orientation = $pageWidth > $pageHeight ? 'L' : 'P'; $pdf = new \TCPDF($orientation, 'mm', [$pageWidth, $pageHeight], true, 'UTF-8', false); $pdf->SetCreator('impressionrapide.fr'); $pdf->SetTitle('Maquette'); $pdf->setPrintHeader(false); $pdf->setPrintFooter(false); $pdf->SetMargins(0, 0, 0, true); $pdf->SetAutoPageBreak(false); $visualPath = $visualFile ? $this->visualPathForPdf($visualFile) : null; $pages = !empty($layout['pages']) && is_array($layout['pages']) ? $layout['pages'] : [[]]; // boîtes PDF déclarées pour le RIP de l'imprimeur : format fini (TrimBox) et fond perdu (BleedBox) $pageFormat = [ 'MediaBox' => ['llx' => 0, 'lly' => 0, 'urx' => $pageWidth, 'ury' => $pageHeight], 'BleedBox' => ['llx' => 0, 'lly' => 0, 'urx' => $pageWidth, 'ury' => $pageHeight], 'TrimBox' => ['llx' => $bleed, 'lly' => $bleed, 'urx' => $bleed + $width, 'ury' => $bleed + $height], ]; foreach ($pages as $page) { $pdf->AddPage($orientation, $pageFormat); foreach ($page['elements'] ?? [] as $element) { if (is_array($element)) { $this->renderElement($pdf, $element, $bleed, $visualPath); } } $this->drawCropMarks($pdf, $width, $height, $bleed); } $pdf->Output($outputPath, 'F'); if (!is_file($outputPath)) { $this->logger->error("Rendu de maquette : échec d'écriture du PDF", [ "dossier" => dirname($outputPath) ]); throw new \RuntimeException("Impossible d'enregistrer la maquette dans ".dirname($outputPath)); } } /** * Dessine un élément de la description. Les coordonnées du JSON sont relatives * au format fini : le fond perdu les décale vers l'intérieur de la page. */ private function renderElement(\TCPDF $pdf, array $element, float $bleed, ?string $visualPath): void { $x = (float) ($element['x'] ?? 0) + $bleed; $y = (float) ($element['y'] ?? 0) + $bleed; $width = (float) ($element['width'] ?? 0); $height = (float) ($element['height'] ?? 0); $color = $this->cmjnColor($element['color'] ?? null); switch ($element['type'] ?? '') { case 'rect': $pdf->SetFillColorArray($color); $pdf->Rect($x, $y, $width, $height, 'F'); break; // "image" = visuel unique fourni par le client (chemin partagé $visualPath) case 'image': if($visualPath) { $this->placeVisual($pdf, $visualPath, $x, $y, $width, $height); } break; // "generated_image" = image IA propre à cet élément (chemin stocké dans l'élément) case 'generated_image': $generatedPath = $element['generated_path'] ?? null; if ($generatedPath && is_file($generatedPath)) { $this->placeVisual($pdf, $this->fitToBox($generatedPath, $width, $height), $x, $y, $width, $height); } break; // les types "text" et "frame" ne sont volontairement pas rendus : // le visuel du client est l'œuvre finale, rien ne doit s'y ajouter } } /** * Place le visuel dans la zone demandée sans jamais le déformer : * en "cover" (remplit toute la zone, l'excédent est rogné par les bords du document) * quand la zone couvre la page entière, sinon en "contain" (ajusté à l'intérieur, centré). */ private function placeVisual(\TCPDF $pdf, string $visualPath, float $x, float $y, float $width, float $height): void { $size = getimagesize($visualPath); $coversPage = $x <= 0 && $y <= 0 && $x + $width >= $pdf->getPageWidth() && $y + $height >= $pdf->getPageHeight(); if (!$coversPage || false === $size || $size[0] < 1 || $size[1] < 1) { // contain : fitbox 'CM' ajuste le visuel dans la zone, centré $pdf->Image($visualPath, $x, $y, $width, $height, '', '', '', true, 300, '', false, false, 0, 'CM'); return; } // cover : le visuel remplit la page entière, fond perdu compris $ratio = $size[0] / $size[1]; $coverWidth = $width; $coverHeight = $width / $ratio; if ($coverHeight < $height) { $coverHeight = $height; $coverWidth = $height * $ratio; } $pdf->Image( $visualPath, $x - ($coverWidth - $width) / 2, $y - ($coverHeight - $height) / 2, $coverWidth, $coverHeight ); } private function fitToBox(string $path, float $width, float $height): string { $size = getimagesize($path); if ($width <= 0 || $height <= 0 || false === $size || $size[0] < 1 || $size[1] < 1) { return $path; } $geometry = $this->paddingGeometry($size[0], $size[1], $width / $height); if ($geometry === null || !function_exists('imagecreatetruecolor')) { return $path; } [$targetWidth, $targetHeight, $offsetX, $offsetY] = $geometry; $source = @imagecreatefromstring(file_get_contents($path)); if ($source === false) { $this->logger->warning("Rendu de maquette : image générée illisible, proportions non adaptées", [ "image" => $path ]); return $path; } $canvas = imagecreatetruecolor($targetWidth, $targetHeight); imagefill($canvas, 0, 0, imagecolorallocate($canvas, 255, 255, 255)); if ($offsetX > 0) { imagecopyresampled($canvas, $source, 0, 0, 0, 0, $offsetX, $size[1], 1, $size[1]); imagecopyresampled($canvas, $source, $offsetX + $size[0], 0, $size[0] - 1, 0, $targetWidth - $offsetX - $size[0], $size[1], 1, $size[1]); } if ($offsetY > 0) { imagecopyresampled($canvas, $source, 0, 0, 0, 0, $size[0], $offsetY, $size[0], 1); imagecopyresampled($canvas, $source, 0, $offsetY + $size[1], 0, $size[1] - 1, $size[0], $targetHeight - $offsetY - $size[1], $size[0], 1); } imagecopy($canvas, $source, $offsetX, $offsetY, 0, 0, $size[0], $size[1]); $fitted = tempnam(sys_get_temp_dir(), 'mockup_fit_').'.png'; imagepng($canvas, $fitted); imagedestroy($canvas); imagedestroy($source); return $fitted; } /** * @return int[]|null [largeur, hauteur, décalage horizontal, décalage vertical] */ private function paddingGeometry(int $sourceWidth, int $sourceHeight, float $targetRatio): ?array { $currentRatio = $sourceWidth / $sourceHeight; if (abs($currentRatio - $targetRatio) <= 0.01 * $targetRatio) { return null; } if ($targetRatio > $currentRatio) { // emplacement plus large que l'image : bandes à gauche et à droite $targetWidth = (int) round($sourceHeight * $targetRatio); $targetHeight = $sourceHeight; } else { $targetWidth = $sourceWidth; $targetHeight = (int) round($sourceWidth / $targetRatio); } return [ $targetWidth, $targetHeight, (int) floor(($targetWidth - $sourceWidth) / 2), (int) floor(($targetHeight - $sourceHeight) / 2) ]; } /** * Traits de coupe aux quatre coins du format fini, tracés dans le fond perdu. */ private function drawCropMarks(\TCPDF $pdf, float $width, float $height, float $bleed): void { if ($bleed <= 0) { return; } $pageWidth = $width + 2 * $bleed; $pageHeight = $height + 2 * $bleed; $length = $bleed * 0.75; $pdf->SetLineWidth(0.1); $pdf->SetDrawColorArray([100, 100, 100, 100]); // haut-gauche $pdf->Line(0, $bleed, $length, $bleed); $pdf->Line($bleed, 0, $bleed, $length); // haut-droit $pdf->Line($pageWidth - $length, $bleed, $pageWidth, $bleed); $pdf->Line($bleed + $width, 0, $bleed + $width, $length); // bas-gauche $pdf->Line(0, $bleed + $height, $length, $bleed + $height); $pdf->Line($bleed, $pageHeight - $length, $bleed, $pageHeight); // bas-droit $pdf->Line($pageWidth - $length, $bleed + $height, $pageWidth, $bleed + $height); $pdf->Line($bleed + $width, $pageHeight - $length, $bleed + $width, $pageHeight); } /** * TCPDF ne lit pas le webp : convertit le visuel en PNG temporaire si nécessaire. */ private function visualPathForPdf(File $visualFile): string { if ($visualFile->getMimeType() !== 'image/webp') { return $visualFile->getPathname(); } $pngPath = tempnam(sys_get_temp_dir(), 'mockup_visual_').'.png'; imagepng(imagecreatefromwebp($visualFile->getPathname()), $pngPath); return $pngPath; } private function cmjnColor($color): array { if (!is_array($color)) { // noir renforcé par défaut return [30, 30, 30, 100]; } return [ (float) ($color['c'] ?? 0), (float) ($color['m'] ?? 0), (float) ($color['j'] ?? 0), (float) ($color['n'] ?? 0) ]; } }