---
title: Construire une Factur-X sans bibliothèque (en PHP)
source: https://synapx.fr/blog/construire-factur-x-sans-bibliotheque-php/
date: 2026-06-27
category: Facturation électronique
site: SynapxLab
---

# Construire une Factur-X sans bibliothèque (en PHP)

> **Avertissement préliminaire.** Cet article est pédagogique, pas une recette de production. Assembler un PDF/A-3 strictement conforme « from scratch » est un travail de bas niveau non trivial — profil ICC, marquage PDF/A, structure XMP, calcul d'offset xref — que les bibliothèques spécialisées résolvent pour vous. Vous trouverez ici une compréhension des rouages, pas un générateur prêt à l'emploi. Pour la production, allez directement à la [version avec bibliothèque](/blog/generer-factur-x-php/).

---

Une Factur-X est une facture PDF ordinaire en apparence, mais qui est en réalité un **PDF/A-3** (ISO 19005-3) contenant un fichier XML embarqué conforme à la norme EN 16931 (syntaxe CII). C'est ce qu'on appelle le format hybride : l'humain lit le PDF, la machine parse le XML. La spécification officielle est publiée par le FNFE-MPE ; sa lecture directe est recommandée avant tout développement sérieux.

Décomposons les trois briques indépendantes à assembler.

---

## 1. La brique XML : le fichier CII conforme EN 16931

### Pourquoi CII et pas UBL ?

Factur-X (côté français) et ZUGFeRD (côté allemand) utilisent la **Cross-Industry Invoice (CII)** de l'UN/CEFACT, pas l'UBL de l'OASIS. Les deux sont des syntaxes valides pour EN 16931, mais elles ne sont pas interchangeables.

### Les namespaces réels

```xml
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"
```

Ces URN ne sont pas des URL accessibles — ce sont des identificateurs stables définis par l'UN/CEFACT. Ne cherchez pas à les résoudre via HTTP.

### Squelette MINIMUM (le profil le plus restrictif)

Le profil **MINIMUM** n'exige que les champs strictement obligatoires de la règle métier EN 16931. Il est utile pour les flux B2B simples ou pour commencer à apprendre.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
    xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
    xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
    xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">

  <!-- === En-tête : profil et type de document === -->
  <rsm:ExchangedDocumentContext>
    <ram:GuidelineSpecifiedDocumentContextParameter>
      <ram:ID>urn:factur-x.eu:1p0:minimum</ram:ID>
    </ram:GuidelineSpecifiedDocumentContextParameter>
  </rsm:ExchangedDocumentContext>

  <!-- === Document : numéro, type, date === -->
  <rsm:ExchangedDocument>
    <ram:ID>FA-2026-00042</ram:ID>
    <!-- 380 = facture commerciale (code UNTDID 1001) -->
    <ram:TypeCode>380</ram:TypeCode>
    <!-- Format de date : 102 = AAAAMMJJ -->
    <ram:IssueDateTime>
      <udt:DateTimeString format="102">20260627</udt:DateTimeString>
    </ram:IssueDateTime>
  </rsm:ExchangedDocument>

  <!-- === Transaction commerciale === -->
  <rsm:SupplyChainTradeTransaction>

    <!-- Vendeur (BT-27 à BT-37) -->
    <ram:ApplicableHeaderTradeAgreement>
      <ram:SellerTradeParty>
        <ram:Name>Synapx SAS</ram:Name>
        <ram:SpecifiedTaxRegistration>
          <!-- BT-31 : numéro de TVA -->
          <ram:ID schemeID="VA">FR00123456789</ram:ID>
        </ram:SpecifiedTaxRegistration>
      </ram:SellerTradeParty>

      <!-- Acheteur (BT-44) -->
      <ram:BuyerTradeParty>
        <ram:Name>Client ACME</ram:Name>
      </ram:BuyerTradeParty>
    </ram:ApplicableHeaderTradeAgreement>

    <!-- Livraison : obligatoire même en MINIMUM -->
    <ram:ApplicableHeaderTradeDelivery/>

    <!-- Règlement : montants HT, TVA, TTC -->
    <ram:ApplicableHeaderTradeSettlement>
      <ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
      <ram:SpecifiedTradeSettlementHeaderMonetarySummation>
        <!-- BT-109 : total HT -->
        <ram:TaxBasisTotalAmount>1000.00</ram:TaxBasisTotalAmount>
        <!-- BT-110 : total TVA -->
        <ram:TaxTotalAmount currencyID="EUR">200.00</ram:TaxTotalAmount>
        <!-- BT-112 : total TTC -->
        <ram:GrandTotalAmount>1200.00</ram:GrandTotalAmount>
        <!-- BT-115 : montant à payer -->
        <ram:DuePayableAmount>1200.00</ram:DuePayableAmount>
      </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
    </ram:ApplicableHeaderTradeSettlement>

  </rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>
