PDF/A-3 est la norme ISO 19005-3 et c'est le conteneur exigé par la spécification Factur-X pour embarquer un fichier XML de facture dans un PDF. Avant d'aller plus loin : produire un PDF/A-3 réellement conforme en PHP est plus difficile qu'il n'y paraît. La majorité des bibliothèques PHP ne le font pas nativement. Cet article l'explique sans détour.

🔎 Si vous cherchez à intégrer Factur-X de bout en bout (génération de l'XML + embarquement), consultez le testeur /sdk/FactureX/ qui valide ces étapes.


Pourquoi PDF/A-3 et pas un PDF ordinaire ?

PDF/A est une famille de profils ISO conçue pour l'archivage long terme. Chaque version de la norme ajoute des capacités :

Norme Fichiers joints autorisés
PDF/A-1 (ISO 19005-1) Non
PDF/A-2 (ISO 19005-2) Oui (mais usage limité)
PDF/A-3 (ISO 19005-3) Oui, tout type de fichier

C'est cette dernière caractéristique qui rend PDF/A-3 obligatoire pour Factur-X : la norme exige que le fichier factur-x.xml (ou zugferd-invoice.xml) soit une pièce jointe embedded avec une relation AF (Associated File). PDF/A-1 et la plupart des PDF classiques ne le permettent pas de manière conforme.

Autrement dit : un PDF Factur-X sans le profil PDF/A-3 n'est pas un document Factur-X valide.


Les exigences d'un PDF/A-3

La norme impose plusieurs contraintes cumulatives.

OutputIntent avec profil colorimétrique ICC

Le fichier PDF doit déclarer un OutputIntent pointant vers un profil ICC valide (typiquement sRGB.icc). Sans cela, les couleurs ne sont pas définies de manière indépendante du périphérique, ce qui viole la norme.

Métadonnées XMP obligatoires

Le document doit embarquer un flux XMP incluant au minimum :

  • l'identifiant PDF/A : pdfaid:part = 3
  • le niveau de conformance : pdfaid:conformance = B (Basic) ou U (Unicode)
  • les métadonnées Dublin Core standard (titre, auteur, date…)

Ces métadonnées doivent être un flux XMP valide dans le dictionnaire Metadata du catalogue PDF, pas seulement dans les Info du document.

Polices intégrées

Toutes les polices utilisées doivent être intégralement embarquées. Les polices référencées par nom mais non embarquées rendent le document non conforme.

Contenus interdits

PDF/A-3 interdit explicitement : JavaScript embarqué, chiffrement (pas de mot de passe), références externes non résolues, transparence non aplatie (selon le niveau), et certaines actions.


Les voies disponibles en PHP

Voie 1 — Post-traitement par Ghostscript (la plus fiable)

Ghostscript est un interpréteur PostScript/PDF open source qui dispose d'une option -dPDFA permettant de convertir un PDF existant vers PDF/A. C'est actuellement la voie la plus documentée pour obtenir un PDF/A-3 réellement conforme depuis PHP.

Principe : PHP génère d'abord un PDF « ordinaire » avec la bibliothèque de son choix, puis appelle Ghostscript en sous-processus pour le convertir.

Fichier de définition PDFA_def.ps

Ghostscript a besoin d'un fichier PostScript de définition pour connaître le profil ICC et les métadonnées de base. Exemple minimal (à adapter) :

%!
% PDFA_def.ps — définition pour conversion PDF/A-3
% Adapter le chemin vers votre profil ICC sRGB

[ /Title (Ma Facture)
  /Author (Mon Entreprise)
  /Subject (Facture Factur-X)
  /DOCINFO pdfmark

[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
[{icc_PDFA}
<<
  /N 3
>>
/PUT pdfmark
[{icc_PDFA} (/usr/share/ghostscript/iccprofiles/default_rgb.icc) (r) file /PUT pdfmark

[/_objdef {OutputIntent_PDFA} /type /dict /OBJ pdfmark
[{OutputIntent_PDFA} <<
  /Type /OutputIntent
  /S /GTS_PDFA1
  /DestOutputProfile {icc_PDFA}
  /OutputConditionIdentifier (sRGB)
>> /PUT pdfmark

[ {Catalog} << /OutputIntents [ {OutputIntent_PDFA} ] >> /PUT pdfmark

Le chemin vers le profil ICC (default_rgb.icc) varie selon votre installation Ghostscript et votre OS. Vérifiez avec find /usr -name "*.icc" 2>/dev/null.

Appel depuis PHP

<?php

function convertToPdfA3(string $inputPdf, string $outputPdf, string $pdfaDefPs): bool
{
    if (!file_exists($inputPdf)) {
        throw new \InvalidArgumentException("Fichier source introuvable : $inputPdf");
    }
    if (!file_exists($pdfaDefPs)) {
        throw new \InvalidArgumentException("Fichier de définition PDFA introuvable : $pdfaDefPs");
    }

    $command = sprintf(
        'gs -dBATCH -dNOPAUSE -dNOOUTERSAVE'
        . ' -dPDFA=3'
        . ' -dPDFACompatibilityPolicy=1'
        . ' -sColorConversionStrategy=UseDeviceIndependentColor'
        . ' -sDEVICE=pdfwrite'
        . ' -sOutputFile=%s'
        . ' %s'
        . ' %s'
        . ' 2>&1',
        escapeshellarg($outputPdf),
        escapeshellarg($pdfaDefPs),
        escapeshellarg($inputPdf)
    );

    exec($command, $output, $exitCode);

    if ($exitCode !== 0) {
        throw new \RuntimeException(
            "Ghostscript a échoué (code $exitCode) :\n" . implode("\n", $output)
        );
    }

    return file_exists($outputPdf);
}

// Usage
convertToPdfA3('/tmp/facture_brute.pdf', '/tmp/facture_pdfa3.pdf', '/var/app/pdfa/PDFA_def.ps');

Avertissements importants :

  • Les options exactes de Ghostscript et leur comportement varient selon la version (la syntaxe ci-dessus vise Ghostscript 9.x/10.x mais peut évoluer).
  • exec() doit être disponible (souvent désactivé en hébergement mutualisé).
  • La conversion ne garantit pas la conformité si le PDF source contient des éléments non convertibles. Ghostscript retourne parfois 0 malgré des avertissements.
  • Valider le résultat avec veraPDF est indispensable.

Voie 2 — mPDF avec son option PDFA

mPDF propose une option PDFA qui tente de produire un PDF compatible PDF/A. Il faut être honnête sur ses limites.

<?php

require_once __DIR__ . '/vendor/autoload.php';

$mpdf = new \Mpdf\Mpdf([
    'PDFA'      => true,
    'PDFAauto'  => true,
    'fontDir'   => [__DIR__ . '/fonts/'],
    'tempDir'   => '/tmp/mpdf',
]);

$mpdf->SetTitle('Ma Facture');
$mpdf->WriteHTML('<h1>Facture N° 2026-001</h1>');
$mpdf->Output('/tmp/facture_mpdf.pdf', 'F');

Limites à connaître :

  • mPDF vise plutôt PDF/A-1b, pas nécessairement PDF/A-3. Le niveau réellement atteint dépend de la version et de la configuration.
  • L'embarquement d'un fichier joint avec la relation AF requise par Factur-X n'est pas une fonctionnalité standard de mPDF.
  • Validez toujours avec veraPDF.

Voie 3 — TCPDF, FPDF, Dompdf

Ces bibliothèques n'ont pas de support natif pour un PDF/A conforme : FPDF (aucune gestion PDF/A), TCPDF (mécanismes partiels, pas de PDF/A-3 conforme sans travail important), Dompdf (pas de support). Une voie HTML → PDF puis conversion Ghostscript reste possible avec les mêmes précautions.


Voie 4 — Déléguer à une librairie Factur-X

Pour produire une Factur-X complète (PDF/A-3 + XML embarqué + XMP), la voie la plus pragmatique est une bibliothèque qui gère tout le pipeline. Voir :


Valider le résultat

Produire le fichier ne suffit pas. Un PDF/A-3 doit être validé par un outil qui implémente ISO 19005-3.

veraPDF — le validateur de référence

veraPDF est le validateur PDF/A open source de référence, soutenu par la PDF Association. Disponible en ligne de commande :

# Validation d'un fichier
verapdf --flavour 3b votre_facture.pdf

# Avec rapport détaillé en XML
verapdf --flavour 3b --format xml votre_facture.pdf > rapport_pdfa3.xml

L'option --flavour 3b correspond à PDF/A-3 niveau B (Basic) ; 3u pour le niveau U (Unicode). Une sortie conforme affiche PASS ; tout FAIL indique une violation à corriger.

veraPDF valide la conformance PDF/A mais pas la sémantique Factur-X (XML, profil EN 16931). Pour valider l'ensemble, combinez veraPDF et un validateur Factur-X.


FAQ

Un PDF généré par FPDF/TCPDF peut-il passer veraPDF ? Dans la grande majorité des cas, non, sans post-traitement : il manque au minimum les métadonnées XMP correctement structurées et souvent l'OutputIntent ICC.

Ghostscript est-il disponible sur les hébergements mutualisés ? Rarement. Sur un VPS ou serveur dédié, installez-le (apt install ghostscript). En mutualisé, une API tierce est souvent la seule option.

PDF/A-3 niveau B ou U : quelle différence ? Le niveau B garantit la reproduction visuelle fiable. Le niveau U ajoute que tout le texte est mappé sur des caractères Unicode. Pour Factur-X, le niveau B suffit, mais U est préférable pour l'extraction de texte.

Peut-on générer un PDF/A-3 entièrement en PHP sans Ghostscript ? Techniquement oui, en manipulant les structures PDF bas niveau (XMP, OutputIntent, fichiers avec AF) — c'est ce que font les bibliothèques Factur-X. Le réimplémenter from scratch est long et sujet à erreurs.


Pour aller plus loin