Le symptôme est toujours le même : vous générez un PDF/A-3 avec mPDF, vous attachez votre fichier factur-x.xml, le PDF s’ouvre correctement, la pièce jointe apparaît dans le panneau des pièces jointes d’Acrobat Reader — mais quand vous l’extrayez, elle fait 0 octets. Le flux est vide. Le XML est présent dans la structure du PDF mais son contenu a disparu.
Ce bug est une source majeure de factures Factur-X invalides dans l’écosystème PHP. Il affecte les ERP open source (Dolibarr, certains modules PrestaShop), les systèmes de facturation sur mesure, et toute application qui utilise mPDF pour produire des conteneurs PDF/A-3.
Ce guide décrit la cause exacte du problème, comment le reproduire, et quatre chemins de correction — du patch mPDF direct jusqu’au remplacement complet de la chaîne de génération.
Pour le contexte général sur les exigences PDF/A-3 pour Factur-X, voir PDF/A-3 pour Factur-X : checklist de conformité.
Ce que PDF/A-3 exige pour les fichiers embarqués
PDF/A-3 (ISO 19005-3) est le seul niveau PDF/A qui autorise les pièces jointes de type arbitraire. C’est ce qui permet d’embarquer un fichier XML CII dans un PDF. Mais l’attachement ne se résume pas à “ajouter un fichier” — la structure interne du PDF doit respecter plusieurs contraintes.
La structure d’un fichier embarqué conforme :
/Type /Filespec
/F (factur-x.xml)
/UF (factur-x.xml)
/AFRelationship /Alternative
/Desc (Factur-X XML invoice)
/EF <<
/F << /Type /EmbeddedFile
/Subtype /text#2Fxml
/Length 2847
/Params << /Size 2847 /ModDate (D:20260623120000+02'00') >>
stream ... endstream
>>
/UF (référence identique à /F)
>>
Les points critiques :
- Le flux (stream) du fichier embarqué doit contenir les données réelles. Un
Lengthde 0 ou un stream vide invalide le document. - Le sous-type MIME doit être
text/xml(encodé en PDF name comme/text#2Fxml). - AFRelationship doit correspondre à la version ciblée (
/Alternativepour Factur-X 1.x). - Le dictionnaire Names/EmbeddedFiles du catalogue PDF doit référencer le Filespec.
- Le tableau AF (Associated Files) du catalogue doit aussi référencer le Filespec.
Quand mPDF produit un fichier avec un XML de 0 bytes, c’est le point 1 qui échoue : la structure est correcte mais le flux est vide.
Pourquoi mPDF produit un XML de 0 bytes
Le problème vient de la façon dont mPDF gère l’écriture des flux de fichiers embarqués par rapport au cycle de vie du document PDF.
L’architecture interne de mPDF
mPDF génère le PDF en mémoire via un buffer. Le cycle de génération suit cet ordre :
new \Mpdf\Mpdf([...])— initialisationWriteHTML()— rendu du contenu HTMLSetAssociatedFiles()— déclaration des fichiers embarquésOutput()— sérialisation finale du PDF
Le problème se situe dans l’étape 4. Lors de Output(), mPDF appelle sa méthode interne _putresources() qui écrit les objets PDF dans l’ordre suivant :
- Images
- Polices
- Objets de formulaire
- Fichiers embarqués (associated files)
- Profil ICC et OutputIntents
- Métadonnées XMP
Mais dans certaines versions de mPDF et certaines configurations, le flux du fichier embarqué est écrit avec un contenu vide. Les causes identifiées :
Cause 1 : Compression du flux qui vide le contenu
Quand la compression Zlib est active (c’est le défaut), mPDF compresse les flux avec gzcompress(). Si le contenu du fichier associé n’a pas été correctement chargé en mémoire au moment de la compression, le résultat est un flux compressé de 0 bytes.
Cela arrive notamment quand le contenu du fichier associé n’est pas correctement chargé en mémoire au moment de la sérialisation du flux PDF — par exemple en raison d’un conflit d’encodage, d’une version de mPDF avec un bug connu, ou d’une interaction avec d’autres options de configuration (compression, timing d’écriture).
Cause 2 : Appel de SetAssociatedFiles() après le rendu
Si SetAssociatedFiles() est appelé après WriteHTML() mais que le buffer interne a déjà été finalisé (par exemple via un appel intermédiaire qui déclenche _putpages()), le fichier est enregistré dans les métadonnées mais son contenu n’est jamais écrit dans le flux.
Cause 3 : Confusion entre les clés du tableau SetAssociatedFiles()
Le paramètre path est officiellement supporté par mPDF pour référencer un fichier sur disque. Cependant, le bug XML 0 bytes peut survenir indépendamment de l’utilisation de path ou content. Les causes réelles incluent : l’encodage du contenu XML, la version spécifique de mPDF, ou des interactions avec d’autres options de configuration (mode PDF/A, compression). Passer le contenu directement via la clé content reste la méthode la plus fiable car elle élimine les problèmes potentiels de lecture de fichier au moment de la sérialisation.
Reproduire le bug
Voici le code minimal qui déclenche le problème dans mPDF 8.x :
<?php
// composer require mpdf/mpdf
require_once __DIR__ . '/vendor/autoload.php';
$mpdf = new \Mpdf\Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'PDFA' => true,
'PDFAauto' => true,
]);
$html = '<h1>Facture F-2026-0042</h1><p>Montant TTC : 1 200,00 EUR</p>';
$mpdf->WriteHTML($html);
// Configuration qui peut déclencher le bug dans certaines versions de mPDF
$mpdf->SetAssociatedFiles([
[
'name' => 'factur-x.xml',
'mime' => 'text/xml',
'description' => 'Factur-X XML invoice',
'AFRelationship' => 'Alternative',
'path' => __DIR__ . '/factur-x.xml', // path est supporté mais moins fiable que content
]
]);
$mpdf->Output(__DIR__ . '/facture-output.pdf', \Mpdf\Output\Destination::FILE);
Vérification du résultat :
<?php
// Vérification rapide avec un script PHP
// ATTENTION : cette méthode par regex fonctionne sur les PDF non compressés.
// Pour les PDF avec compression de flux (FlateDecode), extraire le fichier
// embarqué avec un outil dédié (pypdf, pdfdetach) est plus fiable.
$pdfContent = file_get_contents(__DIR__ . '/facture-output.pdf');
// Chercher le stream du fichier embarqué
if (preg_match('/\/F\s*\(factur-x\.xml\)/', $pdfContent)) {
echo "Filespec présent dans le PDF\n";
} else {
echo "ERREUR: Filespec absent\n";
}
// Chercher la taille du flux embarqué (fiable uniquement sans compression)
if (preg_match('/\/Type\s*\/EmbeddedFile.*?\/Length\s+(\d+)/s', $pdfContent, $matches)) {
$length = (int) $matches[1];
echo "Taille du flux embarqué : {$length} bytes\n";
if ($length === 0) {
echo "BUG CONFIRMÉ : flux XML vide (0 bytes)\n";
}
} else {
echo "ERREUR: EmbeddedFile non trouvé\n";
}
Pour un diagnostic fiable sur tous les PDF (y compris compressés), utiliser Python :
from pypdf import PdfReader
reader = PdfReader("facture-output.pdf")
for name, data_list in reader.attachments.items():
for data in data_list:
print(f"{name}: {len(data)} bytes")
# Output attendu si bug présent :
# factur-x.xml: 0 bytes
Fix 1 : Utilisation correcte de mPDF
Le premier chemin de correction est de rester avec mPDF mais de maximiser la fiabilité de l’écriture du flux embarqué. Bien que le paramètre path soit officiellement supporté, passer le contenu brut du XML via la clé content est la méthode la plus fiable pour éviter les problèmes de flux vide. Contrôler la compression aide aussi au diagnostic.
<?php
require_once __DIR__ . '/vendor/autoload.php';
// Lire le contenu XML en mémoire AVANT de créer le PDF
$xmlContent = file_get_contents(__DIR__ . '/factur-x.xml');
if ($xmlContent === false) {
throw new \RuntimeException('Impossible de lire factur-x.xml');
}
$mpdf = new \Mpdf\Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'PDFA' => true,
'PDFAauto' => true,
]);
$html = '<h1>Facture F-2026-0042</h1><p>Montant TTC : 1 200,00 EUR</p>';
$mpdf->WriteHTML($html);
// Fix : passer le contenu brut, pas le chemin
$mpdf->SetAssociatedFiles([
[
'name' => 'factur-x.xml',
'mime' => 'text/xml',
'description' => 'Factur-X XML invoice',
'AFRelationship' => 'Alternative',
'content' => $xmlContent, // contenu brut du XML
]
]);
$mpdf->Output(__DIR__ . '/facture-corrigee.pdf', \Mpdf\Output\Destination::FILE);
Points d’attention :
- La clé du tableau est
content, paspathnifile. La documentation de mPDF n’est pas toujours explicite sur ce point. - Le XML doit être lu en mémoire avant l’appel à
SetAssociatedFiles(). Ne pas utiliser de référence tardive. SetAssociatedFiles()doit être appelé aprèsWriteHTML()mais avantOutput(). Cet ordre est non-négociable.- Si le bug persiste malgré l’utilisation de
content, désactiver la compression interne pour diagnostiquer :
$mpdf = new \Mpdf\Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'PDFA' => true,
'PDFAauto' => true,
'compress' => false, // désactiver la compression pour diagnostic
]);
Si le XML apparaît correctement avec compress => false mais pas avec compress => true, le problème est dans la gestion Zlib de mPDF pour les flux de fichiers embarqués. Dans ce cas, les alternatives ci-dessous sont plus fiables.
Avertissement important : même avec cette correction, mPDF n’est pas garanti de produire un PDF/A-3 pleinement conforme. Les discussions sur le dépôt GitHub de mPDF indiquent que les fichiers produits sont parfois identifiés comme PDF/A-1b par VeraPDF, même quand le mode PDFA => true est activé. Validez systématiquement avec VeraPDF avant de déployer en production.
Fix 2 : Utiliser la bibliothèque atgp/factur-x
La bibliothèque atgp/factur-x (namespace Atgp\FacturX) est dédiée à la génération de fichiers Factur-X. Elle gère elle-même la couche PDF/A-3 en s’appuyant sur FPDI pour manipuler des PDF existants.
Son approche est différente de mPDF : au lieu de générer le PDF de zéro, elle prend un PDF existant (généré par n’importe quel outil) et y injecte le XML Factur-X avec les métadonnées PDF/A-3 correctes.
composer require atgp/factur-x
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Atgp\FacturX\Facturx;
// Étape 1 : générer le PDF visuel avec n'importe quel outil
// (mPDF, TCPDF, wkhtmltopdf, Chromium headless, etc.)
$pdfContent = file_get_contents(__DIR__ . '/facture-visuelle.pdf');
$xmlContent = file_get_contents(__DIR__ . '/factur-x.xml');
if ($pdfContent === false || $xmlContent === false) {
throw new \RuntimeException('Fichiers source introuvables');
}
// Étape 2 : générer le PDF/A-3 Factur-X
$facturx = new Facturx();
$facturxPdf = $facturx->generateFacturx(
$pdfContent,
$xmlContent,
Facturx::PROFIL_EN16931 // ou PROFIL_BASIC, PROFIL_MINIMUM, etc.
);
file_put_contents(__DIR__ . '/facture-facturx.pdf', $facturxPdf);
Avantages de cette approche :
- La séparation entre génération visuelle et conformité PDF/A-3 est nette. Vous pouvez utiliser mPDF, TCPDF, Dompdf, wkhtmltopdf ou tout autre outil pour le rendu HTML-vers-PDF, sans vous soucier de la conformité PDF/A-3.
- La bibliothèque gère l’attachement du XML, la validation XSD, les métadonnées XMP Factur-X (namespace
fx:, extension schema PDF/A), l’AFRelationship et le dictionnaire EmbeddedFiles. Cependant, la conformité PDF/A-3 complète (profils ICC, polices embarquées) dépend aussi du PDF source fourni en entrée et du moteur PDF utilisé en amont. - Le flux XML est garanti non-vide car la bibliothèque écrit le contenu directement dans le stream PDF.
Limites :
- La bibliothèque repose sur FPDI, qui a ses propres contraintes (ne supporte pas tous les PDF en entrée, notamment les PDF chiffrés ou avec des formulaires complexes).
- Le PDF en entrée doit déjà contenir les polices embarquées et être relativement propre. Un PDF avec des polices système non embarquées produira un PDF/A-3 invalide au niveau des polices, même si la couche Factur-X est correcte.
Fix 3 : Utiliser horstoeko/zugferd
La bibliothèque horstoeko/zugferd est l’alternative utilisée par InvoiceNinja. Elle prend une approche différente : elle génère le XML à partir d’une API PHP, puis l’embarque dans un PDF existant.
composer require horstoeko/zugferd
Pour la partie embarquement du XML dans le PDF (si vous avez déjà votre XML généré) :
<?php
require_once __DIR__ . '/vendor/autoload.php';
use horstoeko\zugferd\ZugferdDocumentPdfMerger;
$xmlContent = file_get_contents(__DIR__ . '/factur-x.xml');
$pdfPath = __DIR__ . '/facture-visuelle.pdf';
$outputPath = __DIR__ . '/facture-zugferd.pdf';
if ($xmlContent === false) {
throw new \RuntimeException('Fichier XML introuvable');
}
// Merger le XML dans le PDF existant
$merger = new ZugferdDocumentPdfMerger($xmlContent, $pdfPath);
$merger->generateDocument();
$merger->saveDocument($outputPath);
Si vous voulez aussi générer le XML Factur-X via l’API de la bibliothèque :
<?php
require_once __DIR__ . '/vendor/autoload.php';
use horstoeko\zugferd\ZugferdDocumentBuilder;
use horstoeko\zugferd\ZugferdProfiles;
// Construire le document XML EN16931
$builder = ZugferdDocumentBuilder::createNew(ZugferdProfiles::PROFILE_EN16931);
$builder
->setDocumentInformation('F-2026-0042', '380', new \DateTime('2026-06-23'))
->setDocumentBuyerReference('PO-2026-1234')
->addDocumentNote('Facture de test')
->setDocumentSupplyChainEvent(new \DateTime('2026-06-23'));
// Vendeur
$builder
->setDocumentSeller('Mon Entreprise SAS')
->addDocumentSellerTaxRegistration('VA', 'FR12345678901')
->setDocumentSellerAddress('12 rue de la Paix', '', '', '75002', 'Paris', 'FR')
->setDocumentSellerContact('Service facturation', '', '0142000000', '', '[email protected]');
// Acheteur
$builder
->setDocumentBuyer('Client SARL')
->setDocumentBuyerAddress('8 avenue des Champs', '', '', '75008', 'Paris', 'FR');
// Conditions de paiement
$builder
->addDocumentPaymentMean(30) // virement
->setDocumentPaymentTerms('Paiement à 30 jours');
// Ligne de facture
$builder
->addNewPosition('1')
->setDocumentPositionProductDetails('Service de conseil', '', 'SC-001')
->setDocumentPositionNetPrice(1000.00)
->setDocumentPositionQuantity(1, 'HUR')
->addDocumentPositionTax('S', 'VAT', 20.0)
->setDocumentPositionLineSummation(1000.00);
// Totaux
$builder
->setDocumentSummation(1200.00, 1200.00, 1000.00, 0.00, 0.00, 1000.00, 200.00)
->addDocumentTax('S', 'VAT', 1000.00, 200.00, 20.0);
// Générer le XML
$xmlContent = $builder->getContent();
// Merger dans le PDF
$merger = new \horstoeko\zugferd\ZugferdDocumentPdfMerger(
$xmlContent,
__DIR__ . '/facture-visuelle.pdf'
);
$merger->generateDocument();
$merger->saveDocument(__DIR__ . '/facture-finale.pdf');
Avantages :
- Bibliothèque activement maintenue, utilisée en production par InvoiceNinja et d’autres projets.
- API de construction du XML qui assure la conformité structurelle (namespace, profils, types).
- Le merger PDF/A-3 gère les métadonnées XMP et le profil ICC.
Limites :
- L’API de construction du XML est verbeuse. Pour des factures complexes, le code devient long.
- Le support des profils EXTENDED est moins mature que pour EN16931.
Pour les erreurs de validation spécifiques à InvoiceNinja, voir InvoiceNinja et Factur-X : valider ses factures avant la réforme 2026.
Fix 4 : Post-traitement avec FPDI pour ré-embarquer le XML
Si vous avez un PDF généré par mPDF (ou tout autre outil) avec un XML de 0 bytes, vous pouvez le corriger en post-traitement avec FPDI, sans régénérer le PDF.
composer require setasign/fpdi
composer require setasign/fpdf
<?php
require_once __DIR__ . '/vendor/autoload.php';
use setasign\Fpdi\Fpdi;
/**
* Ré-embarque un fichier XML dans un PDF existant en remplacement
* d'un attachement vide ou corrompu.
*
* ATTENTION : cette approche ne garantit pas à elle seule la conformité
* PDF/A-3 complète (métadonnées XMP, ICC, etc.). Elle corrige uniquement
* le flux du fichier embarqué.
*/
function reembedXmlInPdf(string $pdfPath, string $xmlPath, string $outputPath): void
{
$xmlContent = file_get_contents($xmlPath);
if ($xmlContent === false) {
throw new \RuntimeException("Impossible de lire : {$xmlPath}");
}
$pdf = new Fpdi();
// Importer toutes les pages du PDF source
$pageCount = $pdf->setSourceFile($pdfPath);
for ($i = 1; $i <= $pageCount; $i++) {
$templateId = $pdf->importPage($i);
$size = $pdf->getTemplateSize($templateId);
$pdf->AddPage($size['orientation'], [$size['width'], $size['height']]);
$pdf->useTemplate($templateId);
}
$pdf->Output('F', $outputPath);
// À ce stade, le PDF est régénéré sans l'attachement.
// L'embarquement du XML doit être fait via une manipulation
// bas niveau du PDF ou via atgp/factur-x sur le PDF produit.
}
// Utilisation combinée : FPDI pour nettoyer + atgp/factur-x pour ré-embarquer
$cleanPdf = __DIR__ . '/facture-clean.pdf';
reembedXmlInPdf(
__DIR__ . '/facture-bug-0bytes.pdf',
__DIR__ . '/factur-x.xml',
$cleanPdf
);
// Ré-embarquer proprement avec atgp/factur-x
$facturx = new \Atgp\FacturX\Facturx();
$result = $facturx->generateFacturx(
file_get_contents($cleanPdf),
file_get_contents(__DIR__ . '/factur-x.xml'),
\Atgp\FacturX\Facturx::PROFIL_EN16931
);
file_put_contents(__DIR__ . '/facture-finale.pdf', $result);
// Nettoyer le fichier intermédiaire
unlink($cleanPdf);
Cette approche en deux étapes est utile quand vous avez un pipeline existant basé sur mPDF que vous ne pouvez pas facilement remplacer : vous gardez mPDF pour le rendu visuel, puis vous corrigez la couche PDF/A-3 en post-traitement.
Valider que le XML n’est pas vide
Après correction, il faut vérifier que le flux du fichier embarqué contient bien le XML. Voici un script de validation complet :
<?php
/**
* Vérifie qu'un PDF contient un fichier XML embarqué non vide.
* Ce script fait un diagnostic basique sans dépendance externe.
* LIMITATION : cette approche par regex ne fonctionne que sur les PDF
* non compressés. Pour les PDF avec FlateDecode, utiliser pypdf ou
* pdfdetach pour extraire et vérifier le fichier embarqué.
* Pour une validation PDF/A-3 complète, utiliser VeraPDF.
*/
function validateEmbeddedXml(string $pdfPath): array
{
$result = [
'fileExists' => false,
'filespecFound' => false,
'streamLength' => 0,
'xmlParsable' => false,
'errors' => [],
];
if (!file_exists($pdfPath)) {
$result['errors'][] = "Fichier introuvable : {$pdfPath}";
return $result;
}
$result['fileExists'] = true;
$content = file_get_contents($pdfPath);
// Vérifier la présence du Filespec factur-x.xml
if (preg_match('/\/F\s*\(factur-x\.xml\)/', $content)) {
$result['filespecFound'] = true;
} else {
$result['errors'][] = 'Filespec factur-x.xml absent du PDF';
return $result;
}
// Extraire la taille déclarée du flux embarqué
if (preg_match(
'/\/Type\s*\/EmbeddedFile.*?\/Length\s+(\d+)/s',
$content,
$matches
)) {
$result['streamLength'] = (int) $matches[1];
}
if ($result['streamLength'] === 0) {
$result['errors'][] = 'Flux du fichier embarqué vide (0 bytes) — le bug est présent';
return $result;
}
return $result;
}
// Utilisation
$diagnostic = validateEmbeddedXml(__DIR__ . '/facture-finale.pdf');
echo "=== Diagnostic PDF Factur-X ===\n";
echo "Fichier présent : " . ($diagnostic['fileExists'] ? 'OUI' : 'NON') . "\n";
echo "Filespec factur-x.xml : " . ($diagnostic['filespecFound'] ? 'OUI' : 'NON') . "\n";
echo "Taille du flux : {$diagnostic['streamLength']} bytes\n";
if (!empty($diagnostic['errors'])) {
echo "\nERREURS :\n";
foreach ($diagnostic['errors'] as $error) {
echo " - {$error}\n";
}
} else {
echo "\nRésultat : flux XML non vide. Valider avec VeraPDF pour conformité complète.\n";
}
Pour une validation complète (PDF/A-3 + XSD + Schematron) :
# Validation PDF/A-3 avec VeraPDF
docker run --rm -v "$PWD":/work verapdf/cli \
--flavour 3b \
--format json \
/work/facture-finale.pdf | jq '.jobs[0].validationResult.isCompliant'
# Validation complète via l'API FacturX (PDF/A-3 + XSD + Schematron)
curl -X POST https://facturxapi.com/api/v1/validate \
-H "X-API-Key: votre-cle-api" \
-F "[email protected]"
Autres pièges PHP courants en PDF/A-3
Le bug du XML 0 bytes n’est pas le seul problème que rencontrent les développeurs PHP avec PDF/A-3. Voici les autres causes fréquentes d’échec.
Profil ICC manquant ou invalide
PDF/A-3 exige un profil colorimétrique ICC dans le dictionnaire OutputIntents quand des espaces colorimétriques non calibrés sont utilisés (DeviceRGB, DeviceGray). mPDF inclut un profil sRGB par défaut quand PDFA => true, mais d’autres bibliothèques ne le font pas.
Symptôme typique avec VeraPDF :
clause 6.2.3 — The document catalog shall include a DestOutputProfile key
En TCPDF, le profil ICC doit être ajouté explicitement :
// TCPDF : ajout du profil ICC
$iccProfile = file_get_contents(__DIR__ . '/sRGB_IEC61966-2-1.icc');
if ($iccProfile === false) {
throw new \RuntimeException('Profil ICC introuvable');
}
$tcpdf->setColorProfile($iccProfile);
Le fichier sRGB_IEC61966-2-1.icc est disponible sur le site de l’International Color Consortium (ICC). Ne pas utiliser un profil CMYK si votre PDF utilise des couleurs RGB.
Métadonnées XMP incomplètes
Les métadonnées XMP doivent contenir à la fois les propriétés PDF/A standard et les propriétés Factur-X dans le namespace fx:. L’oubli le plus fréquent est l’absence de la description d’extension de schéma PDF/A pour le namespace fx:.
Sans cette description, les propriétés fx:DocumentFileName, fx:DocumentType, fx:Version et fx:ConformanceLevel sont présentes dans le XMP mais le fichier n’est pas formellement conforme à PDF/A-3. VeraPDF signale :
clause 6.6.2.4 — Extension schema ... is not defined
La description d’extension doit contenir la déclaration de chaque propriété du namespace fx: avec son type et sa description. Les exemples de référence se trouvent dans le dépôt GitHub officiel de la spécification Factur-X.
pdfaid:part absent ou incorrect
mPDF avec PDFA => true et PDFAauto => true est censé déclarer pdfaid:part = 3 dans les métadonnées XMP. En pratique, certaines versions déclarent pdfaid:part = 1, produisant un PDF identifié comme PDF/A-1 par les validateurs — ce qui interdit les pièces jointes.
Vérification rapide :
<?php
$content = file_get_contents('facture.pdf');
if (preg_match('/pdfaid:part>(\d+)</', $content, $matches)) {
$part = (int) $matches[1];
echo "PDF/A-{$part} déclaré\n";
if ($part !== 3) {
echo "ERREUR: Factur-X nécessite PDF/A-3, pas PDF/A-{$part}\n";
}
} else {
echo "ERREUR: aucune déclaration pdfaid:part trouvée\n";
}
OutputIntent avec le mauvais sous-type
Le dictionnaire OutputIntents doit contenir une entrée avec S égal à GTS_PDFA1. Ce nom est le même pour toutes les versions de PDF/A (1, 2 et 3) — c’est une source de confusion, mais GTS_PDFA1 est bien la valeur correcte même pour PDF/A-3.
Certaines bibliothèques ou configurations Ghostscript écrivent GTS_PDFA (sans le 1) ou GTS_PDFA3, ce qui invalide le document.
Tableau comparatif des approches
| Critère | mPDF (corrigé) | atgp/factur-x | horstoeko/zugferd | FPDI + atgp |
|---|---|---|---|---|
| Génère le PDF visuel | Oui | Non (PDF en entrée) | Non (PDF en entrée) | Non (PDF en entrée) |
| Génère le XML CII | Non | Non | Oui (API builder) | Non |
| Gère PDF/A-3 | Partiel | Oui | Oui | Oui |
| Métadonnées XMP fx: | Non | Oui | Oui | Oui |
| Extension schema XMP | Non | Oui | Oui | Oui |
| Profil ICC | Auto (sRGB) | Auto | Auto | Dépend du PDF source |
| Fiabilité du flux XML | Variable | Fiable | Fiable | Fiable |
| Maintenance active | Oui | Oui | Oui | Oui |
Recommandation : pour un nouveau projet, utiliser mPDF (ou tout autre outil) pour le rendu visuel HTML-vers-PDF, puis atgp/factur-x ou horstoeko/zugferd pour la couche PDF/A-3 Factur-X. Séparer le rendu visuel de la conformité PDF/A-3 évite toute une catégorie de bugs.
En résumé
Le bug du XML 0 bytes dans mPDF peut avoir plusieurs causes : encodage du contenu, version de mPDF, compression qui vide le flux, ou timing incorrect de SetAssociatedFiles() par rapport au cycle de vie du document. La correction directe dans mPDF fonctionne (passer le contenu via la clé content, appeler dans le bon ordre), mais la fiabilité à long terme dépend de la version de mPDF et de sa conformité PDF/A-3 réelle.
L’approche la plus robuste en PHP est de séparer les responsabilités : un outil pour le PDF visuel, un autre pour l’embarquement PDF/A-3 conforme. Les bibliothèques atgp/factur-x et horstoeko/zugferd remplissent ce second rôle de façon fiable.
Pour valider le résultat après correction :
- Vérification rapide : extraire le fichier embarqué et vérifier qu’il n’est pas vide (script PHP ci-dessus ou PyPDF)
- Validation PDF/A-3 : VeraPDF avec
--flavour 3b - Validation Factur-X complète : XSD + Schematron via l’API FacturX ou un validateur local
Ressources
- PDF/A-3 pour Factur-X : checklist de conformité et pièges courants — les exigences ISO 19005-3 détaillées
- Valider EN16931/Factur-X : XSD vs Schematron, erreurs BR-* — la validation XML une fois le PDF/A-3 conforme
- Facturation électronique 2026 : le guide technique complet — contexte de la réforme et pipeline complet
- Déployer un validateur : KoSIT vs API SaaS — comparaison des options de validation