import 'dart:typed_data'; import 'dart:io' show Platform, File; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; import 'package:qr_flutter/qr_flutter.dart'; // Classe pour gérer les formats d'étiquettes Niimbot class NiimbotLabelSize { final double width; final double height; final String name; const NiimbotLabelSize(this.width, this.height, this.name); // Formats courants Niimbot B1 static const small = NiimbotLabelSize(50, 15, "Petit (50x15mm)"); static const medium = NiimbotLabelSize(40, 30, "Moyen (40x30mm)"); static const large = NiimbotLabelSize(50, 30, "Grand (50x30mm)"); PdfPageFormat get pageFormat => PdfPageFormat( width * PdfPageFormat.mm, height * PdfPageFormat.mm, marginAll: 0, ); } class PdfPrintService { pw.Font? robotoRegular; pw.Font? robotoBold; PdfPrintService(); /// Charge les polices Unicode Future loadFonts() async { if (robotoRegular != null && robotoBold != null) return; try { // Charger les polices depuis les assets ou utiliser les polices système final regularData = await rootBundle.load('assets/fonts/Roboto-Regular.ttf'); final boldData = await rootBundle.load('assets/fonts/Roboto-Bold.ttf'); robotoRegular = pw.Font.ttf(regularData); robotoBold = pw.Font.ttf(boldData); } catch (e) { print('Erreur chargement polices personnalisées: $e'); // Fallback vers les polices par défaut try { robotoRegular = await PdfGoogleFonts.robotoRegular(); robotoBold = await PdfGoogleFonts.robotoBold(); } catch (e2) { print('Erreur chargement polices Google: $e2'); // Utiliser les polices système par défaut robotoRegular = null; robotoBold = null; } } } /// Génère une image QR optimisée Future _generateQrImage(String data, {double size = 512}) async { final qrValidation = QrValidator.validate( data: data, version: QrVersions.auto, errorCorrectionLevel: QrErrorCorrectLevel.M, ); if (qrValidation.status != QrValidationStatus.valid) { throw Exception('QR code invalide: ${qrValidation.error}'); } final qrCode = qrValidation.qrCode!; final painter = QrPainter.withQr( qr: qrCode, color: Colors.black, emptyColor: Colors.white, gapless: true, embeddedImageStyle: null, eyeStyle: const QrEyeStyle( eyeShape: QrEyeShape.square, color: Colors.black, ), dataModuleStyle: const QrDataModuleStyle( dataModuleShape: QrDataModuleShape.square, color: Colors.black, ), ); final picData = await painter.toImageData( size, format: ImageByteFormat.png ); return picData!.buffer.asUint8List(); } /// Génère le PDF optimisé pour l'impression d'étiquettes Future generateQrPdf(String data, { String? productName, String? reference, bool isLabelFormat = true, }) async { await loadFonts(); final qrImageData = await _generateQrImage(data, size: 300); final qrImage = pw.MemoryImage(qrImageData); final pdf = pw.Document(); if (isLabelFormat) { // Format étiquette (petit format) pdf.addPage( pw.Page( pageFormat: const PdfPageFormat( 2.5 * PdfPageFormat.inch, 1.5 * PdfPageFormat.inch, marginAll: 0, ), build: (context) => _buildLabelContent(qrImage, data, productName, reference), ), ); } else { // Format A4 standard pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(20), build: (context) => _buildA4Content(qrImage, data, productName, reference), ), ); } return pdf.save(); } /// Contenu pour format étiquette pw.Widget _buildLabelContent( pw.MemoryImage qrImage, String data, String? productName, String? reference, ) { return pw.Container( width: double.infinity, height: double.infinity, padding: const pw.EdgeInsets.all(4), child: pw.Center( child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ // QR Code principal pw.Container( width: 70, height: 70, child: pw.Center( child: pw.Image( qrImage, width: 70, height: 70, fit: pw.BoxFit.contain, ), ), ), pw.SizedBox(height: 4), // Informations textuelles centrées if (reference != null) ...[ pw.Container( width: double.infinity, child: pw.Text( reference, style: _getTextStyle(fontSize: 8, bold: true), textAlign: pw.TextAlign.center, maxLines: 1, ), ), pw.SizedBox(height: 2), ], if (productName != null && productName.isNotEmpty) ...[ pw.Container( width: double.infinity, child: pw.Text( productName.length > 20 ? '${productName.substring(0, 17)}...' : productName, style: _getTextStyle(fontSize: 6), textAlign: pw.TextAlign.center, maxLines: 1, ), ), ], ], ), ), ); } /// Contenu pour format A4 pw.Widget _buildA4Content( pw.MemoryImage qrImage, String data, String? productName, String? reference, ) { return pw.Center( child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, children: [ if (productName != null) ...[ pw.Text( productName, style: _getTextStyle(fontSize: 18, bold: true), textAlign: pw.TextAlign.center, ), pw.SizedBox(height: 20), ], pw.Image( qrImage, width: 200, height: 200, fit: pw.BoxFit.contain, ), pw.SizedBox(height: 20), if (reference != null) ...[ pw.Text( 'Référence: $reference', style: _getTextStyle(fontSize: 14, bold: true), ), pw.SizedBox(height: 10), ], pw.Text( 'URL: $data', style: _getTextStyle(fontSize: 12), textAlign: pw.TextAlign.center, ), ], ), ); } /// Obtient le style de texte approprié pw.TextStyle _getTextStyle({double fontSize = 12, bool bold = false}) { if (bold && robotoBold != null) { return pw.TextStyle(font: robotoBold!, fontSize: fontSize); } else if (!bold && robotoRegular != null) { return pw.TextStyle(font: robotoRegular!, fontSize: fontSize); } else { return pw.TextStyle(fontSize: fontSize, fontWeight: bold ? pw.FontWeight.bold : pw.FontWeight.normal); } } /// Imprime le QR code avec options Future printQr( String data, { String? productName, String? reference, bool isLabelFormat = true, bool showPreview = true, }) async { try { final pdfBytes = await generateQrPdf( data, productName: productName, reference: reference, isLabelFormat: isLabelFormat, ); if (Platform.isWindows || Platform.isAndroid || Platform.isIOS || Platform.isMacOS) { await Printing.layoutPdf( onLayout: (format) async => pdfBytes, name: 'QR_Code_${reference ?? 'etiquette'}', format: isLabelFormat ? const PdfPageFormat( 2.5 * PdfPageFormat.inch, 1.5 * PdfPageFormat.inch, marginAll: 0, ) : PdfPageFormat.a4, usePrinterSettings: false, ); Get.snackbar( 'Succès', 'QR Code envoyé à l\'imprimante', backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 2), ); } else { Get.snackbar( 'Info', 'Impression non supportée sur cette plateforme', backgroundColor: Colors.orange, colorText: Colors.white, ); } } catch (e) { print('Erreur impression: $e'); Get.snackbar( 'Erreur', 'Impossible d\'imprimer: ${e.toString()}', backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 3), ); } } // ===== NOUVELLES MÉTHODES OPTIMISÉES POUR NIIMBOT B1 ===== Future printQrNiimbotOptimized( String data, { String? productName, String? reference, double leftPadding = 1.0, double topPadding = 0.5, double qrSize = 11.5, // légèrement plus petit pour laisser la place au texte double fontSize = 5.0, NiimbotLabelSize labelSize = NiimbotLabelSize.small, }) async { try { await loadFonts(); // Générer le QR code en image mémoire final qrImageData = await _generateQrImage(data, size: 110); final qrImage = pw.MemoryImage(qrImageData); final pdf = pw.Document(); pdf.addPage( pw.Page( pageFormat: labelSize.pageFormat, margin: pw.EdgeInsets.zero, build: (context) { return pw.Container( width: double.infinity, height: double.infinity, padding: pw.EdgeInsets.only( left: leftPadding * PdfPageFormat.mm, top: topPadding * PdfPageFormat.mm, right: 1 * PdfPageFormat.mm, bottom: 0.3 * PdfPageFormat.mm, // bordure minimale ), child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ // --- QR code --- pw.Container( width: qrSize * PdfPageFormat.mm, height: qrSize * PdfPageFormat.mm, child: pw.Image(qrImage, fit: pw.BoxFit.contain), ), // --- Référence directement sous le QR --- if (reference != null && reference.isNotEmpty) pw.Padding( padding: pw.EdgeInsets.only(top: 0.3 * PdfPageFormat.mm), child: pw.Text( reference, style: pw.TextStyle( fontSize: fontSize, fontWeight: pw.FontWeight.bold, ), textAlign: pw.TextAlign.center, maxLines: 1, overflow: pw.TextOverflow.clip, ), ), ], ), ); }, ), ); final pdfBytes = await pdf.save(); // Impression via Printing await Printing.layoutPdf( onLayout: (format) async => pdfBytes, name: 'QR_${reference ?? DateTime.now().millisecondsSinceEpoch}', format: labelSize.pageFormat, ); Get.snackbar( 'Succès', 'QR imprimé (${labelSize.name}) avec référence visible', backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 2), ); } catch (e) { print('Erreur impression Niimbot: $e'); Get.snackbar( 'Erreur', 'Impression échouée: ${e.toString()}', backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 3), ); } } /// Méthode de calibrage pour trouver le décalage optimal Future printNiimbotCalibration({ NiimbotLabelSize labelSize = NiimbotLabelSize.small, }) async { try { final pdf = pw.Document(); pdf.addPage( pw.Page( pageFormat: labelSize.pageFormat, build: (context) => pw.Container( width: double.infinity, height: double.infinity, decoration: pw.BoxDecoration( border: pw.Border.all(width: 0.5, color: PdfColors.black), ), child: pw.Stack( children: [ // Grille de référence pw.Positioned( left: 0, top: 0, child: pw.Container( width: labelSize.width * PdfPageFormat.mm, height: labelSize.height * PdfPageFormat.mm, child: pw.CustomPaint( painter: (PdfGraphics canvas, PdfPoint size) { // Lignes verticales tous les 5mm for (int i = 5; i < labelSize.width; i += 5) { canvas.drawLine( i * PdfPageFormat.mm, 0, i * PdfPageFormat.mm, labelSize.height * PdfPageFormat.mm, ); } // Lignes horizontales tous les 5mm for (int i = 5; i < labelSize.height; i += 5) { canvas.drawLine( 0, i * PdfPageFormat.mm, labelSize.width * PdfPageFormat.mm, i * PdfPageFormat.mm, ); } }, ), ), ), // Marqueur centre pw.Positioned( left: (labelSize.width / 2 - 3) * PdfPageFormat.mm, top: (labelSize.height / 2 - 3) * PdfPageFormat.mm, child: pw.Container( width: 6 * PdfPageFormat.mm, height: 6 * PdfPageFormat.mm, color: PdfColors.black, child: pw.Center( child: pw.Text( 'C', style: pw.TextStyle( color: PdfColors.white, fontSize: 5, fontWeight: pw.FontWeight.bold, ), ), ), ), ), // Coin haut-gauche pw.Positioned( left: 1 * PdfPageFormat.mm, top: 1 * PdfPageFormat.mm, child: pw.Container( width: 4 * PdfPageFormat.mm, height: 4 * PdfPageFormat.mm, color: PdfColors.black, child: pw.Center( child: pw.Text( '1', style: pw.TextStyle( color: PdfColors.white, fontSize: 3, fontWeight: pw.FontWeight.bold, ), ), ), ), ), // Titre pw.Positioned( left: 1 * PdfPageFormat.mm, bottom: 1 * PdfPageFormat.mm, child: pw.Text( 'CALIBRAGE ${labelSize.name}', style: _getTextStyle(fontSize: 3, bold: true), ), ), ], ), ), ), ); final pdfBytes = await pdf.save(); await Printing.layoutPdf( onLayout: (format) async => pdfBytes, name: 'Calibrage_Niimbot_${DateTime.now().millisecondsSinceEpoch}', format: labelSize.pageFormat, usePrinterSettings: false, ); Get.snackbar( 'Calibrage', 'Page de calibrage envoyée (${labelSize.name})', backgroundColor: Colors.blue, colorText: Colors.white, duration: const Duration(seconds: 4), ); } catch (e) { Get.snackbar( 'Erreur calibrage', 'Impossible d\'imprimer: $e', backgroundColor: Colors.red, colorText: Colors.white, ); } } /// Méthode avec paramètres ajustables pour corriger le décalage Future printQrNiimbotWithOffset( String data, { String? productName, String? reference, double offsetX = 0.0, double offsetY = 0.0, double scale = 1.0, NiimbotLabelSize labelSize = NiimbotLabelSize.small, }) async { try { await loadFonts(); final qrImageData = await _generateQrImage(data, size: 120); final qrImage = pw.MemoryImage(qrImageData); final pdf = pw.Document(); pdf.addPage( pw.Page( pageFormat: labelSize.pageFormat, build: (context) => pw.Container( width: double.infinity, height: double.infinity, child: pw.Stack( children: [ pw.Positioned( left: (labelSize.width / 2 + offsetX) * PdfPageFormat.mm - (15 * scale) * PdfPageFormat.mm / 2, top: (labelSize.height / 2 + offsetY) * PdfPageFormat.mm - (15 * scale) * PdfPageFormat.mm / 2, child: pw.Transform( transform: Matrix4.identity()..scale(scale), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Container( width: 12 * PdfPageFormat.mm, height: 12 * PdfPageFormat.mm, child: pw.Image( qrImage, fit: pw.BoxFit.contain, ), ), if (reference != null && reference.isNotEmpty) ...[ pw.SizedBox(height: 0.5 * PdfPageFormat.mm), pw.Container( width: 20 * PdfPageFormat.mm, child: pw.Text( reference.length > 12 ? '${reference.substring(0, 12)}...' : reference, style: _getTextStyle(fontSize: 4, bold: true), textAlign: pw.TextAlign.center, maxLines: 1, ), ), ], ], ), ), ), ], ), ), ), ); final pdfBytes = await pdf.save(); await Printing.layoutPdf( onLayout: (format) async => pdfBytes, name: 'QR_Offset_X${offsetX}_Y${offsetY}_S${scale}', format: labelSize.pageFormat, usePrinterSettings: false, ); Get.snackbar( 'Test décalage', 'QR imprimé avec décalage X:${offsetX}mm Y:${offsetY}mm Scale:${scale}', backgroundColor: Colors.orange, colorText: Colors.white, duration: const Duration(seconds: 3), ); } catch (e) { Get.snackbar( 'Erreur test', 'Impossible d\'imprimer: $e', backgroundColor: Colors.red, colorText: Colors.white, ); } } /// Widget de dialogue pour sélectionner le format d'étiquette void showNiimbotFormatDialog(Function(NiimbotLabelSize) onFormatSelected) { Get.dialog( AlertDialog( title: Row( children: [ Icon(Icons.straighten, color: Colors.orange[700]), const SizedBox(width: 8), const Text('Taille d\'étiquette'), ], ), content: Container( width: 300, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Sélectionnez la taille de vos étiquettes Niimbot B1 :', style: TextStyle(fontSize: 14, color: Colors.grey.shade700), ), const SizedBox(height: 16), // Option Petit _buildFormatOption( size: NiimbotLabelSize.small, icon: Icons.label_outline, color: Colors.green, onTap: () { Get.back(); onFormatSelected(NiimbotLabelSize.small); }, ), const SizedBox(height: 8), // Option Moyen _buildFormatOption( size: NiimbotLabelSize.medium, icon: Icons.label, color: Colors.orange, onTap: () { Get.back(); onFormatSelected(NiimbotLabelSize.medium); }, ), const SizedBox(height: 8), // Option Grand _buildFormatOption( size: NiimbotLabelSize.large, icon: Icons.label_important, color: Colors.red, onTap: () { Get.back(); onFormatSelected(NiimbotLabelSize.large); }, ), ], ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Annuler'), ), ], ), ); } Widget _buildFormatOption({ required NiimbotLabelSize size, required IconData icon, required Color color, required VoidCallback onTap, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( border: Border.all(color: color.withOpacity(0.5)), borderRadius: BorderRadius.circular(8), color: color.withOpacity(0.1), ), child: Row( children: [ Icon(icon, color: color, size: 32), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( size.name, style: TextStyle( fontWeight: FontWeight.bold, color: color.withOpacity(0.7), fontSize: 14, ), ), Text( '${size.width.toInt()}mm x ${size.height.toInt()}mm', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), ], ), ), Icon(Icons.arrow_forward_ios, size: 16, color: color), ], ), ), ); } /// Widget de dialogue pour ajuster les paramètres d'impression void showNiimbotSettingsDialog() { final offsetXController = TextEditingController(text: '0.0'); final offsetYController = TextEditingController(text: '0.0'); final scaleController = TextEditingController(text: '1.0'); final qrSizeController = TextEditingController(text: '12.0'); // Variable pour stocker la taille sélectionnée Rx selectedSize = NiimbotLabelSize.small.obs; Get.dialog( Obx(() => AlertDialog( title: Row( children: [ Icon(Icons.settings, color: Colors.blue.shade700), const SizedBox(width: 8), const Text('Paramètres Niimbot B1'), ], ), content: Container( width: 400, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ // Sélection de format Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Format d\'étiquette :', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orange.shade700), ), const SizedBox(height: 8), DropdownButton( value: selectedSize.value, isExpanded: true, items: [ DropdownMenuItem(value: NiimbotLabelSize.small, child: Text(NiimbotLabelSize.small.name)), DropdownMenuItem(value: NiimbotLabelSize.medium, child: Text(NiimbotLabelSize.medium.name)), DropdownMenuItem(value: NiimbotLabelSize.large, child: Text(NiimbotLabelSize.large.name)), ], onChanged: (value) { if (value != null) { selectedSize.value = value; } }, ), ], ), ), const SizedBox(height: 16), // Instructions Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Conseils pour corriger le décalage :', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue.shade700), ), const SizedBox(height: 4), Text('• Décalage X : + = droite, - = gauche', style: TextStyle(fontSize: 12)), Text('• Décalage Y : + = bas, - = haut', style: TextStyle(fontSize: 12)), Text('• Échelle : 0.9 = plus petit, 1.1 = plus grand', style: TextStyle(fontSize: 12)), ], ), ), const SizedBox(height: 16), // Champs de saisie Row( children: [ Expanded( child: TextField( controller: offsetXController, decoration: const InputDecoration( labelText: 'Décalage X (mm)', border: OutlineInputBorder(), ), keyboardType: TextInputType.numberWithOptions(decimal: true), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: offsetYController, decoration: const InputDecoration( labelText: 'Décalage Y (mm)', border: OutlineInputBorder(), ), keyboardType: TextInputType.numberWithOptions(decimal: true), ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: TextField( controller: scaleController, decoration: const InputDecoration( labelText: 'Échelle', border: OutlineInputBorder(), ), keyboardType: TextInputType.numberWithOptions(decimal: true), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: qrSizeController, decoration: const InputDecoration( labelText: 'Taille QR (mm)', border: OutlineInputBorder(), ), keyboardType: TextInputType.numberWithOptions(decimal: true), ), ), ], ), ], ), ), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('Fermer'), ), TextButton( onPressed: () async { await printNiimbotCalibration(labelSize: selectedSize.value); }, child: const Text('Calibrage'), style: TextButton.styleFrom(foregroundColor: Colors.blue), ), ElevatedButton( onPressed: () async { final offsetX = double.tryParse(offsetXController.text) ?? 0.0; final offsetY = double.tryParse(offsetYController.text) ?? 0.0; final scale = double.tryParse(scaleController.text) ?? 1.0; Get.back(); await printQrNiimbotWithOffset( 'https://stock.guycom.mg/TEST_${DateTime.now().millisecondsSinceEpoch}', reference: 'TEST-${DateTime.now().millisecond}', offsetX: offsetX, offsetY: offsetY, scale: scale, labelSize: selectedSize.value, ); }, child: const Text('Test'), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, foregroundColor: Colors.white, ), ), ], )), ); } // ===== MÉTHODES ORIGINALES (CONSERVÉES POUR COMPATIBILITÉ) ===== /// Méthode spécialisée pour imprimantes Niimbot (ancienne version) Future printQrNiimbot( String data, { String? productName, String? reference, }) async { try { await loadFonts(); final qrImageData = await _generateQrImage(data, size: 200); final qrImage = pw.MemoryImage(qrImageData); final pdf = pw.Document(); // Format spécifique Niimbot : 50mm x 30mm pdf.addPage( pw.Page( pageFormat: const PdfPageFormat( 50 * PdfPageFormat.mm, 30 * PdfPageFormat.mm, marginAll: 0, ), build: (context) => pw.Container( width: double.infinity, height: double.infinity, child: pw.Center( child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ // QR Code centré pw.Container( width: 20 * PdfPageFormat.mm, height: 20 * PdfPageFormat.mm, child: pw.Image( qrImage, fit: pw.BoxFit.contain, ), ), if (reference != null) ...[ pw.SizedBox(height: 2 * PdfPageFormat.mm), pw.Text( reference, style: _getTextStyle(fontSize: 8, bold: true), textAlign: pw.TextAlign.center, ), ], ], ), ), ), ), ); final pdfBytes = await pdf.save(); await Printing.layoutPdf( onLayout: (format) async => pdfBytes, name: 'QR_Niimbot_${reference ?? 'etiquette'}', format: const PdfPageFormat( 50 * PdfPageFormat.mm, 30 * PdfPageFormat.mm, marginAll: 0, ), usePrinterSettings: false, ); Get.snackbar( 'Succès', 'QR Code Niimbot envoyé à l\'imprimante', backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 2), ); } catch (e) { print('Erreur impression Niimbot: $e'); Get.snackbar( 'Erreur', 'Impossible d\'imprimer sur Niimbot: ${e.toString()}', backgroundColor: Colors.red, colorText: Colors.white, duration: const Duration(seconds: 3), ); } } /// Prévisualise le PDF avant impression Future previewQr( String data, { String? productName, String? reference, bool isLabelFormat = true, }) async { try { final pdfBytes = await generateQrPdf( data, productName: productName, reference: reference, isLabelFormat: isLabelFormat, ); await Printing.layoutPdf( onLayout: (format) async => pdfBytes, name: 'Aperçu_QR_Code', ); } catch (e) { print('Erreur aperçu: $e'); Get.snackbar( 'Erreur', 'Impossible d\'afficher l\'aperçu: ${e.toString()}', backgroundColor: Colors.red, colorText: Colors.white, ); } } /// Sauvegarde le PDF Future saveQrPdf( String data, { String? productName, String? reference, bool isLabelFormat = true, String? fileName, }) async { try { final pdfBytes = await generateQrPdf( data, productName: productName, reference: reference, isLabelFormat: isLabelFormat, ); final directory = await getApplicationDocumentsDirectory(); final file = File('${directory.path}/${fileName ?? 'qr_code_${reference ?? DateTime.now().millisecondsSinceEpoch}'}.pdf'); await file.writeAsBytes(pdfBytes); Get.snackbar( 'Succès', 'PDF sauvegardé: ${file.path}', backgroundColor: Colors.green, colorText: Colors.white, duration: const Duration(seconds: 3), ); } catch (e) { print('Erreur sauvegarde: $e'); Get.snackbar( 'Erreur', 'Impossible de sauvegarder: ${e.toString()}', backgroundColor: Colors.red, colorText: Colors.white, ); } } }