image_path || ! Storage::disk('public')->exists($mappa->image_path)) { throw new RuntimeException('Prima di generare il PDF va generata l\'immagine della piantina.'); } $immagine = Storage::disk('public')->path($mappa->image_path); } $dimensioni = @getimagesize($immagine); if ($dimensioni === false) { throw new RuntimeException('L\'immagine della piantina non è leggibile: rigenerala.'); } [$imgPxW, $imgPxH] = $dimensioni; $pdf = new Fpdi(); $pdf->SetAutoPageBreak(false); $pdf->setSourceFile(resource_path('pdf/S-12_I.pdf')); $template = $pdf->importPage(1); $misure = $pdf->getTemplateSize($template); $w = $misure['width']; $h = $misure['height']; // ── Pagina 1: il modulo ──────────────────────────────────────────── $pdf->AddPage($misure['orientation'], [$w, $h]); $pdf->useTemplate($template); $pdf->SetTextColor(0, 0, 0); // Intestazione: i valori si appoggiano sulle righe punteggiate del // modulo. Posizioni in proporzione alla pagina, tarate sull'S-12. $pdf->SetFont('Helvetica', 'B', 10); $headerY = $h * 0.125; $pdf->SetXY($w * 0.185, $headerY); $pdf->Cell($w * 0.45, 5, $this->latin((string) $territorio->zona?->nome), 0, 0, 'L'); $pdf->SetXY($w * 0.875, $headerY); $pdf->Cell($w * 0.10, 5, $this->latin($territorio->numero), 0, 0, 'C'); $this->scriviConfini($pdf, $territorio, $w, $h); // ── Pagina 2: la piantina ────────────────────────────────────────── $pdf->AddPage($misure['orientation'], [$w, $h]); $boxW = $w * self::OCCUPAZIONE; $boxH = $h * self::OCCUPAZIONE; $scala = min($boxW / $imgPxW, $boxH / $imgPxH); $imgW = $imgPxW * $scala; $imgH = $imgPxH * $scala; $pdf->Image( $immagine, ($w - $imgW) / 2, ($h - $imgH) / 2, $imgW, $imgH, $this->tipoImmagine($immagine) ); return $pdf->Output('S'); } /** * I confini nello spazio che sul modulo è riservato alla piantina. * * Il corpo del testo si restringe finché non ci sta: i confini di un * territorio vanno da due righe a venti, e una misura fissa o lascerebbe * mezzo modulo vuoto o sborderebbe sulla dicitura stampata sotto. */ private function scriviConfini(Fpdi $pdf, Territorio $territorio, float $w, float $h): void { $testo = trim((string) $territorio->confini); if ($testo === '') { $pdf->SetFont('Helvetica', 'I', 9); $pdf->SetTextColor(120, 120, 120); $pdf->SetXY($w * 0.06, $h * 0.42); $pdf->Cell($w * 0.88, 5, $this->latin('Confini non ancora descritti.'), 0, 0, 'C'); $pdf->SetTextColor(0, 0, 0); return; } $boxX = $w * 0.06; $boxY = $h * 0.20; $boxW = $w * 0.88; $boxH = $h * 0.53; $righe = preg_split('/\R/', $testo) ?: [$testo]; foreach ([10, 9, 8, 7, 6, 5.5, 5] as $corpo) { $interlinea = $corpo * 0.44; $pdf->SetFont('Helvetica', '', $corpo); if ($this->altezzaTesto($pdf, $righe, $boxW, $interlinea) <= $boxH) { break; } } $pdf->SetXY($boxX, $boxY); foreach ($righe as $riga) { $riga = trim($riga); if ($riga === '') { $pdf->Ln($interlinea * 0.6); continue; } $pdf->SetX($boxX); $pdf->MultiCell($boxW, $interlinea, $this->latin($riga), 0, 'L'); } } /** Quanto verrebbe alto il blocco dei confini a questo corpo. */ private function altezzaTesto(Fpdi $pdf, array $righe, float $larghezza, float $interlinea): float { $altezza = 0.0; foreach ($righe as $riga) { $riga = trim($riga); if ($riga === '') { $altezza += $interlinea * 0.6; continue; } // GetStringWidth misura una riga sola: quante ne servono lo dice il // rapporto con la larghezza disponibile, arrotondato per eccesso. $larghezzaTesto = $pdf->GetStringWidth($this->latin($riga)); $altezza += max(1, ceil($larghezzaTesto / $larghezza)) * $interlinea; } return $altezza; } /** FPDF vuole sapere il formato quando l'estensione non basta. */ private function tipoImmagine(string $path): string { return strtolower(pathinfo($path, PATHINFO_EXTENSION)) === 'png' ? 'PNG' : 'JPEG'; } /** * Genera il PDF e lo rende quello ufficiale del territorio, archiviando * l'eventuale PDF precedente nello storico versioni. */ public function sostituisci(Territorio $territorio, TerritorioMappa $mappa, string $origine = 'piantine'): void { $this->sostituisciConPdf($territorio, $this->genera($territorio, $mappa), $origine); } /** * Rende ufficiale un PDF già composto, archiviando il precedente. * * Prima si scrive il nuovo file, poi si archivia il vecchio: nell'ordine * inverso un errore di scrittura (disco pieno) lasciava pdf_path puntato * a un file già spostato nello storico, e i link delle assegnazioni in 404. */ public function sostituisciConPdf(Territorio $territorio, string $binario, string $origine = 'piantine'): void { $disk = Storage::disk('public'); $nuovoPath = 'territori-pdf/piantina-' . $territorio->id . '-' . now()->format('Ymd-His') . '.pdf'; if (! $disk->put($nuovoPath, $binario)) { throw new RuntimeException('Non è stato possibile scrivere il nuovo PDF sul disco: il PDF in vigore non è stato toccato.'); } $this->archiviaCorrente($territorio, $origine); $territorio->update([ 'pdf_path' => $nuovoPath, 'thumbnail_path' => $this->thumbnails->generate($nuovoPath), ]); activity() ->causedBy(auth()->user()) ->performedOn($territorio) ->log('piantina_pdf_sostituito'); } /** * Rimette in vigore una versione archiviata; il PDF attuale finisce a sua * volta nello storico, quindi il ripristino è sempre reversibile. */ public function ripristina(TerritorioPdfVersione $versione): void { $disk = Storage::disk('public'); $territorio = $versione->territorio; if (! $territorio) { throw new RuntimeException('La versione non appartiene più a nessun territorio.'); } if (! $disk->exists($versione->pdf_path)) { throw new RuntimeException('Il file di questa versione non esiste più sul disco.'); } $this->archiviaCorrente($territorio, 'ripristino'); $nuovoPath = 'territori-pdf/ripristino-' . $territorio->id . '-' . now()->format('Ymd-His') . '.pdf'; $disk->move($versione->pdf_path, $nuovoPath); $territorio->update([ 'pdf_path' => $nuovoPath, 'thumbnail_path' => $this->thumbnails->generate($nuovoPath), ]); $versione->delete(); activity() ->causedBy(auth()->user()) ->performedOn($territorio) ->log('piantina_pdf_ripristinato'); } /** * Sposta il PDF attuale del territorio (se esiste) nello storico. * * Pubblica perché la usa anche l'import dei PDF da cartella: chi * sostituisce un PDF lo archivia sempre allo stesso modo. */ public function archiviaCorrente(Territorio $territorio, string $origine): void { $disk = Storage::disk('public'); if (! $territorio->pdf_path || ! $disk->exists($territorio->pdf_path)) { return; } $versionePath = 'territori-pdf-versioni/territorio-' . $territorio->id . '-' . now()->format('Ymd-His') . '-' . substr(uniqid(), -5) . '.pdf'; $disk->makeDirectory('territori-pdf-versioni'); $disk->move($territorio->pdf_path, $versionePath); TerritorioPdfVersione::create([ 'territorio_id' => $territorio->id, 'pdf_path' => $versionePath, 'thumbnail_path' => $territorio->thumbnail_path, 'origine' => $origine, 'sostituito_il' => now(), ]); } /** FPDF ragiona in windows-1252: gli accenti UTF-8 vanno convertiti. */ private function latin(string $testo): string { $convertito = @iconv('UTF-8', 'windows-1252//TRANSLIT', $testo); return $convertito !== false ? $convertito : $testo; } }