Aller au contenu

Writer : le sérialiseur PDF 2.0 + xref

Le module Writer sérialise un document sous forme d’octets PDF. Il choisit une stratégie de version, écrit le graphe d’objets, puis émet la structure de références croisées et le trailer.

Fenêtre de terminal
composer require nextpdf/core:^3

PdfWriter est le point d’entrée. La méthode write() accepte un objet-valeur DocumentData. Elle renvoie le PDF complet sous forme de chaîne d’octets. Le writer assemble le graphe d’objets, attribue les numéros d’objet, enregistre les décalages en octets, puis écrit la structure de références croisées à la fin.

Le writer utilise une seule stratégie de sérialisation par appel. L’interface PdfSerializationStrategy définit quatre méthodes : writeHeader(), getCatalogVersion(), writeXrefAndTrailer() et usesXrefStream(). Trois stratégies l’implémentent. Pdf20StreamStrategy écrit l’en-tête %PDF-2.0, définit la version du catalogue à /2.0 et émet un stream de références croisées. Pdf17TableStrategy écrit %PDF-1.7 et une table de références croisées classique. Pdf14TableStrategy écrit %PDF-1.4 et une table de références croisées. PdfWriter choisit la stratégie avec un match sur DocumentData::$outputProfile. La stratégie par défaut est Pdf20StreamStrategy.

L’énumération PdfOutputProfile regroupe les trois versions cibles : Pdf20, Pdf17 et Pdf14. L’énumération expose headerVersion(), catalogVersion(), allowsObjectStreams() et usesXrefStream(). Un mode de conformité archivistique prend le pas sur le profil choisi avant la sélection de la stratégie. Pdf14FeatureGuard rejette les fonctionnalités PDF 2.0 quand le profil est Pdf14.

Un stream de références croisées associe chaque numéro d’objet à son décalage en octets — ISO 32000-2 §7. Les mises à jour incrémentales ajoutent les nouveaux objets à la fin du fichier — ISO 32000-2 §7.5.6. Le writer échappe chaque chaîne littérale via le point d’intégration canonique PdfStringEscaper::escapeLiteral(), qui suit la table d’échappement normative de l’ISO 32000-2 §7.3.4.2 (ADR-015).

Le writer prend en charge une sortie déterministe. setDeterministicMode() fige les identifiants d’objet et l’ordre des clés de dictionnaire. setReproducibleClock() fige l’horodatage du document. Une fois les deux figés, une entrée fixe produit une sortie strictement identique octet par octet. La méthode writeChunked() renvoie un générateur qui produit le PDF par blocs de taille fixe. Streaming/StreamingPdfWriter écrit une page à la fois dans un flux fourni par l’appelant, pour les documents qui dépassent le budget mémoire.

Linearizer réécrit un PDF terminé dans une disposition linéarisée. Il place la première page suffisamment tôt pour qu’un lecteur puisse l’afficher avant la fin du téléchargement complet. shadowValidate() vérifie la réécriture sans modifier l’entrée.

Attention. PdfWriter.php et Linearizer.php sont critiques pour les décalages d’octets et le graphe d’objets (zones de danger du manifeste). Ne modifie pas la numérotation des objets ni l’arithmétique des décalages xref sans exécuter la golden suite du Writer.

ClasseMéthodes clésRôle
PdfWriterwrite(DocumentData): string, writeChunked(DocumentData, int): Generator, setDeterministicMode(), setReproducibleClock(), setOutputColorProfile(), getLastXrefOffset(), getFileId()Sérialiseur principal
PdfSerializationStrategy (interface)writeHeader(), getCatalogVersion(), writeXrefAndTrailer(), usesXrefStream()Contrat de stratégie de version
Pdf20StreamStrategywriteHeader()%PDF-2.0, getCatalogVersion()/2.0, usesXrefStream()trueStratégie xref-stream PDF 2.0
Pdf17TableStrategywriteHeader()%PDF-1.7, table xrefStratégie xref-table PDF 1.7
Pdf14TableStrategywriteHeader()%PDF-1.4, table xrefStratégie xref-table PDF 1.4
PdfOutputProfile (énumération)Pdf20, Pdf17, Pdf14 ; headerVersion(), catalogVersion(), allowsObjectStreams()Sélecteur de version cible
PdfXrefWritergenerateFileId(), finalizeTrailerAndXref()ID de fichier + finalisation trailer/xref
Linearizerlinearize(string): string, shadowValidate(string): arrayRéécriture pour affichage web rapide
Streaming\StreamingPdfWriteropen(), newPage(), close()Writer streaming en une seule passe

Exécute composer docs:generate-api-php -- --module=Writer pour obtenir le tableau PHPDoc complet.

Source : examples/02-pdf-factory.php.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Writer\PdfWriter;
$writer = new PdfWriter();
$pdfBytes = $writer->write($documentData);
file_put_contents('out.pdf', $pdfBytes);

Le profil par défaut est PDF 2.0. La sortie commence par %PDF-2.0 et se termine par un stream de références croisées.

Cet exemple fige le mode déterministe et l’horloge sur une valeur fixe, pour une sortie strictement identique octet par octet, puis diffuse en blocs de taille fixe.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use DateTimeImmutable;
use NextPDF\Writer\PdfWriter;
use NextPDF\Writer\ReproducibleClock;
$pinned = new DateTimeImmutable('2026-01-01T00:00:00Z');
$writer = new PdfWriter();
$writer->setDeterministicMode($pinned, 'nextpdf-fixed-file-id');
$writer->setReproducibleClock(new ReproducibleClock($pinned));
$out = fopen('php://output', 'wb');
foreach ($writer->writeChunked($documentData, chunkSize: 65536) as $chunk) {
fwrite($out, $chunk);
}
fclose($out);
  • Une seule stratégie est exécutée par appel à write(). Le writer réinitialise la stratégie à partir du profil à chaque appel. Un appel précédent ne propage pas sa version.
  • Un mode de conformité archivistique remplace le profil demandé. Une génération PDF/A-3 force le PDF 1.7. Une génération PDF/A-4 force le PDF 2.0.
  • Une sortie strictement identique octet par octet exige les deux verrous. Active le mode déterministe et une horloge reproductible. Un seul verrou ne suffit pas.
  • writeChunked() produit un générateur. Consomme-le entièrement. Une lecture partielle produit un PDF tronqué et invalide.
  • Linearizer réécrit les décalages de références croisées. Exécute d’abord shadowValidate() dans tout pipeline qui ne tolère pas l’échec d’une réécriture.
  • Pdf14TableStrategy est final readonly. Le chemin PDF 1.4 rejette les fonctionnalités PDF 2.0 via Pdf14FeatureGuard au lieu de les dégrader.

La sérialisation est linéaire par rapport au nombre d’objets et à la taille totale en octets. Le stream de références croisées ajoute une passe sur la table des objets. writeChunked() garde en mémoire le document assemblé, mais le produit par tranches bornées ; le pic mémoire correspond donc à la taille du document plus un bloc. Streaming\StreamingPdfWriter ne conserve pas le document entier — c’est le chemin pour les entrées plus grandes que le budget mémoire. Pour la charge de travail de référence, le budget est de 1500 ms en temps réel et 64 Mo en pic. La linéarisation ajoute une seconde passe complète et une passe de mesure. Prévois-la explicitement dans ton budget.

Le writer sérialise un graphe d’objets de confiance déjà en mémoire. Les principales menaces viennent de ses entrées. Chaque chaîne littérale passe par le point canonique PdfStringEscaper::escapeLiteral() (ADR-015), si bien que des octets de contrôle intégrés ne peuvent pas s’échapper d’un token de chaîne. Le chiffrement est câblé via PdfEncryptionWriter et l’entrée de trailer /Encrypt. Le chiffrement à clé publique est rejeté par une exception explicite, plutôt que d’être rétrogradé silencieusement. Les modes déterministe et horloge reproductible retirent de la sortie les canaux auxiliaires d’horodatage et d’ordonnancement. Consulte /modules/core/security/ pour le modèle de menace du document et la frontière de confiance du chiffrement.

Le Writer produit des structures de fichier PDF 2.0 : l’en-tête %PDF-2.0, une version de catalogue /2.0, un stream de références croisées et l’échappement des chaînes littérales selon la table d’échappement de l’ISO 32000-2 §7.3.4.2. Ce sont des faits d’implémentation. La preuve se trouve dans src/Writer/Pdf20StreamStrategy.php, src/Writer/PdfSerializationStrategy.php et la sélection de stratégie dans src/Writer/PdfWriter.php, exercée par tests/Unit/Writer/ (192 tests, dont les suites Pdf20StreamStrategy, PdfXrefWriter et Linearizer*) ainsi que dans la base de référence tests/Golden/PdfWriter/PdfWriterGoldenBaselineSmokeTest.

Ce n’est pas une revendication de conformité PDF 2.0 complète. La conformité ISO 32000-2 complète est une propriété d’un document complet validé par un oracle externe, et non du sérialiseur seul. La conformité de bout en bout n’est affirmée que là où un oracle la confirme : tests/Integration/Accessibility/VeraPdfUa2GoldenTest valide une fixture générée avec veraPDF pour PDF/UA-2, et tests/Standards/Profile/PdfRConformanceTest couvre le profil PDF/R. Le golden test veraPDF est ignoré lorsque le binaire veraPDF est absent du runner : c’est donc une barrière d’oracle optionnelle, pas une barrière inconditionnelle. Définis VERAPDF_BINARY pour l’exécuter. La sélection du profil archivistique (PDF/A-3 → PDF 1.7, PDF/A-4 → PDF 2.0) est décidée par l’ADR-011 et le mode de conformité, et validée par les suites de conformité dans /modules/core/conformance/. En dehors de ces profils adossés à un oracle, indique que le Writer « produit des structures PDF 2.0 ; la conformité est validée par veraPDF pour le profil PDF/UA-2 » plutôt que d’affirmer une conformité sans réserve.