PDFium Component balise du contenu PDF existant depuis Delphi via EditPdfDocumentStructure dans FPdfDocumentStructure.pas. Les opérations psekAddMarkedContent et psekAddObjectReference font plus quajouter un MCR ou un OBJR au /K dun élément de structure : elles allouent aussi la clé /StructParents ou /StructParent et écrivent lentrée /ParentTree correspondante, lindex inversé quISO 32000-1 §14.7.4.4 exige
Cette seconde moitié est là où la pose rétroactive de balises tourne dordinaire mal. Ajouter une enveloppe BDC et un MCR est facile, et une visionneuse qui parcourt larbre de structure depuis le haut trouvera joyeusement le nouveau contenu. Les technologies dassistance vont souvent dans lautre sens, cependant : elles partent dun morceau de contenu sur la page et demandent quel élément de structure le possède. Si la page na pas de clé /StructParents, ou si le /ParentTree na pas dentrée pour le nouveau MCID, cette recherche échoue, et les vérificateurs le rapportent comme un arbre parent manquant ou incohérent. Le fichier a lair balisé et ne marche quand même pas
Que mappe réellement /ParentTree ?
Le /ParentTree est un arbre de nombres qui mappe des clés entières vers des éléments de structure, et les clés viennent du contenu, pas de larbre. ISO 32000-1 §14.7.4.4 donne à chaque page ou Form XObject contenant du contenu marqué un entier /StructParents, et la valeur darbre parent sous cette clé est un tableau indexé par MCID : lentrée 3 du tableau est lélément de structure qui possède <</MCID 3>>. Une annotation ou un XObject employé comme article de contenu entier reçoit une clé /StructParent unique à la place, et sa valeur darbre parent est lélément de structure lui-même, pas un tableau
Le format conteneur est larbre de nombres générique de §7.9.7. Une feuille détient /Nums, un tableau plat de paires clé-valeur triées en ordre de clé croissant. Les nœuds intermédiaires et feuilles sous la racine portent /Limits avec la plus petite et la plus grande clé quils couvrent, tandis que la racine elle-même ne doit pas avoir de /Limits. Par-dessus, la racine de larbre de structure peut porter /ParentTreeNextKey (§14.7.2), la prochaine clé quun rédacteur devrait distribuer. Toute édition qui balise du nouveau contenu touche donc jusquà quatre objets qui doivent sentendre : le flux de contenu, le dictionnaire de page ou de formulaire, la feuille de larbre parent, et la racine de larbre de structure
Le sens direct a ses propres règles, posées dans §14.7.4.2 et §14.7.4.3. Une référence de contenu marqué (MCR) pour du contenu dans le flux de contenu propre de la page na besoin que de /Pg et /MCID. Quand le contenu marqué vit à lintérieur dun Form XObject, le MCR exige aussi un /Stm pour nommer ce flux, parce que les MCIDs sont numérotés par flux de contenu. Une référence dobjets (OBJR) nomme lannotation ou le XObject avec /Obj et sa page avec /Pg
Balisage dun flux de contenu avec EditPdfDocumentStructure
EditPdfDocumentStructure est un éditeur par lots gardé par photo. Vous appelez dabord ReadPdfDocumentStructure pour obtenir un TPdfDocumentStructureSnapshot, puis repassez son SourceSha256 avec un tableau denregistrements TPdfStructureEditOperation. Si les octets de la source ont changé entre-temps, lédition est refusée sans sortie. Les éléments existants sont adressés par leur numéro dobjets dans NodeID, et 0 dénote la racine de larbre de structure
uses
System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentStructure;
function TagContentStream(const FileName, OutName: string;
ElementID, StreamObj, StreamGen: Integer): Boolean;
var
Source: TMemoryStream;
Options: TPdfStructureEditOptions;
Snapshot: TPdfDocumentStructureSnapshot;
Ops: TPdfStructureEditOperations;
Report: TPdfStructureEditReport;
Output: TBytes;
begin
Source := TMemoryStream.Create;
try
Source.LoadFromFile(FileName);
Options := TPdfStructureEditOptions.Default;
if not ReadPdfDocumentStructure(Source, Options, Snapshot, Report) then
raise Exception.Create(Report.ErrorMessage);
SetLength(Ops, 1);
Ops[0] := Default(TPdfStructureEditOperation);
Ops[0].Kind := psekAddMarkedContent;
Ops[0].NodeID := ElementID; // élément existant, par numéro dobjets
Ops[0].TargetObjectNumber := StreamObj; // flux de contenu de page ou Form XObject de page
Ops[0].TargetGeneration := StreamGen;
Result := EditPdfDocumentStructure(Source, Snapshot.SourceSha256,
Ops, Options, Output, Report);
if not Result then
raise Exception.Create(Report.ErrorMessage);
TFile.WriteAllBytes(OutName, Output);
finally
Source.Free;
end;
end;
Pour cette seule opération, EditPdfDocumentStructure enveloppe le flux cible dans /P <</MCID n>> BDC ... EMC, ajoute le MCR au /K de lélément, donne à la page (ou à la forme) une clé /StructParents si elle nen a pas, et écrit lélément n dans le tableau darbre parent pour cette clé. Le résultat est écrit comme une mise à jour incrémentale après les octets dorigine, et Report.CreatedMarkedContents vous dit combien de MCRs ont été créés. Quand le flux contient déjà des paires BDC/EMC équilibrées, seule la queue non balisée après le dernier EMC de premier niveau est enveloppée ; sinon tout le flux lest
Comment les nouveaux MCIDs et clés darbre parent sont-ils choisis ?
Un nouveau MCID est le maximum courant plus un, et une nouvelle clé darbre parent est Max(MaxKey + 1, ParentTreeNextKey). Pour le MCID, EditPdfDocumentStructure prend le maximum au travers des MCRs déjà enregistrés pour cette page (ou ce Form XObject) et de chaque /MCID quil trouve dans les listes de propriétés propres du flux. Compter les deux compte : un arbre de structure peut avoir perdu un MCR alors que le contenu porte encore la marque, et réemployer ce nombre ferait que deux éléments revendiquent le même contenu
La logique de clé suit la même idée défensive. Si le propriétaire a déjà une valeur /StructParents, le tableau existant est étendu à lindex MCID, avec null comblant tout trou, et aucune nouvelle clé nest allouée. Sinon, le composant balaye chaque feuille /Nums pour la clé la plus haute, la compare à /ParentTreeNextKey, et prend la plus grande. Une /ParentTreeNextKey périmée plus basse que des clés déjà employées et une qui réserve des clés au-delà de la dernière entrée sont toutes deux gérées, parce que la plus grande des deux valeurs est toujours libre. Après lallocation, /ParentTreeNextKey est avancée à la nouvelle clé plus un
La nouvelle paire est insérée dans /Nums en ordre croissant plutôt quajoutée, parce que les recherches dans les arbres de nombres comptent sur cet ordre. Si la feuille porte /Limits, les bornes sont recalculées depuis le tableau réécrit. Quand la racine de larbre de structure na aucun /ParentTree du tout, ce qui est courant dans des fichiers balisés par des outils qui nont écrit que larbre direct, EditPdfDocumentStructure synthétise un nouvel objet indirect << /Nums [] >> et le lie depuis /StructTreeRoot avant dinsérer la première entrée
Form XObjects, /StmOwn et références dobjets
Le contenu marqué à lintérieur dun Form XObject nest pris en charge que quand cette forme est employée par exactement une page. Le MCR quEditPdfDocumentStructure écrit pour une forme porte /Pg, un /Stm nommant le flux de la forme, et un /StmOwn nommant la page, et la clé /StructParents va sur le dictionnaire de la forme plutôt que sur la page. Une forme dessinée par trois pages a trois propriétaires plausibles, si bien que tout /StmOwn unique serait faux pour deux dentre elles. Le composant refuse lédition avec une erreur au lieu de deviner
La même règle dunicité sapplique à psekAddObjectReference. La cible doit être une annotation qui apparaît dans le /Annots dexactement une page, ou un Form ou Image XObject qui apparaît dans les ressources dexactement une page ; tout le reste est rejeté. LOBJR reçoit /Obj et /Pg, la cible reçoit une clé /StructParent (réemployée si présente), et la valeur darbre parent est une référence simple vers lélément. Cest la correction habituelle pour une annotation de lien ou une image de figure qui était balisée visuellement mais jamais reliée à larbre
// Envelopper une image non balisée dans un nouvel élément Figure, puis la lier deux façons :
// comme un OBJR pour limage XObject et comme contenu marqué sur la page
SetLength(Ops, 3);
Ops[0] := Default(TPdfStructureEditOperation);
Ops[0].Kind := psekInsertContainer;
Ops[0].NodeID := -1; // ID temporaire, doit être négatif et unique
Ops[0].ParentNodeID := SectID; // élément Sect existant
Ops[0].Value := 'Figure'; // rôle standard ou entrée RoleMap
Ops[1] := Default(TPdfStructureEditOperation);
Ops[1].Kind := psekAddObjectReference;
Ops[1].NodeID := -1; // réfère le conteneur créé ci-dessus
Ops[1].TargetObjectNumber := ImageObj;
Ops[1].TargetGeneration := 0;
Ops[2] := Default(TPdfStructureEditOperation);
Ops[2].Kind := psekAddMarkedContent;
Ops[2].NodeID := -1;
Ops[2].TargetObjectNumber := ContentObj;
Ops[2].TargetGeneration := 0;
if EditPdfDocumentStructure(Source, Snapshot.SourceSha256, Ops, Options,
Output, Report) then
Writeln(Format('Figure is object %d, %d OBJR, %d MCR', [
Report.InsertedNodes[0].ObjectNumber,
Report.CreatedObjectReferences, Report.CreatedMarkedContents]));
Des valeurs NodeID négatives nomment des conteneurs créés plus tôt dans le même lot, et Report.InsertedNodes mappe chaque ID demandé vers le numéro dobjets quil a reçu. psekInsertContainer accepte les rôles standards PDF 1.7, PDF 2.0 et PDF/UA tels que P, H1 à H6, Figure, Table ou LI, plus les rôles personnalisés déjà mappés dans /RoleMap. Régler BeforeNodeID sur les deux opérations de contenu place le nouveau MCR ou OBJR avant un enfant existant au lieu de le mettre à la fin
Pourquoi éditer deux fois le même flux dans un lot exige-t-il du soin ?
Un lot peut frapper le même objet de façon répétée, et chaque opération ultérieure doit voir le résultat des antérieures. Deux opérations psekAddMarkedContent contre la même page réécrivent toutes deux son flux de contenu, et toutes deux mettent à jour la même feuille de larbre parent. Une implémentation qui monte chaque édition comme un objet ajouté séparé écrit deux versions de lobjet 12 dans la mise à jour. Selon laquelle le lecteur garde, vous perdez la première enveloppe BDC ou la première paire /Nums, et le second MCID peut entrer en collision avec le premier parce quil a été calculé depuis des octets périmés
EditPdfDocumentStructure indexe les mises à jour montées par (ObjectNumber, Generation) et écrase une entrée existante en place, et il rafraîchit en même temps la copie en mémoire de lobjet. La seconde opération analyse donc le flux déjà enveloppé, voit MCID 0, alloue MCID 1, et étend le même tableau darbre parent. Un objet, une version finale. Avant que quoi que ce soit soit renvoyé, la sortie est relue et comparée au modèle en mémoire : compte déléments, comptes darticles de contenu et chaque MCR ou OBJR retenu doivent correspondre, sinon lappel échoue sans sortie
Les flux compressés suivent le même chemin. Un flux de contenu avec /Filter /FlateDecode (ou un tableau [/FlateDecode] à un élément) et sans prédicteur /DecodeParms est dégonflé avec un décompresseur borné plafonné à 32 Mio ou MaxOutputBytes, le plus petit des deux, enveloppé, et recomprimé en /FlateDecode avec un nouveau /Length. Un flux qui gonflerait au-delà du plafond est rejeté plutôt que tronqué, ce qui compte pour des fichiers de sources non fiables
Limites à connaître avant une passe de réparation PDF/UA
EditPdfDocumentStructure est conservateur par conception, et la plupart de ses limites surgissent comme un Report.ErrorMessage clair plutôt que comme un fichier discrètement cassé. Gardez ceci en tête quand vous planifiez une réparation par lots :
- Seuls les flux non filtrés et
FlateDecodesimples peuvent être enveloppés ; les autres filtres, ou Flate avec prédicteur, sont refusés - Un flux de contenu doit appartenir au
/Contentsdexactement une page, et les Form XObjects, annotations et XObjects doivent être employés par exactement une page - Lenveloppe emploie toujours la balise
/P; le rôle sémantique vient du/Sde lélément de structure, pas du nom de baliseBDC - La synchronisation de larbre parent lit une racine
/Numsplate ou un niveau de feuilles/Kids; les nœuds intermédiaires plus profonds ne sont pas descendus, donc revalidez de tels fichiers après édition - Les fichiers qui contiennent des signatures ou des champs de signature sont refusés carrément, donc signez après la réparation de structure, pas avant
Létape de validation nest pas optionnelle pour un pipeline de réparation. Lancez la validation de larbre de structure PDF/UA dans PDFium Component avant et après le lot, et employez lAPI de contenu marqué dobjets de page pour lire les balises BDC et les MCIDs quand vous devez confirmer quels objets de page ont fini à lintérieur des nouvelles marques. Un arbre parent propre vous dit que les recherches se résolvent ; il ne vous dit pas que lordre de lecture ou le texte alternatif est bon
FAQ
EditPdfDocumentStructure reconstruit-il tout le /ParentTree ?
Non. EditPdfDocumentStructure ne met à jour que les entrées du contenu quil balise dans le lot courant, en réemployant les clés /StructParents existantes et en allouant les nouvelles là où il le faut, si bien que les entrées darbre parent existantes restent intactes
Que se passe-t-il si le PDF a un arbre de structure mais pas de /ParentTree ?
Le composant crée un arbre de nombres indirect << /Nums [] >>, le lie depuis /StructTreeRoot, et insère la première clé pour le contenu en cours de balisage
Puis-je baliser du contenu dans un Form XObject partagé par plusieurs pages ?
Pas avec cette opération. Une forme partagée rend /StmOwn ambigu, si bien que psekAddMarkedContent exige que la forme soit référencée par exactement une page et rapporte une erreur sinon
La lecture et lédition de larbre de structure, la validation PDF/UA et laccès au contenu marqué font partie du PDFium Component for Delphi and C++Builder, qui enveloppe PDFium pour des projets Delphi, C++Builder et Lazarus