Le script complet construit une page avec une structure logique indépendante de son dessin. Il combine des rôles PDF 2.0, les types de table des matières propres à PDF 1.7 et un paragraphe personnalisé. Une police incorporée, des liens ParentTree réciproques et des métadonnées XMP rendent ces associations inspectables dans le fichier réel.
L’aperçu présente cinq lignes. Leur position ne crée pas leur sens : le script construit explicitement les éléments H1, P, TOC, TOCI et Reference qui permettent de les interpréter.
Exécuter l’exemple
Depuis un checkout du core 0.7.1 avec ses dépendances de développement verrouillées :
php examples/pdf20-tagged-namespaces.php namespaces.pdf --audit-json
Le premier argument désigne le PDF de sortie. --audit-json écrit un rapport séparé sur la sortie d’erreur ; il ne mélange pas du JSON aux octets du PDF. Les options --xref-stream et --hybrid choisissent respectivement un flux xref et une section hybride. Sans ces options, le writer produit une table xref classique.
Le module optionnel pdftoolkit/pdf-fonts lit la police Noto Sans de démonstration. Le core reçoit ses objets PDF normalisés : programme incorporé, descripteur, métriques CID, correspondance CID/GID et CMap ToUnicode. Installer uniquement le core n’installe pas ce convertisseur.
Qualifier le sens par un namespace
Dans PDF 2.0, le nom d’un type ne suffit pas toujours à exprimer sa sémantique. StructureRole associe ce nom à un objet StructureNamespace :
$pdf20 = new StructureNamespace(StandardStructureNamespace::PDF20);
$pdf17 = new StructureNamespace(StandardStructureNamespace::PDF17);
$document = new StructureElement(
new StructureRole(new NameType('Document'), $pdf20),
);
Chaque élément écrit /S pour son type et une référence indirecte /NS vers le dictionnaire de namespace. StructureNamespaces fournit au StructureTreeRoot les instances canoniques utilisées dans ce document. Deux objets portant le même identifiant de namespace ne sont pas interchangeables dans ce modèle : un jeu de namespaces possède une seule instance pour chaque identifiant.
Les types communs à PDF 1.7 et PDF 2.0 utilisent le namespace PDF 2.0 dans un arbre mixte. Certains types, dont TOC, TOCI et Reference, restent définis dans le namespace PDF 1.7. ISO/TS 32005 précise leurs relations de parenté avec les autres éléments. Le script conserve donc leurs namespaces distincts au lieu de renommer tous les types.
Définir un rôle personnalisé sans registre global
Le namespace de démonstration urn:pdftoolkit:example:report contient le type ReportParagraph. Son mapping cible explicitement le P du namespace PDF 2.0 :
$report = new StructureNamespace(
'urn:pdftoolkit:example:report',
new NamespaceRoleMap(new NamespaceRoleMapping(
new NameType('ReportParagraph'),
new StructureRole(new NameType('P'), $pdf20),
)),
);
Le dictionnaire /RoleMapNS contient une cible de la forme [/P référence-namespace]. Un nom seul y désignerait le namespace PDF 1.7 par défaut, même si le mapping appartient à un autre namespace. La résolution transitive conserve cette distinction, détecte les cycles et possède une limite explicite d’opérations.
Le writer vérifie que les namespaces utilisés et ciblés appartiennent au jeu du document. La validation du graphe effectif contrôle aussi les dictionnaires génériques et les faits figés des révisions : fabriquer des entrées /NS à la main ne permet pas de contourner la version minimale PDF 2.0.
Relier le dessin aux éléments logiques
Chaque ligne est entourée d’une séquence de contenu marqué avec un MCID. Ces nombres sont locaux au conteneur de contenu ; ils ne sont pas des identifiants globaux d’éléments de structure. La page porte /StructParents 0, qui désigne sa clé dans le ParentTree.
PageMarkedContent relie chaque MCID à son StructureElement. Le StructureTreeRoot dérive alors le ParentTree réciproque : MCID 0 vers le titre, 1 vers le paragraphe personnalisé, 2 vers la référence de table des matières, puis 3 et 4 vers les paragraphes suivants. Le même arbre porte les liens /P entre parents et enfants.
La table des matières ajoute une autre relation. Son élément Reference possède un tableau /Ref qui cible le H1. Cette référence structurelle ne crée pas une zone de lien cliquable ; le script ne construit aucune annotation Link. addReference() permet aussi de construire des citations réciproques entre un élément appelant et une note FENote lorsque le document en a besoin.
Choisir les règles d’inclusion et leurs corrections
StructureInclusionRules applique les 53 lignes du tableau 5 d’ISO/TS 32005 aux rôles résolus. Il vérifie les types d’enfants autorisés, leurs bornes d’occurrence, l’héritage des contraintes à travers Part/Div/NonStruct et les assemblages Ruby/Warichu. L’exception prévue pour un arbre exclusivement PDF 1.7 reste distincte des règles d’un arbre mixte.
Le mode par défaut Published2023 suit la source fournie. L’exemple choisit explicitement StructureInclusionCorrections::Resolved336And440 : les résolutions de la PDF Association autorisent Sub dans Code et remplacent la cellule WP/Figure indéfinie c par 0..n. Cette sélection ne prétend pas constituer une nouvelle édition de la norme. Les autres discussions d’errata ne sont pas appliquées implicitement.
Distinguer déclaration et vérification
Le paquet XMP contient un dc:title avec les valeurs x-default et fr. Il contient aussi deux déclarations explicites de l’auteur pour ce petit exemple : ISO/TS 32005 et le niveau de réutilisation WTPDF. Une déclaration est une assertion ; sa présence ne prouve pas que son contenu est vrai.
Les identifiants WTPDF fournis par la spécification sont exactement :
http://pdfa.org/declarations/wtpdf/#reuse1.0
http://pdfa.org/declarations/wtpdf/#accessibility1.0
Le script demande ensuite un audit distinct :
$audit = (new WtPdfAudit())->inspect(
$catalog,
new EffectivePdfVersion($header, $catalog),
WtPdfConformanceLevel::Reuse,
);
$audit->assertNoDetectedFailures();
L’audit ne crée aucune déclaration. Il distingue Failed, HumanReviewRequired et AdditionalValidationRequired. L’exemple ne présente aucun échec détecté, mais conserve les revues en attente. assertNoDetectedFailures() ne signifie donc pas « document certifié accessible ».
Les contrôles mécaniques portent notamment sur les namespaces, les tableaux réguliers avec cellules fusionnées, certaines relations de listes et de notes, les annotations, les métadonnées, les descriptions de fichiers incorporés et l’usage réel des polices. Ils distinguent une police rendue, dont le programme doit être incorporé, d’une police utilisée exclusivement avec le mode de rendu de texte 3. Ils vérifient aussi certaines correspondances CID/GID et ToUnicode.
Le choix sémantique des types, la pertinence d’une description, l’ordre de lecture voulu et le contexte d’un champ exigent une revue humaine. Les programmes de fonte, leurs droits d’incorporation, certaines métriques, la grammaire MathML et d’autres règles exhaustives exigent une validation complémentaire. Le relevé détaillé sépare ces limites des contrôles exécutés. Cet audit partiel ne constitue pas une certification WTPDF ou PDF/UA-2.
Vérifier les octets générés
tools/validate_pdf20_tagging.py inspecte les références indirectes, le mapping personnalisé, le ParentTree, les MCID, Ref et le XML XMP avec pypdf et qpdf. Il vérifie aussi chaque glyphe montré dans cet exemple : présence de son GID, correspondance Unicode et accord entre largeur PDF et métrique du programme TrueType réellement incorporé. Poppler fournit une extraction de texte indépendante. Les trois formats xref sont exercés.
On peut modifier les textes, créer un second namespace personnalisé ou remplacer la table des matières par une liste. Chaque modification doit conserver les associations entre opérateurs, MCID, conteneurs, parents et types résolus. Le fichier visible et le rapport sont deux résultats complémentaires à examiner.
Références
Les sources du bundle conservées proviennent de la PDF Association. Pages physiques utiles : ISO 32000-2, 739–744 pour les dictionnaires et namespaces, 809–810 pour les namespaces standards ; ISO/TS 32005, 9–13 pour les règles de mélange et 14–46 pour le tableau complet ; WTPDF, 9–10 pour les niveaux de conformité, 14–30 pour la structure, 32–49 pour les autres contraintes. Les empreintes et l’identité de chaque fichier figurent dans le catalogue du bundle.
Parcours des tutoriels · Guide PDF 2.0 · Couverture
Voir le résultat PDF
Ce fichier a été généré avec le code de cette documentation. Toutes ses pages sont reproduites ci-dessous ; ouvrir le PDF pour utiliser ses liens et ses signets.
Utiliser le lecteur PDF du navigateur
Ce lecteur dépend du navigateur. Si le document ne s’affiche pas, utiliser « Ouvrir le PDF » ou « Télécharger ».
Code source complet
Code de roukmoute/pdf · Consulter la source au commit documenté ↗
Lire le script commenté : pdf20-tagged-namespaces.php
<?php
declare(strict_types=1);
use PDF\Document\Fonts\CIDFontType2;
use PDF\Document\Fonts\CIDToGIDMap;
use PDF\Document\Fonts\CIDWidths;
use PDF\Document\Fonts\FontDescriptorFlags;
use PDF\Document\Fonts\FontFile2;
use PDF\Document\Fonts\ToUnicodeCMap;
use PDF\Document\Fonts\Type0Font;
use PDF\Document\Metadata\Declarations\Declaration;
use PDF\Document\Metadata\Declarations\Declarations;
use PDF\Document\Metadata\MetadataEntry;
use PDF\Document\Metadata\MetadataStream;
use PDF\Document\Metadata\MetadataUri;
use PDF\Document\Metadata\XmlText;
use PDF\Document\Metadata\XmpLanguageAlternative;
use PDF\Document\Metadata\XmpLocalizedText;
use PDF\Document\Metadata\XmpName;
use PDF\Document\Metadata\XmpPacket;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\Document\Structure\MarkInformation;
use PDF\Document\Structure\MarkedContentContainer;
use PDF\Document\Structure\MarkedContentIdentifier;
use PDF\Document\Structure\NamespaceRoleMap;
use PDF\Document\Structure\NamespaceRoleMapping;
use PDF\Document\Structure\PageMarkedContent;
use PDF\Document\Structure\StandardStructureNamespace;
use PDF\Document\Structure\StructureElement;
use PDF\Document\Structure\StructureInclusionCorrections;
use PDF\Document\Structure\StructureInclusionRules;
use PDF\Document\Structure\StructureNamespace;
use PDF\Document\Structure\StructureNamespaces;
use PDF\Document\Structure\StructureRole;
use PDF\Document\Structure\StructureTreeRoot;
use PDF\Document\Structure\WtPdfAudit;
use PDF\Document\Structure\WtPdfConformanceLevel;
use PDF\FileStructure;
use PDF\Fonts\TrueType\CharacterMapFactory;
use PDF\Fonts\TrueType\CIDFontProgram;
use PDF\Fonts\TrueType\FontDescriptorFactory;
use PDF\Fonts\TrueType\FontProgram;
use PDF\Fonts\TrueType\TextEncoder;
use PDF\Fonts\TrueType\UnicodeText;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators as Op;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DataStructure\TextString;
use PDF\Syntax\DocumentStructure\DocumentCatalog;
use PDF\Syntax\DocumentStructure\PageTree\PageObject;
use PDF\Syntax\DocumentStructure\PageTree\PageTreeNode;
use PDF\Syntax\FileStructure\EffectivePdfVersion;
use PDF\Syntax\FileStructure\CrossReferenceFormat;
use PDF\Syntax\FileStructure\FileBody;
use PDF\Syntax\FileStructure\FileHeader;
use PDF\Syntax\Filters\FlateDecode;
use PDF\Syntax\Object\BooleanType;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\IntegerType;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType;
use PDF\Syntax\Object\StreamObject;
require dirname(__DIR__) . '/vendor/autoload.php';
// PDF 2.0 identifies meaning by the pair (namespace, type), not by a bare name.
// TS 32005 keeps TOC/TOCI/Reference in PDF 1.7 while shared roles use PDF 2.0.
$pdf20 = new StructureNamespace(StandardStructureNamespace::PDF20);
$pdf17 = new StructureNamespace(StandardStructureNamespace::PDF17);
$report = new StructureNamespace('urn:pdftoolkit:example:report', new NamespaceRoleMap(
new NamespaceRoleMapping(new NameType('ReportParagraph'), new StructureRole(new NameType('P'), $pdf20)),
));
$lines = [
'1. Explicit structure namespaces',
'A custom report paragraph resolves to the PDF 2.0 P role.',
'Contents: explicit structure namespaces',
'The table of contents uses TOC, TOCI and Reference from PDF 1.7.',
'Mechanical checks support review; they do not certify accessibility.',
];
// Font conversion is an optional module. The core receives explicit normalized
// font program, CID widths, glyph mapping and ToUnicode PDF objects.
$fontBytes = file_get_contents(dirname(__DIR__) . '/vendor/pdftoolkit/pdf-fonts/examples/assets/fonts/NotoSans-Regular.ttf');
if ($fontBytes === false) {
throw new RuntimeException('Unable to read the demonstration font.');
}
$program = new CIDFontProgram(new FontProgram($fontBytes));
$fontFile = new FontFile2($program->bytes, new FlateDecode());
$map = (new CharacterMapFactory($program))->create($fontFile, new UnicodeText(implode('', $lines)));
$encoder = new TextEncoder($map);
$descriptor = (new FontDescriptorFactory($program))->create($fontFile, new FontDescriptorFlags(4), new NumericType(80));
$font = new Type0Font(new CIDFontType2($descriptor, new CIDToGIDMap($map), new CIDWidths($map)), new ToUnicodeCMap($map));
$resources = new ResourceDictionary(new Font(new DictionaryType(new Entry(new NameType('F1'), new IndirectReference($font)))));
$operators = [];
foreach ($lines as $index => $line) {
$identifier = new MarkedContentIdentifier(new IntegerType($index));
$operators[] = $identifier->begin(new NameType($index === 0 ? 'H1' : 'P'));
$operators[] = new Op\BeginTextObject();
$operators[] = new Op\SetTextFontAndSize(new NameType('F1'), new IntegerType($index === 0 ? 19 : 11));
$operators[] = new Op\MoveTextPosition(new IntegerType(40), new IntegerType(770 - 48 * $index));
$operators[] = new Op\ShowText($encoder->encode(new UnicodeText($line)));
$operators[] = new Op\EndTextObject();
$operators[] = new Op\EndMarkedContent();
}
$page = new PageObject(new Rectangle(new IntegerType(0), new IntegerType(0), new IntegerType(595), new IntegerType(842)), $resources, new StreamObject(new ContentStreams(...$operators)), new Entry(new NameType('StructParents'), new IntegerType(0)));
$pages = (new PageTreeNode())->addKid($page);
$root = new StructureTreeRoot($pages, namespaces: new StructureNamespaces($pdf20, $pdf17, $report), inclusionRules: new StructureInclusionRules(StructureInclusionCorrections::Resolved336And440));
$document = new StructureElement(new StructureRole(new NameType('Document'), $pdf20));
$container = new MarkedContentContainer($page, new IntegerType(0));
$types = [new StructureRole(new NameType('H1'), $pdf20), new StructureRole(new NameType('ReportParagraph'), $report), new StructureRole(new NameType('Reference'), $pdf17), new StructureRole(new NameType('P'), $pdf20), new StructureRole(new NameType('P'), $pdf20)];
$elements = [];
foreach ($types as $index => $type) {
$element = new StructureElement($type, $page);
$element->addKid(new PageMarkedContent($container, new MarkedContentIdentifier(new IntegerType($index))));
$elements[] = $element;
}
$toc = new StructureElement(new StructureRole(new NameType('TOC'), $pdf17));
$item = new StructureElement(new StructureRole(new NameType('TOCI'), $pdf17));
$elements[2]->addReference($elements[0]);
$item->addKid($elements[2]);
$toc->addKid($item);
$document->addKid($elements[0])->addKid($elements[1])->addKid($toc)->addKid($elements[3])->addKid($elements[4]);
$root->addKid($document);
// These are explicit author assertions for this small, manually reviewed sample.
// The audit below never inserts a declaration or turns a successful check into
// certification. Font program/metric and semantic reviews remain separate evidence.
$level = WtPdfConformanceLevel::Reuse;
$metadata = new MetadataStream(new XmpPacket(
new XmpLanguageAlternative(new XmpName('dc', new MetadataUri('http://purl.org/dc/elements/1.1/'), 'title'), new XmlText('PDF 2.0: explicit structure namespaces'), new XmpLocalizedText('fr', new XmlText('PDF 2.0 : espaces de noms de structure explicites'))),
new Declarations(new Declaration(new MetadataUri('https://pdfa.org/declarations#iso32005')), new Declaration(new MetadataUri($level->declarationIdentifier()))),
));
$catalog = new DocumentCatalog($pages, ...[...$root->catalogEntries(new MarkInformation(new BooleanType(true))), new MetadataEntry($metadata), new Entry(new NameType('Lang'), new TextString('en-US'))]);
$header = new FileHeader('2.0');
$audit = (new WtPdfAudit())->inspect($catalog, new EffectivePdfVersion($header, $catalog), $level);
$audit->assertNoDetectedFailures();
// stdout stays binary PDF; the optional --audit-json writes a separate review report.
if (in_array('--audit-json', $argv ?? [], true)) {
fwrite(STDERR, json_encode(['detectedFailures' => count($audit->failures()), 'pendingReviews' => array_map(static fn ($finding): array => ['clause' => $finding->clause, 'page' => $finding->pdfPage, 'status' => $finding->status->name, 'message' => $finding->message], $audit->pending())], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) . PHP_EOL);
}
$format = in_array('--xref-stream', $argv ?? [], true) ? CrossReferenceFormat::Stream : (in_array('--hybrid', $argv ?? [], true) ? CrossReferenceFormat::Hybrid : CrossReferenceFormat::Table);
$file = new FileStructure($header, new FileBody($catalog), new IndirectReference($catalog), crossReferenceFormat: $format);
if (isset($argv[1]) && !str_starts_with($argv[1], '--')) {
$file->writeTo($argv[1]);
} else {
echo $file->toPdf();
}
