import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:em2rp/services/qr_code_service.dart'; /// Formats d'étiquettes QR disponibles enum QRLabelFormat { small, medium, large } /// Configuration pour la génération de PDF class PDFGeneratorConfig { final double qrCodeSize; final int itemsPerPage; final bool withProgress; const PDFGeneratorConfig({ required this.qrCodeSize, required this.itemsPerPage, this.withProgress = false, }); static const small = PDFGeneratorConfig( qrCodeSize: 150, itemsPerPage: 50, ); // 4 colonnes x 10 lignes = 40 étiquettes static const medium = PDFGeneratorConfig( qrCodeSize: 150, // Réduit légèrement pour entrer dans 25.4mm de haut itemsPerPage: 40, ); // 2 colonnes x 5 lignes = 10 étiquettes static const large = PDFGeneratorConfig( qrCodeSize: 300, itemsPerPage: 10, ); static PDFGeneratorConfig fromFormat(QRLabelFormat format) { switch (format) { case QRLabelFormat.small: return small; case QRLabelFormat.medium: return medium; case QRLabelFormat.large: return large; } } } /// Service UNIQUE et optimisé pour la génération de PDFs avec QR codes class PDFService { static Uint8List? _cachedLogoBytes; static bool _logoLoadAttempted = false; /// Charge le logo en cache une seule fois static Future _ensureLogoLoaded() async { if (_logoLoadAttempted) return; _logoLoadAttempted = true; try { final logoData = await rootBundle.load('assets/logos/LowQRectangleLogoBlack.png'); _cachedLogoBytes = logoData.buffer.asUint8List(); } catch (e) { _cachedLogoBytes = Uint8List(0); } } /// Tronque un texte static String _truncate(String text, int max) { return text.length <= max ? text : '${text.substring(0, max - 3)}...'; } /// Génère un PDF avec QR codes pour n'importe quel type d'objet static Future generatePDF({ required List items, required QRLabelFormat format, required String Function(T) getId, String Function(T)? getTitle, List Function(T)? getDetails, Function(int current, int total)? onProgress, }) async { if (items.isEmpty) { throw Exception('La liste d\'items est vide'); } final config = PDFGeneratorConfig.fromFormat(format); final pdf = pw.Document(); // Pré-charger le logo pour formats medium et large if (format == QRLabelFormat.medium || format == QRLabelFormat.large) { await _ensureLogoLoaded(); } // Générer tous les QR codes en bulk avec progression final allQRImages = await QRCodeService.generateBulkQRCodes( items.map((item) => getId(item)).toList(), size: config.qrCodeSize, withLogo: false, useCache: true, onProgress: onProgress, ); // Générer les pages switch (format) { case QRLabelFormat.small: _addSmallLabels(pdf, items, getId, allQRImages, config); break; case QRLabelFormat.medium: _addMediumLabels(pdf, items, getId, getTitle, allQRImages, config); break; case QRLabelFormat.large: _addLargeLabels(pdf, items, getId, getTitle, getDetails, allQRImages, config); break; } return pdf.save(); } // ======================================================================== // PETITS LABELS (Original: 2x2 cm approx) // ======================================================================== static void _addSmallLabels( pw.Document pdf, List items, String Function(T) getId, List qrImages, PDFGeneratorConfig config, ) { const qrSize = 56.69; // ~2cm for (int pageStart = 0; pageStart < items.length; pageStart += config.itemsPerPage) { final pageItems = items.skip(pageStart).take(config.itemsPerPage).toList(); final pageQRs = qrImages.skip(pageStart).take(config.itemsPerPage).toList(); pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(20), build: (_) => pw.Wrap( spacing: 10, runSpacing: 10, children: List.generate(pageItems.length, (i) { return pw.Container( width: qrSize, height: qrSize + 20, child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, children: [ pw.Image(pw.MemoryImage(pageQRs[i])), pw.SizedBox(height: 2), pw.Text( getId(pageItems[i]), style: pw.TextStyle(fontSize: 6, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center, ), ], ), ); }), ), ), ); } } // ======================================================================== // LABELS MOYENS (49 x 26 mm | 4 colonnes, 10 lignes) // ======================================================================== static void _addMediumLabels( pw.Document pdf, List items, String Function(T) getId, String Function(T)? getTitle, List qrImages, PDFGeneratorConfig config, ) { // 1. Dimensions exactes des étiquettes const double labelWidth = 50 * PdfPageFormat.mm; const double labelHeight = 26.0 * PdfPageFormat.mm; // 2. Calcul du centrage manuel // Marge théorique = (210mm - (49*4)) / 2 = 7mm // CORRECTION : On enlève 1.5mm pour réduire la marge de gauche (décalage vers la gauche) const double horizontalCorrection = PdfPageFormat.mm; final double leftMargin = ((PdfPageFormat.a4.width - (labelWidth * 4)) / 2) + horizontalCorrection; // Centrage vertical standard final double topMargin = (PdfPageFormat.a4.height - (labelHeight * 10)) / 2 -0.75; for (int pageStart = 0; pageStart < items.length; pageStart += config.itemsPerPage) { final pageItems = items.skip(pageStart).take(config.itemsPerPage).toList(); final pageQRs = qrImages.skip(pageStart).take(config.itemsPerPage).toList(); pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, // 3. Application des marges calculées (plus de pw.Center) margin: pw.EdgeInsets.only( left: leftMargin, top: topMargin, right: 0, bottom: 0 ), build: (_) => pw.Wrap( spacing: 0, runSpacing: 0, children: List.generate(pageItems.length, (i) { return pw.Container( width: labelWidth, height: labelHeight, padding: const pw.EdgeInsets.all(2), child: pw.Row( children: [ // QR Code à gauche pw.Container( width: labelHeight - 4, height: labelHeight - 4, child: pw.Image(pw.MemoryImage(pageQRs[i])), ), pw.SizedBox(width: 4), // Texte à droite pw.Expanded( child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // Logo if (_cachedLogoBytes != null && _cachedLogoBytes!.isNotEmpty) pw.Container( height: 12, alignment: pw.Alignment.centerLeft, margin: const pw.EdgeInsets.only(bottom: 2), child: pw.Image(pw.MemoryImage(_cachedLogoBytes!)), ), pw.Text( getId(pageItems[i]), style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold), maxLines: 1, ), if (getTitle != null) pw.Text( _truncate(getTitle(pageItems[i]), 18), style: const pw.TextStyle(fontSize: 6, color: PdfColors.grey700), maxLines: 2, overflow: pw.TextOverflow.clip, ), ], ), ), ], ), ); }), ), ), ); } } // ======================================================================== // GRANDS LABELS (105 x 57 mm | 2 colonnes, 5 lignes) // ======================================================================== static void _addLargeLabels( pw.Document pdf, List items, String Function(T) getId, String Function(T)? getTitle, List Function(T)? getDetails, List qrImages, PDFGeneratorConfig config, ) { // UTILISATION DE LA LARGEUR A4 DIVISÉE PAR 2 // Cela garantit que 2 colonnes rentrent pile poil (210mm / 2 = 105mm) final double labelWidth = PdfPageFormat.a4.width / 2; const double labelHeight = 57.0 * PdfPageFormat.mm; const int cols = 2; const int rows = 5; final double totalGridWidth = labelWidth * cols; final double totalGridHeight = labelHeight * rows; const double innerQrSize = 45.0 * PdfPageFormat.mm; for (int pageStart = 0; pageStart < items.length; pageStart += config.itemsPerPage) { final pageItems = items.skip(pageStart).take(config.itemsPerPage).toList(); final pageQRs = qrImages.skip(pageStart).take(config.itemsPerPage).toList(); pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: pw.EdgeInsets.zero, build: (_) => pw.Center( child: pw.Container( width: totalGridWidth, height: totalGridHeight, child: pw.Wrap( spacing: 0, // Très important : 0 espace entre les colonnes runSpacing: 0, // 0 espace entre les lignes children: List.generate(pageItems.length, (i) { final item = pageItems[i]; return pw.Container( width: labelWidth, height: labelHeight, padding: const pw.EdgeInsets.all(6), // Suppression de la décoration (bordure) child: pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ // QR Code pw.Container( width: innerQrSize, height: innerQrSize, child: pw.Image(pw.MemoryImage(pageQRs[i])), ), pw.SizedBox(width: 8), // Détails pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.center, children: [ // Logo if (_cachedLogoBytes != null && _cachedLogoBytes!.isNotEmpty) pw.Container( height: 20, alignment: pw.Alignment.centerLeft, margin: const pw.EdgeInsets.only(bottom: 4), child: pw.Image(pw.MemoryImage(_cachedLogoBytes!)), ), // Titre if (getTitle != null) ...[ pw.Text( _truncate(getTitle(item), 40), style: pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold), maxLines: 2, ), ], // ID pw.SizedBox(height: 2), pw.Text( getId(item), style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey700), ), // Détails supplémentaires if (getDetails != null) ...[ pw.SizedBox(height: 4), ...getDetails(item).take(4).map((line) { return pw.Padding( padding: const pw.EdgeInsets.only(bottom: 1), child: pw.Text( _truncate(line, 35), style: const pw.TextStyle(fontSize: 7, color: PdfColors.grey800), ), ); }), ], ], ), ), ], ), ); }), ), ), ), ), ); } } /// Nettoie le cache (logo) static void clearCache() { _cachedLogoBytes = null; _logoLoadAttempted = false; } }