feat: Introduce PDFService for optimized PDF generation and caching in container and equipment management
This commit is contained in:
336
em2rp/lib/services/pdf_service.dart
Normal file
336
em2rp/lib/services/pdf_service.dart
Normal file
@@ -0,0 +1,336 @@
|
||||
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<void> _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<Uint8List> generatePDF<T>({
|
||||
required List<T> items,
|
||||
required QRLabelFormat format,
|
||||
required String Function(T) getId,
|
||||
String Function(T)? getTitle,
|
||||
List<String> 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<T>(
|
||||
pw.Document pdf,
|
||||
List<T> items,
|
||||
String Function(T) getId,
|
||||
List<Uint8List> 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<T>(
|
||||
pw.Document pdf,
|
||||
List<T> items,
|
||||
String Function(T) getId,
|
||||
String Function(T)? getTitle,
|
||||
List<Uint8List> 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<T>(
|
||||
pw.Document pdf,
|
||||
List<T> items,
|
||||
String Function(T) getId,
|
||||
String Function(T)? getTitle,
|
||||
List<String> Function(T)? getDetails,
|
||||
List<Uint8List> 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user