```

> **Note sur le format de date `102`.** Le code `102` est un code UNTDID 2379, pas un format PHP. Il signifie que la chaîne est en `AAAAMMJJ`. N'écrivez jamais `format="Y-m-d"`.

### Validation du XML seul

Avant d'aller plus loin, validez le XML contre les schémas XSD fournis dans la spec FNFE-MPE **et** contre les règles métier Schematron (les XSD seuls ne couvrent pas toutes les contraintes EN 16931).

```php
<?php
$xml = new DOMDocument();
$xml->load('factur-x.xml');

if (!$xml->schemaValidate('CrossIndustryInvoice_100pD22B.xsd')) {
    foreach (libxml_get_errors() as $error) {
        echo $error->message;
    }
}
```

> 🔎 Pour valider votre XML directement en ligne avant d'aller plus loin : [/sdk/FactureX/](/sdk/FactureX/)

---

## 2. La brique PDF/A-3 : pourquoi FPDF ne suffit pas

### Ce que PDF/A-3 exige (ISO 19005-3)

Un PDF/A-3 n'est pas un simple PDF avec un XML dedans. La norme impose :

| Exigence | Ce que ça implique concrètement |
|---|---|
| **Profil ICC embarqué** | Un flux ColorSpace avec un profil ICC (sRGB, CMYK…) doit être inclus. |
| **Bloc XMP de conformité** | Les métadonnées `pdfaid:part = 3` et `pdfaid:conformance = B` (ou U) doivent être présentes dans le flux XMP du catalogue. |
| **Pas de chiffrement** | Un PDF/A-3 chiffré est invalide. |
| **Polices embarquées** | Toutes les polices utilisées doivent être sous-settées dans le fichier. |
| **Marquage `/Marked true`** | Le catalogue doit déclarer `MarkInfo` avec `Marked true`. |
| **Fichiers attachés via `/AF`** | L'association du XML se fait via `/AF` (Associated Files), pas via une simple annotation. |

**FPDF** produit du PDF 1.x standard. Il ne gère ni le profil ICC, ni le bloc XMP `pdfaid`, ni le marquage `/Marked`, ni `/AF`. Il serait techniquement possible de patcher sa sortie binaire, mais c'est le chemin de la douleur.

### Les voies réalistes

**Option A — Post-traitement Ghostscript.** Vous générez un PDF lisible (FPDF, TCPDF, mPDF…) puis vous le convertissez :

```bash
gs \
  -dPDFA=3 \
  -dBATCH \
  -dNOPAUSE \
  -sColorConversionStrategy=RGB \
  -sDEVICE=pdfwrite \
  -dPDFACompatibilityPolicy=1 \
  -sOutputFile=output_pdfa3.pdf \
  PDFA_def.ps \
  input.pdf
```

Le fichier `PDFA_def.ps` est fourni avec Ghostscript et doit être adapté (profil ICC, titre). Cette approche fonctionne mais exige que Ghostscript soit installé et peut silencieusement « corriger » des éléments du PDF original. Voir [Générer un PDF/A-3 en PHP](/blog/generer-pdf-a-3-php/) pour une analyse approfondie.

**Option B — Bibliothèque PHP dédiée.** La voie raisonnable pour la production : [/blog/generer-factur-x-php/](/blog/generer-factur-x-php/).

**Option C — Service externe.** LibreOffice `--headless`, wkhtmltopdf avec post-traitement, ou une API tierce.

---

## 3. La brique embarquement : XML + métadonnées XMP

C'est ici que réside la spécificité Factur-X par rapport à un simple PDF/A-3. Deux éléments sont obligatoires.

### 3a. La structure d'embarquement dans le PDF

Un PDF est un graphe d'objets numérotés. Pour embarquer un fichier, vous avez besoin d'au moins trois objets supplémentaires.

**L'objet FileSpec (décrit le fichier attaché) :**

```
12 0 obj
<<
  /Type /Filespec
  /F (factur-x.xml)
  /UF (factur-x.xml)
  /Desc (Factur-X XML Invoice)
  /AFRelationship /Data
  /EF << /F 13 0 R /UF 13 0 R >>
