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:
ElPoyo
2026-01-16 19:23:57 +01:00
parent 4e7af9119a
commit 7e111ec041
3 changed files with 182 additions and 141 deletions

2
em2rp/.gitignore vendored
View File

@@ -44,4 +44,4 @@ app.*.map.json
# Environment configuration with credentials
lib/config/env.dev.dart
functions/.env

View File

@@ -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';

View File

@@ -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,7 +118,7 @@ class PDFService {
}
// ========================================================================
// PETITS LABELS (2x2 cm, 20 par page)
// PETITS LABELS (Original: 2x2 cm approx)
// ========================================================================
static void _addSmallLabels<T>(
pw.Document pdf,
@@ -133,7 +127,7 @@ class PDFService {
List<Uint8List> qrImages,
PDFGeneratorConfig config,
) {
const qrSize = 56.69; // 2cm
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,9 +163,8 @@ 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,
@@ -181,7 +174,19 @@ class PDFService {
List<Uint8List> qrImages,
PDFGeneratorConfig config,
) {
const qrSize = 113.39; // 4cm
// 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,32 +195,59 @@ class PDFService {
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(20),
// 3. Application des marges calculées (plus de pw.Center)
margin: pw.EdgeInsets.only(
left: leftMargin,
top: topMargin,
right: 0,
bottom: 0
),
build: (_) => pw.Wrap(
spacing: 20,
runSpacing: 20,
spacing: 0,
runSpacing: 0,
children: List.generate(pageItems.length, (i) {
return pw.Container(
width: qrSize,
height: qrSize + 30,
width: labelWidth,
height: labelHeight,
padding: const pw.EdgeInsets.all(2),
child: pw.Row(
children: [
// QR Code à gauche
pw.Container(
width: labelHeight - 4,
height: labelHeight - 4,
child: pw.Image(pw.MemoryImage(pageQRs[i])),
),
pw.SizedBox(width: 4),
// Texte à droite
pw.Expanded(
child: pw.Column(
mainAxisAlignment: pw.MainAxisAlignment.center,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Image(pw.MemoryImage(pageQRs[i])),
pw.SizedBox(height: 4),
// Logo
if (_cachedLogoBytes != null && _cachedLogoBytes!.isNotEmpty)
pw.Container(
height: 12,
alignment: pw.Alignment.centerLeft,
margin: const pw.EdgeInsets.only(bottom: 2),
child: pw.Image(pw.MemoryImage(_cachedLogoBytes!)),
),
pw.Text(
getId(pageItems[i]),
style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold),
textAlign: pw.TextAlign.center,
style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold),
maxLines: 1,
),
if (getTitle != null) ...[
pw.SizedBox(height: 2),
if (getTitle != null)
pw.Text(
_truncate(getTitle(pageItems[i]), 25),
style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey700),
textAlign: pw.TextAlign.center,
_truncate(getTitle(pageItems[i]), 18),
style: const pw.TextStyle(fontSize: 6, color: PdfColors.grey700),
maxLines: 2,
overflow: pw.TextOverflow.clip,
),
],
),
),
],
),
);
@@ -227,7 +259,7 @@ class PDFService {
}
// ========================================================================
// GRANDS LABELS (avec détails, 10 par page)
// GRANDS LABELS (105 x 57 mm | 2 colonnes, 5 lignes)
// ========================================================================
static void _addLargeLabels<T>(
pw.Document pdf,
@@ -238,7 +270,17 @@ class PDFService {
List<Uint8List> qrImages,
PDFGeneratorConfig config,
) {
const qrSize = 100.0;
// 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();
@@ -247,27 +289,28 @@ class PDFService {
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(20),
build: (_) => pw.Wrap(
spacing: 10,
runSpacing: 10,
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: 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(6),
// Suppression de la décoration (bordure)
child: pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
// QR Code
pw.Container(
width: qrSize,
height: qrSize,
width: innerQrSize,
height: innerQrSize,
child: pw.Image(pw.MemoryImage(pageQRs[i])),
),
pw.SizedBox(width: 8),
@@ -275,22 +318,21 @@ class PDFService {
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
mainAxisAlignment: pw.MainAxisAlignment.center,
children: [
// Logo
if (_cachedLogoBytes != null && _cachedLogoBytes!.isNotEmpty)
pw.Center(
child: pw.Container(
height: 25,
margin: const pw.EdgeInsets.only(bottom: 6),
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.SizedBox(height: 2),
pw.Text(
_truncate(getTitle(item), 20),
style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold),
_truncate(getTitle(item), 40),
style: pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold),
maxLines: 2,
),
],
@@ -298,18 +340,17 @@ class PDFService {
pw.SizedBox(height: 2),
pw.Text(
getId(item),
style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey700),
maxLines: 1,
style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey700),
),
// Détails supplémentaires
if (getDetails != null) ...[
pw.SizedBox(height: 4),
...getDetails(item).take(5).map((line) {
...getDetails(item).take(4).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),
_truncate(line, 35),
style: const pw.TextStyle(fontSize: 7, color: PdfColors.grey800),
),
);
}),
@@ -323,14 +364,14 @@ class PDFService {
}),
),
),
),
),
);
}
}
/// Nettoie le cache (logo)
static void clearCache() {
_cachedLogoBytes = null;
_logoLoadAttempted = false;
}
}