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 } /// Interface pour les items qui peuvent avoir un QR code abstract class QRCodeItem { String get id; String get displayName; } /// Service unifié pour la génération de PDFs avec QR codes /// Fonctionne avec n'importe quel type d'objet ayant un ID class UnifiedPDFGeneratorService { static Uint8List? _cachedLogoBytes; /// Tronque un texte s'il dépasse une longueur maximale static String _truncateText(String text, int maxLength) { if (text.length <= maxLength) { return text; } return '${text.substring(0, maxLength - 3)}...'; } /// Charge le logo en cache (optimisation) static Future _ensureLogoLoaded() async { if (_cachedLogoBytes == null) { try { final logoData = await rootBundle.load('assets/logos/LowQRectangleLogoBlack.png'); _cachedLogoBytes = logoData.buffer.asUint8List(); } catch (e) { // Logo non disponible, on continue sans _cachedLogoBytes = Uint8List(0); } } } /// Génère un PDF avec des QR codes simples (juste ID + QR) static Future generateSimpleQRCodesPDF({ required List items, required String Function(T) getId, required QRLabelFormat format, String Function(T)? getDisplayName, }) async { final pdf = pw.Document(); switch (format) { case QRLabelFormat.small: await _generateSmallQRCodesPDF(pdf, items, getId, getDisplayName); break; case QRLabelFormat.medium: await _generateMediumQRCodesPDF(pdf, items, getId, getDisplayName); break; case QRLabelFormat.large: await _generateLargeQRCodesPDF(pdf, items, getId, getDisplayName, null); break; } return pdf.save(); } /// Génère un PDF avec des QR codes avancés (avec informations supplémentaires) static Future generateAdvancedQRCodesPDF({ required List items, required String Function(T) getId, required String Function(T) getTitle, required List Function(T)? getSubtitle, required QRLabelFormat format, }) async { final pdf = pw.Document(); switch (format) { case QRLabelFormat.small: await _generateSmallQRCodesPDF(pdf, items, getId, getTitle); break; case QRLabelFormat.medium: await _generateMediumQRCodesPDF(pdf, items, getId, getTitle); break; case QRLabelFormat.large: await _generateLargeQRCodesPDF(pdf, items, getId, getTitle, getSubtitle); break; } return pdf.save(); } // ========================================================================== // PETITS QR CODES (2x2 cm, 20 par page) // ========================================================================== static Future _generateSmallQRCodesPDF( pw.Document pdf, List items, String Function(T) getId, String Function(T)? getDisplayName, ) async { const qrSize = 56.69; // 2cm en points const itemsPerPage = 20; // Générer tous les QR codes en une fois (optimisé avec résolution réduite) final allQRImages = await QRCodeService.generateBulkQRCodes( items.map((item) => getId(item)).toList(), size: 150, // Réduit de 200 à 150 pour performance optimale withLogo: false, useCache: true, ); for (int pageStart = 0; pageStart < items.length; pageStart += itemsPerPage) { final pageItems = items.skip(pageStart).take(itemsPerPage).toList(); final pageQRImages = allQRImages.skip(pageStart).take(itemsPerPage).toList(); pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(20), build: (context) { return pw.Wrap( spacing: 10, runSpacing: 10, children: List.generate(pageItems.length, (index) { return pw.Container( width: qrSize, height: qrSize + 20, child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Image(pw.MemoryImage(pageQRImages[index])), pw.SizedBox(height: 2), pw.Text( getId(pageItems[index]), style: pw.TextStyle( fontSize: 6, fontWeight: pw.FontWeight.bold, ), textAlign: pw.TextAlign.center, ), ], ), ); }), ); }, ), ); } } // ========================================================================== // QR CODES MOYENS (4x4 cm, 6 par page) // ========================================================================== static Future _generateMediumQRCodesPDF( pw.Document pdf, List items, String Function(T) getId, String Function(T)? getDisplayName, ) async { const qrSize = 113.39; // 4cm en points const itemsPerPage = 6; // Optimisé avec résolution réduite final allQRImages = await QRCodeService.generateBulkQRCodes( items.map((item) => getId(item)).toList(), size: 250, // Réduit de 400 à 250 pour performance optimale withLogo: false, useCache: true, ); for (int pageStart = 0; pageStart < items.length; pageStart += itemsPerPage) { final pageItems = items.skip(pageStart).take(itemsPerPage).toList(); final pageQRImages = allQRImages.skip(pageStart).take(itemsPerPage).toList(); pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(20), build: (context) { return pw.Wrap( spacing: 20, runSpacing: 20, children: List.generate(pageItems.length, (index) { return pw.Container( width: qrSize, height: qrSize + 30, child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, mainAxisAlignment: pw.MainAxisAlignment.center, children: [ pw.Image(pw.MemoryImage(pageQRImages[index])), pw.SizedBox(height: 4), pw.Text( getId(pageItems[index]), style: pw.TextStyle( fontSize: 10, fontWeight: pw.FontWeight.bold, ), textAlign: pw.TextAlign.center, ), if (getDisplayName != null) ...[ pw.SizedBox(height: 2), pw.Text( _truncateText(getDisplayName(pageItems[index]), 25), style: const pw.TextStyle( fontSize: 8, color: PdfColors.grey700, ), textAlign: pw.TextAlign.center, ), ], ], ), ); }), ); }, ), ); } } // ========================================================================== // GRANDES ÉTIQUETTES (QR + infos détaillées, 6 par page) // ========================================================================== static Future _generateLargeQRCodesPDF( pw.Document pdf, List items, String Function(T) getId, String Function(T)? getTitle, List Function(T)? getSubtitle, ) async { const qrSize = 100.0; const itemsPerPage = 6; // Charger le logo une seule fois await _ensureLogoLoaded(); // Générer les QR codes en bulk pour optimisation final allQRImages = await QRCodeService.generateBulkQRCodes( items.map((item) => getId(item)).toList(), size: 300, // Réduit de 400 à 300 pour améliorer la performance withLogo: false, useCache: true, ); for (int pageStart = 0; pageStart < items.length; pageStart += itemsPerPage) { final pageItems = items.skip(pageStart).take(itemsPerPage).toList(); final pageQRImages = allQRImages.skip(pageStart).take(itemsPerPage).toList(); pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(20), build: (context) { return pw.Wrap( spacing: 10, runSpacing: 10, children: List.generate(pageItems.length, (index) { final item = pageItems[index]; 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(pageQRImages[index]), fit: pw.BoxFit.contain, ), ), pw.SizedBox(width: 8), // Informations pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.start, children: [ // Logo - CENTRÉ ET PLUS GRAND if (_cachedLogoBytes != null && _cachedLogoBytes!.isNotEmpty) pw.Center( child: pw.Container( height: 25, // Augmenté de 15 à 25 margin: const pw.EdgeInsets.only(bottom: 6), child: pw.Image( pw.MemoryImage(_cachedLogoBytes!), fit: pw.BoxFit.contain, ), ), ), // ID (toujours affiché sur plusieurs lignes si nécessaire) if (getTitle != null) ...[ pw.SizedBox(height: 2), pw.Text( _truncateText(getTitle(item), 20), style: pw.TextStyle( fontSize: 10, fontWeight: pw.FontWeight.bold, ), maxLines: 2, ), ], pw.SizedBox(height: 2), pw.Text( getId(item), style: const pw.TextStyle( fontSize: 8, color: PdfColors.grey700, ), maxLines: 1, ), if (getSubtitle != null) ...[ pw.SizedBox(height: 4), ...getSubtitle(item).take(5).map((line) { return pw.Padding( padding: const pw.EdgeInsets.only(bottom: 1), child: pw.Text( _truncateText(line, 25), style: const pw.TextStyle( fontSize: 6, color: PdfColors.grey800, ), ), ); }), ], ], ), ), ], ), ); }), ); }, ), ); } } }