import 'dart:async'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/services.dart' show rootBundle; import 'package:syncfusion_flutter_pdf/pdf.dart'; import 'package:flutter/material.dart'; import 'global_data.dart'; import 'models.dart'; import 'cai_mapping.dart'; class PdfEngine { static Future> generaDocumentoCai() async { PdfDocument? document; try { final ByteData data = await rootBundle.load('assets/CAI_p1.pdf'); // 1. Caricamento e Copia final List bytesCopia = data.buffer.asUint8List().toList(); document = PdfDocument(inputBytes: bytesCopia); final PdfForm form = document.form; final PdfPage page = document.pages[0]; form.setDefaultAppearance(false); // Mappatura Map mappaCampi = {}; for (int i = 0; i < form.fields.count; i++) { if (form.fields[i].name != null) { mappaCampi[form.fields[i].name!.trim().toUpperCase()] = form.fields[i]; } } // --- COMPILAZIONE STANDARD (La tua versione preferita) --- // 1. TESTI CaiMapping.testi.forEach((keyGlobal, keyPdf) { String valore = _valoreDaGlobal(keyGlobal); String keyPdfNorm = keyPdf.trim().toUpperCase(); if (mappaCampi.containsKey(keyPdfNorm) && valore.isNotEmpty) { final field = mappaCampi[keyPdfNorm]; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); field.text = valore.toUpperCase(); } } }); // 2. CHECKBOX _scriviX(mappaCampi, [GlobalData.feriti ? CaiMapping.feriti_SI : CaiMapping.feriti_NO]); _scriviX(mappaCampi, [GlobalData.Veicoli_danni_materiali_oltre ? CaiMapping.danni_veicoli_SI : CaiMapping.danni_veicoli_NO]); _scriviX(mappaCampi, [GlobalData.Oggetti_diversi_danni_materiali ? CaiMapping.danni_oggetti_SI : CaiMapping.danni_oggetti_NO]); _scriviX(mappaCampi, [GlobalData.FLAG_danni_mat_assicurati_A ? CaiMapping.danni_mat_A_SI : CaiMapping.danni_mat_A_NO]); _scriviX(mappaCampi, [GlobalData.FLAG_danni_mat_assicurati_B ? CaiMapping.danni_mat_B_SI : CaiMapping.danni_mat_B_NO]); String catA = GlobalData.Categoria_cond_A.toUpperCase().trim(); if (catA == 'A') _scriviX(mappaCampi, ['cat_a_A']); else if (catA == 'B') _scriviX(mappaCampi, ['cat_b_A']); else if (catA.isNotEmpty) _scriviTesto(mappaCampi, ['cat_altro_A'], catA); String catB = GlobalData.Categoria_cond_B.toUpperCase().trim(); if (catB == 'A') _scriviX(mappaCampi, ['cat_a_B']); else if (catB == 'B') _scriviX(mappaCampi, ['cat_b_B']); else if (catB.isNotEmpty) _scriviTesto(mappaCampi, ['cat_altro_B'], catB); // 3. CIRCOSTANZE int countA = 0; int countB = 0; for (int i = 1; i <= 17; i++) { if (GlobalData.circostanzeA[i] == true) { if (_scriviX(mappaCampi, [i < 10 ? "A0$i" : "A$i"])) countA++; } if (GlobalData.circostanzeB[i] == true) { List nomiTarget = []; if (i == 9) nomiTarget = ["Check Box 26", "CheckBox26", "26"]; else if (i == 10) nomiTarget = ["Check Box 27", "CheckBox27", "27"]; else if (i == 11) nomiTarget = ["Check Box 28", "CheckBox28", "28"]; else if (i == 12) nomiTarget = ["Check Box 29", "CheckBox29", "29"]; else nomiTarget = [i < 10 ? "B0$i" : "B$i"]; if (_scriviX(mappaCampi, nomiTarget)) countB++; } } _scriviTestoTotale(mappaCampi, ['A_TOT', 'A_tot'], countA.toString()); _scriviTestoTotale(mappaCampi, ['B_TOT', 'B_tot'], countB.toString()); // 4. PUNTI URTO for (String punto in GlobalData.puntiUrtoA_List) _scriviXRossa(mappaCampi, [punto]); for (String punto in GlobalData.puntiUrtoB_List) _scriviXRossa(mappaCampi, [punto]); // 5. IMMAGINI await _disegnaInBox(page, mappaCampi, CaiMapping.box_grafico, await _renderGraficoV40(GlobalData.tratti.cast().toList(), GlobalData.elementi.cast().toList())); await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_A, await _renderFirmaTight(GlobalData.puntiFirmaA, Colors.black)); await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_B, await _renderFirmaTight(GlobalData.puntiFirmaB, Colors.black)); // ================================================================= // FASE CRITICA: SALVATAGGIO -> RICARICA -> FLATTEN (Anti-Crash) // ================================================================= // 1. Salviamo il file compilato in memoria. Questo corregge gli errori interni del PDF. List bytesTemporanei = await document.save(); document.dispose(); // Chiudiamo il vecchio // 2. Riapriamo il file "pulito" PdfDocument docFinale = PdfDocument(inputBytes: bytesTemporanei); // 3. Ora eseguiamo il FLATTEN. // È INDISPENSABILE per vedere le X nell'immagine di anteprima. // Poiché il file è stato appena rigenerato, NON DOVREBBE CRASHARE. try { docFinale.form.flattenAllFields(); } catch (e) { debugPrint("⚠️ Errore Flattening anche dopo pulizia: $e"); // Se fallisce ancora, usiamo il fallback ReadOnly, ma l'immagine potrebbe essere incompleta. docFinale.form.readOnly = true; } // 4. Salvataggio finale List bytesFinali = await docFinale.save(); docFinale.dispose(); return bytesFinali; } catch (e) { debugPrint("ERRORE GENERAZIONE PDF: $e"); return []; } } // --- HELPERS (Standard) --- static bool _scriviX(Map mappa, List nomiPossibili) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 14); field.foreColor = PdfColor(0, 0, 0); field.text = "X"; } else if (field is PdfCheckBoxField) { field.isChecked = true; } return true; } } return false; } static void _scriviTesto(Map mappa, List nomiPossibili, String testo) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 10); field.foreColor = PdfColor(0, 0, 0); field.text = testo; } return; } } } static bool _scriviXRossa(Map mappa, List nomiPossibili) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 16, style: PdfFontStyle.bold); field.foreColor = PdfColor(255, 0, 0); field.text = "X"; return true; } } } return false; } static void _scriviTestoTotale(Map mappa, List nomi, String testo) { for (String nome in nomi) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); field.textAlignment = PdfTextAlignment.center; field.text = testo; } return; } } } static Future _disegnaInBox(PdfPage page, Map mappa, String nomeCampo, Uint8List? imgBytes) async { String key = nomeCampo.trim().toUpperCase(); if (imgBytes == null || !mappa.containsKey(key)) return; Rect boxRect = mappa[key]!.bounds; PdfBitmap bitmap = PdfBitmap(imgBytes); double imageW = bitmap.width.toDouble(); double imageH = bitmap.height.toDouble(); if (imageW <= 0 || imageH <= 0) return; double ratioX = boxRect.width / imageW; double ratioY = boxRect.height / imageH; double scale = (ratioX < ratioY) ? ratioX : ratioY; double drawW = imageW * scale; double drawH = imageH * scale; double offsetX = boxRect.left + (boxRect.width - drawW) / 2; double offsetY = boxRect.top + (boxRect.height - drawH) / 2; page.graphics.drawImage(bitmap, Rect.fromLTWH(offsetX, offsetY, drawW, drawH)); } static Future _renderFirmaTight(List punti, Color colore) async { if (punti.isEmpty) return null; double minX = double.infinity, minY = double.infinity, maxX = double.negativeInfinity, maxY = double.negativeInfinity; for (var p in punti) { if (p != null) { if (p.dx < minX) minX = p.dx; if (p.dx > maxX) maxX = p.dx; if (p.dy < minY) minY = p.dy; if (p.dy > maxY) maxY = p.dy; } } double padding = 20.0; double firmaW = maxX - minX; double firmaH = maxY - minY; if (firmaW <= 0) firmaW = 1; if (firmaH <= 0) firmaH = 1; double resolutionScale = 3.0; double canvasW = (firmaW + padding * 2) * resolutionScale; double canvasH = (firmaH + padding * 2) * resolutionScale; final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.scale(resolutionScale); canvas.translate(-minX + padding, -minY + padding); final paint = Paint()..color = colore..strokeWidth = 5.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round; for (int i = 0; i < punti.length - 1; i++) { if (punti[i] != null && punti[i+1] != null) { canvas.drawLine(punti[i]!, punti[i+1]!, paint); } } final img = await recorder.endRecording().toImage(canvasW.toInt(), canvasH.toInt()); final byteData = await img.toByteData(format: ui.ImageByteFormat.png); return byteData?.buffer.asUint8List(); } static Future _renderGraficoV40(List tratti, List elementi) async { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); final size = const Size(2000, 800); final painter = PainterV40(tratti, elementi); painter.paint(canvas, size); final img = await recorder.endRecording().toImage(size.width.toInt(), size.height.toInt()); final byteData = await img.toByteData(format: ui.ImageByteFormat.png); return byteData?.buffer.asUint8List(); } static String _valoreDaGlobal(String key) { switch (key) { case 'data_incidente': return GlobalData.data_incidente; case 'ora': return GlobalData.ora; case 'luogo': return GlobalData.luogo; case 'testimoni': return GlobalData.testimoni; case 'danni_visibili_A': return GlobalData.danni_visibili_A; case 'osservazioni_A': return GlobalData.osservazioni_A; case 'danni_visibili_B': return GlobalData.danni_visibili_B; case 'osservazioni_B': return GlobalData.osservazioni_B; case 'Cognome_contraente_A': return GlobalData.Cognome_contraente_A; case 'Nome_contraente_A': return GlobalData.Nome_contraente_A; case 'Codice_Fiscale_contraente_A': return GlobalData.Codice_Fiscale_contraente_A; case 'Indirizzo_contraente_A': return GlobalData.Indirizzo_contraente_A; case 'CAP_contraente_A': return GlobalData.CAP_contraente_A; case 'Stato_contraente_A': return GlobalData.Stato_contraente_A; case 'N_telefono_mail_contraente_A': return GlobalData.N_telefono_mail_contraente_A; case 'Marca_e_Tipo_A': return GlobalData.Marca_e_Tipo_A; case 'Targa_A': return GlobalData.Targa_A; case 'Stato_immatricolazione_A': return GlobalData.Stato_immatricolazione_A; case 'Rimorchio_A': return GlobalData.Rimorchio_A; case 'Stato_immatricolazione2_A': return GlobalData.Stato_immatricolazione2_A; case 'Denominazione_A': return GlobalData.Denominazione_A; case 'Numero_Polizza_A': return GlobalData.Numero_Polizza_A; case 'N_carta_verde_A': return GlobalData.N_carta_verde_A; case 'Data_Inizio_Dal_A': return GlobalData.Data_Inizio_Dal_A; case 'Data_Scadenza_Al_A': return GlobalData.Data_Scadenza_Al_A; case 'Agenzia_A': return GlobalData.Agenzia_A; case 'Indirizzo_agenzia_A': return GlobalData.Indirizzo_agenzia_A; case 'Stato_agenzia_A': return GlobalData.Stato_agenzia_A; case 'Denominazione_agenzia_A': return GlobalData.Denominazione_agenzia_A; case 'N_tel_mail_agenzia_A': return GlobalData.N_tel_mail_agenzia_A; case 'Cognome_cond_A': return GlobalData.Cognome_cond_A; case 'Nome_cond_A': return GlobalData.Nome_cond_A; case 'Data_nascita_cond_A': return GlobalData.Data_nascita_cond_A; case 'Cod_fiscale_cond_A': return GlobalData.Cod_fiscale_cond_A; case 'Indirizzo_cond_A': return GlobalData.Indirizzo_cond_A; case 'Stato_cond_A': return GlobalData.Stato_cond_A; case 'N_tel_mail_cond_A': return GlobalData.N_tel_mail_cond_A; case 'N_Patente_cond_A': return GlobalData.N_Patente_cond_A; case 'Scadenza_cond_A': return GlobalData.Scadenza_cond_A; case 'Cognome_contraente_B': return GlobalData.Cognome_contraente_B; case 'Nome_contraente_B': return GlobalData.Nome_contraente_B; case 'Codice_Fiscale_contraente_B': return GlobalData.Codice_Fiscale_contraente_B; case 'Indirizzo_contraente_B': return GlobalData.Indirizzo_contraente_B; case 'CAP_contraente_B': return GlobalData.CAP_contraente_B; case 'Stato_contraente_B': return GlobalData.Stato_contraente_B; case 'N_telefono_mail_contraente_B': return GlobalData.N_telefono_mail_contraente_B; case 'Marca_e_Tipo_B': return GlobalData.Marca_e_Tipo_B; case 'Targa_B': return GlobalData.Targa_B; case 'Stato_immatricolazione_B': return GlobalData.Stato_immatricolazione_B; case 'Rimorchio_B': return GlobalData.Rimorchio_B; case 'Stato_immatricolazione2_B': return GlobalData.Stato_immatricolazione2_B; case 'Denominazione_B': return GlobalData.Denominazione_B; case 'Numero_Polizza_B': return GlobalData.Numero_Polizza_B; case 'N_carta_verde_B': return GlobalData.N_carta_verde_B; case 'Data_Inizio_Dal_B': return GlobalData.Data_Inizio_Dal_B; case 'Data_Scadenza_Al_B': return GlobalData.Data_Scadenza_Al_B; case 'Agenzia_B': return GlobalData.Agenzia_B; case 'Indirizzo_agenzia_B': return GlobalData.Indirizzo_agenzia_B; case 'Stato_agenzia_B': return GlobalData.Stato_agenzia_B; case 'Denominazione_agenzia_B': return GlobalData.Denominazione_agenzia_B; case 'N_tel_mail_agenzia_B': return GlobalData.N_tel_mail_agenzia_B; case 'Cognome_cond_B': return GlobalData.Cognome_cond_B; case 'Nome_cond_B': return GlobalData.Nome_cond_B; case 'Data_nascita_cond_B': return GlobalData.Data_nascita_cond_B; case 'Cod_fiscale_cond_B': return GlobalData.Cod_fiscale_cond_B; case 'Indirizzo_cond_B': return GlobalData.Indirizzo_cond_B; case 'Stato_cond_B': return GlobalData.Stato_cond_B; case 'N_tel_mail_cond_B': return GlobalData.N_tel_mail_cond_B; case 'N_Patente_cond_B': return GlobalData.N_Patente_cond_B; case 'Scadenza_cond_B': return GlobalData.Scadenza_cond_B; default: return ""; } } } class PainterV40 extends CustomPainter { final List tr; final List el; PainterV40(this.tr, this.el); final List palette = [Colors.blue, Colors.orange, Colors.green, Colors.purple, Colors.red]; @override void paint(Canvas canvas, Size size) { // 1. SFONDO BIANCO (Risolve il problema del nero) final Paint backgroundPaint = Paint()..color = Colors.white; canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), backgroundPaint); // 2. DISEGNO GRIGLIA (Opzionale, ma rende il disegno professionale come l'originale) final Paint gridPaint = Paint() ..color = Colors.grey.shade300 ..strokeWidth = 2.0; double step = 40.0; // Dimensione quadretti // Linee verticali for (double x = 0; x <= size.width; x += step) { canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint); } // Linee orizzontali for (double y = 0; y <= size.height; y += step) { canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint); } // Se non ci sono tratti o elementi, ci fermiamo qui (abbiamo disegnato solo sfondo e griglia pulita) if (tr.isEmpty && el.isEmpty) return; // --- CALCOLO BOUNDING BOX PER IL CONTENUTO --- double minX = double.infinity, minY = double.infinity; double maxX = double.negativeInfinity, maxY = double.negativeInfinity; for (var t in tr) { for (var p in t.punti) { if (p.dx < minX) minX = p.dx; if (p.dx > maxX) maxX = p.dx; if (p.dy < minY) minY = p.dy; if (p.dy > maxY) maxY = p.dy; } } for (var e in el) { if (e.posizione.dx - 30 < minX) minX = e.posizione.dx - 30; if (e.posizione.dx + 30 > maxX) maxX = e.posizione.dx + 30; if (e.posizione.dy - 30 < minY) minY = e.posizione.dy - 30; if (e.posizione.dy + 30 > maxY) maxY = e.posizione.dy + 30; } // Se non abbiamo trovato nulla (caso raro), usiamo valori di default if (minX == double.infinity) { minX = 0; maxX = 100; minY = 0; maxY = 100; } double padding = 40.0; double drawingW = maxX - minX + (padding * 2); double drawingH = maxY - minY + (padding * 2); if (drawingW <= 0) drawingW = 100; if (drawingH <= 0) drawingH = 100; // Scala per adattare il disegno al box (Contain) double scaleX = size.width / drawingW; double scaleY = size.height / drawingH; double scale = (scaleX < scaleY) ? scaleX : scaleY; // Centratura double offsetX = (size.width - (drawingW * scale)) / 2; double offsetY = (size.height - (drawingH * scale)) / 2; canvas.save(); canvas.translate(offsetX, offsetY); canvas.scale(scale); canvas.translate(-minX + padding, -minY + padding); // --- DISEGNO STRADE E FRECCE --- Paint pStrada = Paint() ..color = Colors.black ..strokeWidth = 4.0 / scale ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; for (var t in tr) { if (t.punti.length > 1) { Path path = Path()..moveTo(t.punti[0].dx, t.punti[0].dy); for (var pt in t.punti) path.lineTo(pt.dx, pt.dy); canvas.drawPath(path, pStrada); if (t.tipo == 'freccia') { double a = (t.punti.last - t.punti[t.punti.length - 2]).direction; canvas.drawLine(t.punti.last, t.punti.last - Offset.fromDirection(a - 0.5, 15), pStrada); canvas.drawLine(t.punti.last, t.punti.last - Offset.fromDirection(a + 0.5, 15), pStrada); } } } // --- DISEGNO AUTO E TESTI --- for (var e in el) { canvas.save(); canvas.translate(e.posizione.dx, e.posizione.dy); canvas.rotate(e.rotazione); if (e.tipo == 'testo') { final tp = TextPainter( text: TextSpan(text: e.label ?? "", style: const TextStyle(color: Colors.black, fontSize: 24, fontWeight: FontWeight.bold)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width/2, -tp.height/2)); } else if (e.tipo.startsWith('auto')) { int idx = ((e.label ?? "A").codeUnitAt(0) - 65) % palette.length; Paint p = Paint()..color = palette[idx]; // Corpo auto colorato canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: 60, height: 30), p); // Bordo auto nero canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: 60, height: 30), Paint()..style=PaintingStyle.stroke..color=Colors.black..strokeWidth=2); // Lettera A/B Bianca final tp = TextPainter( text: TextSpan(text: e.label ?? "A", style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width/2, -tp.height/2)); } canvas.restore(); } canvas.restore(); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }