refactor: Amélioration de la génération des étiquettes PDF
Cette mise à jour refactorise en profondeur la génération des étiquettes PDF (formats moyen et grand) pour correspondre précisément aux dimensions de planches d'étiquettes standards, remplaçant la mise en page approximative par un placement calculé au millimètre près. La version de l'application est également incrémentée à `1.0.4`.
**Changements principaux sur le `PDFService` :**
- **Précision des formats d'étiquettes :**
- **Format Moyen :**
- Calibré pour des étiquettes de 49 x 26 mm, disposées en 4 colonnes et 10 lignes (40 par page).
- La mise en page est entièrement refaite : le QR code est à gauche et le texte (logo, ID, titre) est à droite, optimisant l'espace horizontal.
- Un calcul manuel des marges (`leftMargin`, `topMargin`) assure un alignement précis sur la page A4, avec une correction pour un centrage parfait.
- **Format Large :**
- Calibré pour des étiquettes de 105 x 57 mm, disposées en 2 colonnes et 5 lignes (10 par page).
- Utilise la moitié de la largeur d'une page A4 (`PdfPageFormat.a4.width / 2`) pour garantir un ajustement parfait des colonnes.
- La mise en page a été ajustée pour un meilleur centrage vertical du contenu dans l'étiquette.
- Suppression des bordures décoratives pour une impression directe sur planches prédécoupées.
- **Améliorations générales :**
- Le logo de l'entreprise est désormais inclus également sur les étiquettes de format moyen.
- Les tailles de police et la troncature du texte ont été ajustées pour chaque format afin d'éviter les débordements et d'améliorer la lisibilité.
- Le code a été nettoyé, supprimant des commentaires et des paramètres de mise en page obsolètes (`pw.Center`, `spacing`, `runSpacing`).
- **Mise à jour de la version :**
- La version de l'application est passée de `1.0.3` à `1.0.4`.
This commit is contained in:
2
em2rp/.gitignore
vendored
2
em2rp/.gitignore
vendored
@@ -44,4 +44,4 @@ app.*.map.json
|
||||
|
||||
# Environment configuration with credentials
|
||||
lib/config/env.dev.dart
|
||||
|
||||
functions/.env
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// Configuration de la version de l'application
|
||||
class AppVersion {
|
||||
static const String version = '1.0.3';
|
||||
static const String version = '1.0.4';
|
||||
|
||||
/// Retourne la version complète de l'application
|
||||
static String get fullVersion => 'v$version';
|
||||
|
||||
@@ -24,14 +24,16 @@ class PDFGeneratorConfig {
|
||||
itemsPerPage: 50,
|
||||
);
|
||||
|
||||
// 4 colonnes x 10 lignes = 40 étiquettes
|
||||
static const medium = PDFGeneratorConfig(
|
||||
qrCodeSize: 250,
|
||||
itemsPerPage: 20,
|
||||
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: 12,
|
||||
itemsPerPage: 10,
|
||||
);
|
||||
|
||||
static PDFGeneratorConfig fromFormat(QRLabelFormat format) {
|
||||
@@ -47,7 +49,6 @@ class PDFGeneratorConfig {
|
||||
}
|
||||
|
||||
/// 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;
|
||||
@@ -71,13 +72,6 @@ class PDFService {
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -93,8 +87,8 @@ class PDFService {
|
||||
final config = PDFGeneratorConfig.fromFormat(format);
|
||||
final pdf = pw.Document();
|
||||
|
||||
// Pré-charger le logo pour format large
|
||||
if (format == QRLabelFormat.large) {
|
||||
// Pré-charger le logo pour formats medium et large
|
||||
if (format == QRLabelFormat.medium || format == QRLabelFormat.large) {
|
||||
await _ensureLogoLoaded();
|
||||
}
|
||||
|
||||
@@ -124,16 +118,16 @@ class PDFService {
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PETITS LABELS (2x2 cm, 20 par page)
|
||||
// PETITS LABELS (Original: 2x2 cm approx)
|
||||
// ========================================================================
|
||||
static void _addSmallLabels<T>(
|
||||
pw.Document pdf,
|
||||
List<T> items,
|
||||
String Function(T) getId,
|
||||
List<Uint8List> qrImages,
|
||||
PDFGeneratorConfig config,
|
||||
) {
|
||||
const qrSize = 56.69; // 2cm
|
||||
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();
|
||||
@@ -169,19 +163,30 @@ class PDFService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// LABELS MOYENS (4x4 cm, 6 par page)
|
||||
// ========================================================================
|
||||
// LABELS MOYENS (49 x 26 mm | 4 colonnes, 10 lignes)
|
||||
// ========================================================================
|
||||
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
|
||||
pw.Document pdf,
|
||||
List<T> items,
|
||||
String Function(T) getId,
|
||||
String Function(T)? getTitle,
|
||||
List<Uint8List> 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();
|
||||
@@ -190,130 +195,56 @@ class PDFService {
|
||||
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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
// 3. Application des marges calculées (plus de pw.Center)
|
||||
margin: pw.EdgeInsets.only(
|
||||
left: leftMargin,
|
||||
top: topMargin,
|
||||
right: 0,
|
||||
bottom: 0
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 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,
|
||||
spacing: 0,
|
||||
runSpacing: 0,
|
||||
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),
|
||||
width: labelWidth,
|
||||
height: labelHeight,
|
||||
padding: const pw.EdgeInsets.all(2),
|
||||
child: pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
// QR Code
|
||||
// QR Code à gauche
|
||||
pw.Container(
|
||||
width: qrSize,
|
||||
height: qrSize,
|
||||
width: labelHeight - 4,
|
||||
height: labelHeight - 4,
|
||||
child: pw.Image(pw.MemoryImage(pageQRs[i])),
|
||||
),
|
||||
pw.SizedBox(width: 8),
|
||||
// Détails
|
||||
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.Center(
|
||||
child: pw.Container(
|
||||
height: 25,
|
||||
margin: const pw.EdgeInsets.only(bottom: 6),
|
||||
child: pw.Image(pw.MemoryImage(_cachedLogoBytes!)),
|
||||
),
|
||||
pw.Container(
|
||||
height: 12,
|
||||
alignment: pw.Alignment.centerLeft,
|
||||
margin: const pw.EdgeInsets.only(bottom: 2),
|
||||
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),
|
||||
getId(pageItems[i]),
|
||||
style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold),
|
||||
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),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -327,10 +258,120 @@ class PDFService {
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// GRANDS LABELS (105 x 57 mm | 2 colonnes, 5 lignes)
|
||||
// ========================================================================
|
||||
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,
|
||||
) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user