>>
endobj
```

> `/AFRelationship /Data` est la valeur requise par la spec Factur-X (le XML **est** la donnée de la facture, pas un document alternatif).

**L'objet EmbeddedFile (le contenu binaire du XML) :**

```
13 0 obj
<<
  /Type /EmbeddedFile
  /Subtype /text#2Fxml
  /Params <<
    /Size 1842
    /ModDate (D:20260627120000+02'00')
  >>
  /Length 1842
>>
stream
...contenu XML ici...
endstream
endobj
```

**Déclaration dans le catalogue (objet racine du PDF) :**

```
1 0 obj  % Catalogue (objet racine)
<<
  /Type /Catalog
  /Pages 2 0 R
  /Names <<
    /EmbeddedFiles <<
      /Names [(factur-x.xml) 12 0 R]
    >>
  >>
  /AF [12 0 R]   % Associated Files : clé spécifique PDF/A-3
>>
endobj
```

La clé `/AF` (Associated Files) est définie dans la norme PDF 2.0 et reprise par ISO 19005-3. Sans elle, le fichier est attaché au PDF mais pas **associé** — les validateurs Factur-X le rejetteront.

### 3b. Le bloc XMP Factur-X

Le catalogue contient un flux XMP (objet `/Metadata`). Ce flux doit contenir, en plus du bloc standard PDF/A, une **extension de schéma** déclarant les métadonnées Factur-X :

```xml
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">

    <!-- Conformité PDF/A -->
    <rdf:Description rdf:about=""
        xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
      <pdfaid:part>3</pdfaid:part>
      <pdfaid:conformance>B</pdfaid:conformance>
    </rdf:Description>

    <!-- Valeurs Factur-X pour CE document -->
    <rdf:Description rdf:about=""
        xmlns:fx="urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#">
      <fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>
      <fx:DocumentType>INVOICE</fx:DocumentType>
      <fx:Version>1.0</fx:Version>
      <fx:ConformanceLevel>MINIMUM</fx:ConformanceLevel>
    </rdf:Description>

  </rdf:RDF>
