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: 20, ); static const medium = PDFGeneratorConfig( qrCodeSize: 250, itemsPerPage: 6, ); 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 /// Remplace PDFGeneratorService, ContainerPDFGeneratorService et UnifiedPDFGeneratorService 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 /// /// [items] : Liste des objets à générer /// [format] : Format des étiquettes (small, medium, large) /// [getId] : Fonction pour obtenir l'ID unique /// [getTitle] : Fonction pour obtenir le titre (optionnel) /// [getDetails] : Fonction pour obtenir les détails (optionnel, seulement pour large) /// [onProgress] : Callback de progression (optionnel) 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 format large if (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 (2x2 cm, 20 par page) // ======================================================================== 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 (4x4 cm, 6 par page) // ======================================================================== static void _addMediumLabels( pw.Document pdf, List items, String Function(T) getId, String Function(T)? getTitle, List qrImages, PDFGeneratorConfig config, ) { const qrSize = 113.39; // 4cm 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: 20, runSpacing: 20, children: List.generate(pageItems.length, (i) { return pw.Container( width: qrSize, height: qrSize + 30, child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, children: [ pw.Image(pw.MemoryImage(pageQRs[i])), pw.SizedBox(height: 4), pw.Text( getId(pageItems[i]), style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.center, ), if (getTitle != null) ...[ pw.SizedBox(height: 2), pw.Text( _truncate(getTitle(pageItems[i]), 25), style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey700), textAlign: pw.TextAlign.center, ), ], ], ), ); }), ), ), ); } } // ======================================================================== // GRANDS LABELS (avec détails, 10 par page) // ======================================================================== static void _addLargeLabels( pw.Document pdf, List items, String Function(T) getId, String Function(T)? getTitle, List Function(T)? getDetails, List qrImages, PDFGeneratorConfig config, ) { const qrSize = 100.0; 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) { final item = pageItems[i]; return pw.Container( width: 260, height: 120, decoration: pw.BoxDecoration( border: pw.Border.all(color: PdfColors.grey400), borderRadius: pw.BorderRadius.circular(4), ), padding: const pw.EdgeInsets.all(8), child: pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // QR Code pw.Container( width: qrSize, height: qrSize, child: pw.Image(pw.MemoryImage(pageQRs[i])), ), pw.SizedBox(width: 8), // Détails pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // Logo if (_cachedLogoBytes != null && _cachedLogoBytes!.isNotEmpty) pw.Center( child: pw.Container( height: 25, margin: const pw.EdgeInsets.only(bottom: 6), child: pw.Image(pw.MemoryImage(_cachedLogoBytes!)), ), ), // Titre if (getTitle != null) ...[ pw.SizedBox(height: 2), pw.Text( _truncate(getTitle(item), 20), style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold), maxLines: 2, ), ], // ID pw.SizedBox(height: 2), pw.Text( getId(item), style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey700), maxLines: 1, ), // Détails supplémentaires if (getDetails != null) ...[ pw.SizedBox(height: 4), ...getDetails(item).take(5).map((line) { return pw.Padding( padding: const pw.EdgeInsets.only(bottom: 1), child: pw.Text( _truncate(line, 25), style: const pw.TextStyle(fontSize: 6, color: PdfColors.grey800), ), ); }), ], ], ), ), ], ), ); }), ), ), ); } } /// Nettoie le cache (logo) static void clearCache() { _cachedLogoBytes = null; _logoLoadAttempted = false; } }