</x:xmpmeta>
```

> **Attention.** La valeur de `fx:ConformanceLevel` doit correspondre exactement au profil déclaré dans le XML CII (`urn:factur-x.eu:1p0:minimum` → `MINIMUM`). Une incohérence entre les deux est l'une des erreurs les plus fréquentes lors de la validation. La déclaration complète inclut aussi une `pdfaExtension:schemas` décrivant les propriétés `fx:` — voir la spec FNFE-MPE pour le bloc exhaustif.

### 3c. Injecter tout ça dans un PDF existant

Si vous partez d'un PDF déjà généré et voulez ajouter l'XML et les métadonnées **sans ré-encoder l'intégralité du PDF**, l'approche consiste à : lire le PDF comme un flux binaire, localiser la table `xref` et l'objet catalogue, ajouter les nouveaux objets à la fin, puis écrire une **cross-reference table incrémentale** et un nouveau `trailer`.

```php
<?php
// Principe — ne pas utiliser tel quel en production
function appendObjectToPdf(string $pdfContent, string $newObjectData): string
{
    $startxrefPos = strrpos($pdfContent, 'startxref');
    $oldStartxref = (int) trim(substr($pdfContent, $startxrefPos + 9, 20));

    preg_match_all('/^(\d+) 0 obj/m', $pdfContent, $matches);
    $nextObjNum = max(array_map('intval', $matches[1])) + 1;

    $newObjOffset = strlen($pdfContent);
    $objContent = "$nextObjNum 0 obj\n$newObjectData\nendobj\n";

    $xref  = "xref\n$nextObjNum 1\n";
    $xref .= str_pad($newObjOffset, 10, '0', STR_PAD_LEFT) . " 00000 n \n";

    $trailer  = "trailer\n<< /Size " . ($nextObjNum + 1) . " /Prev $oldStartxref >>\n";
    $trailer .= "startxref\n" . ($newObjOffset + strlen($objContent)) . "\n%%EOF\n";

    return $pdfContent . $objContent . $xref . $trailer;
}
```

Ce code illustre la mécanique, pas une implémentation sûre. En pratique, modifier un PDF binaire sans parser complet risque de casser la table d'objets — surtout si le PDF source utilise la compression `xref stream` (PDF 1.5+).

---

## 4. Pourquoi c'est difficile en pratique

| Difficulté | Pourquoi c'est piégeux |
|---|---|
| PDF/A-3 natif | Très peu de libs PHP génèrent du vrai PDF/A-3 sans post-traitement externe. |
| Profil ICC | L'intégrer correctement requiert de manipuler des streams binaires. |
| XMP exact | Une seule balise mal fermée ou un namespace manquant invalide la conformité. |
| Table xref incrémentale | Décaler d'un octet casse tout ; aucune erreur visible à l'œil. |
| Validation Schematron | La XSD seule ne valide pas les règles métier EN 16931 (ex. cohérence TVA). |

---

## Conclusion

Vous savez maintenant ce qu'une bibliothèque comme `atgp/factur-x` fait à votre place : conversion PDF/A-3, injection binaire des objets d'embarquement, génération du bloc XMP, cohérence entre le profil CII et les métadonnées. Ce n'est pas de la magie — c'est du travail de bas niveau que vous venez de parcourir.

**Pour la production**, utilisez une bibliothèque éprouvée et validée. Le « sans bibliothèque » vous a donné le vocabulaire pour lire un ticket d'erreur de validateur, comprendre ce qu'un outil fait (ou ne fait pas), et déboguer intelligemment.

- Version production avec librairie : [/blog/generer-factur-x-php/](/blog/generer-factur-x-php/)
- Générer le conteneur : [/blog/generer-pdf-a-3-php/](/blog/generer-pdf-a-3-php/)
- Valider votre résultat : [/sdk/FactureX/](/sdk/FactureX/)
- Comprendre la norme : [/blog/factur-x-comprendre-valider/](/blog/factur-x-comprendre-valider/)

---

## FAQ

**Le profil MINIMUM est-il accepté par l'administration fiscale française ?**
Factur-X MINIMUM est valide au sens d'EN 16931. Pour la e-facturation obligatoire, vérifiez les exigences de la plateforme agréée que vous utilisez — certaines exigent au minimum le profil BASIC WL.

**Puis-je valider le PDF/A-3 sans outil commercial ?**
Oui : `veraPDF` est le validateur de référence open source pour PDF/A.

**ZUGFeRD et Factur-X utilisent-ils le même XML ?**
Les deux utilisent la syntaxe CII et les mêmes namespaces UN/CEFACT. L'identifiant de profil dans `<ram:ID>` diffère, ainsi que certains détails XMP. Les fichiers ne sont pas directement interchangeables sans ajustement.

**La norme PDF/A-3 autorise-t-elle n'importe quel format de fichier embarqué ?**
Oui, contrairement à PDF/A-2. PDF/A-3 (ISO 19005-3) lève cette restriction — ce qui rend Factur-X possible.

---

## Pour aller plus loin

- **Spec officielle Factur-X** — FNFE-MPE, [fnfe-mpe.org](https://fnfe-mpe.org) (document normatif, gratuit)
- **ISO 19005-3** — norme PDF/A-3 (accès via ISO/AFNOR)
- **veraPDF** — validateur PDF/A open source
- **UN/CEFACT CII** — schémas XSD et documentation (unece.org)
- [Générer un PDF/A-3 en PHP](/blog/generer-pdf-a-3-php/)
- [Factur-X : comprendre et valider](/blog/factur-x-comprendre-valider/)
- [Générer une Factur-X avec une librairie](/blog/generer-factur-x-php/)
