PDFlibPas résout les appels récursifs de Form XObject dans les flux de contenu PDF Delphi en suivant la chaîne d'appel active, pas un ensemble global visité, si bien que TPDFlib.EnumPageContentStatesEx peut parcourir le même Form invoqué plusieurs fois sur une page sans confondre une réutilisation légitime avec un cycle. Un Form XObject de tampon dans un modèle de facture est le cas typique : le même objet est appelé depuis l'en-tête, le pied de page, et une couche de filigrane sur une même page, et seule une chaîne d'appel qui reboucle sur elle-même est un véritable cycle
ISO 32000-1 §8.10 définit un Form XObject comme un flux de contenu autonome qu'une page, ou un autre Form, invoque avec l'opérateur Do, complet avec son propre système de coordonnées dans /Matrix, une limite de découpage dans ce système de coordonnées dans /BBox, et optionnellement son propre dictionnaire de ressources. Rien dans la spécification ne plafonne combien de fois un Form peut être invoqué ou à quelle profondeur les Form peuvent s'invoquer mutuellement, si bien qu'un analyseur conforme doit accepter la réutilisation légitime et l'imbrication légitime tout en se défendant contre le seul arrangement que la spécification interdit réellement : un Form dont le flux de contenu, directement ou transitivement, s'invoque lui-même. PDFlibPas rapporte cette distinction via les valeurs TPDFlibContentFormTraversalStatus attachées à chaque instantané Do, notamment ftsEnumerated pour une descente réussie et ftsCycle pour le seul cas qui est réellement une boucle
Pourquoi réutiliser le même Form XObject ne déclenche-t-il pas un faux cycle ?
Une référence de Form XObject répétée n'est pas, en soi, une preuve que quelque chose ne va pas. ISO 32000-1 permet que le même objet Form soit invoqué depuis autant d'endroits d'un flux de contenu que l'auteur le souhaite, ce qui est exactement comment un tampon de logo, un modèle de papier à en-tête, ou un pied de page de numéro de page est réutilisé à travers une page sans dupliquer son flux de contenu plusieurs fois. La protection naïve contre une récursion incontrôlée est un unique ensemble visité indexé par numéro d'objet : la première fois qu'un parcoureur voit l'objet Form 12, il marque 12 comme vu et refuse d'y entrer à nouveau n'importe où ailleurs dans l'arbre. Cette approche se casse au moment où le même tampon apparaît dans deux coins sans rapport d'une même page, car le second appel, entièrement légitime, arrive après que le numéro d'objet soit déjà marqué vu et se voit rejeté comme s'il s'agissait d'une boucle
PDFlibPas évite ce faux positif en limitant la détection de cycle à la chaîne d'appel actuelle plutôt qu'au document entier. EnumPageContentStatesEx empile le flux Form résolu sur la chaîne d'appel active immédiatement avant d'y descendre, puis dépile cette même entrée dès que la descente revient, avec succès ou non. Une invocation sœur du flux identique ne commence qu'après que la première a déjà été dépilée, si bien que la chaîne d'appel est vide de ce flux au moment où l'appel sœur la vérifie, et le parcoureur l'énumère exactement comme il le ferait pour tout autre Form. Un véritable cycle a une forme différente sur cette même chaîne : le Form A appelle le Form B, B est encore ouvert sur la chaîne quand son propre contenu rappelle A, et A est encore assis sur la chaîne depuis l'appel externe qui n'est pas encore revenu — c'est la seule forme que ftsCycle rapporte, un flux Form encore ouvert quelque part plus tôt sur la chaîne d'appel actuelle, pas simplement présent quelque part ailleurs sur la page
À quelle profondeur la récursion de Form XObject peut-elle aller avant que PDFlibPas ne l'arrête ?
La détection de cycle et la limitation de profondeur résolvent deux problèmes différents, et PDFlibPas les garde comme deux résultats TPDFlibContentFormTraversalStatus différents précisément pour cette raison. Une chaîne de vingt Form distincts, chacun appelant le suivant et aucun ne se répétant, n'est un cycle selon aucune définition — la vérification de chaîne active ne trouve jamais de flux répété — mais vingt niveaux honnêtes d'imbrication représentent tout de même vingt niveaux d'analyse, de concaténation de matrice, et de résolution de ressources qu'un PDF malformé ou adversarial pourrait pousser arbitrairement plus haut si rien d'autre ne l'arrêtait. EnumPageContentStatesEx prend un paramètre MaxFormDepth précisément pour cette raison et plafonne quelle que soit la valeur transmise à un maximum de 64, quel que soit ce que l'appelant demande. Une profondeur de zéro est un cas spécial qui mérite d'être connu à part : elle désactive entièrement la récursion de Form et reproduit le comportement plat, page seule, de l'ancienne méthode EnumPageContentStates, ce qui explique pourquoi chaque instantané Do dans ce mode rapporte ftsNotRequested plutôt que de tenter quoi que ce soit
var
Lib: TPDFlib;
States: array of TPDFlibContentGraphicsState;
Count, I: Integer;
begin
Lib:= TPDFlib.Create;
try
if Lib.LoadFromFile('invoice-batch.pdf', '')<> 1 then
Exit;
Lib.SelectPage(1);
Count:= Lib.EnumPageContentStatesEx(True, 8, States); // count only
SetLength(States, Count);
Lib.EnumPageContentStatesEx(True, 8, States); // fill
for I:= 0 to Count- 1 do
if States[I].FormTraversalStatus= ftsCycle then
LogSuspectForm(States[I].XObjectResource, States[I].ContentDepth);
finally
Lib.Free;
end;
end;
Un sous-tracker par invocation : isoler l'état graphique
Chaque descente dans un Form XObject reçoit son propre tracker d'état graphique plutôt que de partager celui déjà en train de parcourir la page, car le flux de contenu d'un Form est tenu de laisser l'état graphique exactement comme il l'a trouvé, et PDFlibPas ne peut pas supposer que chaque PDF qu'il ouvre honore réellement cette exigence. Le tracker enfant démarre à partir d'un instantané de quelle que soit la CTM, l'état de couleur, et les paramètres de texte actifs à l'instruction Do appelante, puis réinitialise sa propre pile de sauvegarde-et-restauration et son suivi de chemin actuel à vide avant d'exécuter une seule instruction du Form. Un q non équilibré sans Q correspondant à l'intérieur d'un Form négligent ou endommagé, pas rare à trouver dans des PDF produits par un outillage plus ancien, reste contenu à l'intérieur du tracker de cette seule invocation et ne fuit jamais dans le tracker de page ni dans une invocation sœur du même tampon assise une ligne plus loin dans le flux de contenu
Le /Matrix du Form se compose avec la CTM en vigueur au Do de la même façon qu'un opérateur cm, multiplié à gauche contre la transformation actuelle plutôt que substitué à elle, et PDFlibPas réutilise délibérément ce même chemin de code plutôt que de maintenir une seconde formule, car deux implémentations indépendantes de la même algèbre matricielle sont exactement le genre de duplication qui dérive silencieusement après quelques cycles de composition d'échelle, de rotation, et de cisaillement. /BBox découpe ensuite dans le propre espace de coordonnées du Form après que la matrice a déjà été appliquée, et les quatre coins de cette boîte sont transformés individuellement plutôt que seulement les coins opposés, car un Form pivoté ou cisaillé peut sinon rapporter une boîte englobante qui manque du contenu réel assis dans ce qui était un coin extrême avant que la transformation ne le déplace ailleurs. Étendre la boucle de l'exemple précédent sur le même tableau States lit ces champs directement
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Do')and (States[I].XObjectKind= cxkForm)and
States[I].FormBBoxKnown then
Writeln('Form ', States[I].XObjectResource, ' matrix ',
States[I].FormMatrix.M11:0:3, ',', States[I].FormMatrix.M12:0:3,
' bbox ', States[I].FormBBoxLeft:0:1, '..', States[I].FormBBoxRight:0:1);
Deux Form avec le même nom de ressource partagent-ils une police ?
Non. Un nom de ressource tel que /F1 ne signifie quelque chose que relativement au dictionnaire de ressources actif au point où il est utilisé, et deux Form XObject différents sont libres de définir deux polices complètement différentes sous ce nom identique. PDFlibPas résout cela en suivant une portée de ressource aux côtés de chaque nom de ressource : lorsqu'un Form porte son propre dictionnaire /Resources, ce dictionnaire devient la portée de ressource complète pour tout ce qui se trouve à l'intérieur, sans repli par clé vers le dictionnaire de la page ou de l'appelant pour ce que le propre dictionnaire du Form se trouve omettre. Seul un Form sans aucune clé /Resources du tout, un schéma encore produit par certains générateurs PDF plus anciens, hérite du dictionnaire appelant en bloc, et c'est une exception de compatibilité délibérée plutôt qu'une règle générale sur laquelle s'appuyer dans une nouvelle sortie. L'identité de police dans un instantané TPDFlibContentGraphicsState est donc la paire de FontResource et FontResourceScope, pas le nom seul, avec FontObjectNumber disponible pour confirmer exactement à quel objet indirect un /F1 donné s'est résolu dans cette portée particulière
Le même scoping s'applique à toute autre ressource nommée qu'un Form peut porter, entrées ExtGState et entrées XObject imbriquées incluses, puisque le mécanisme de résolution sous-jacent ne traite pas les polices comme un cas spécial — le cas de la police se trouve simplement compter le plus, car une identité de police mal appariée produit silencieusement les mauvais glyphes plutôt qu'un échec évident. Un code d'extraction qui regroupe les suites de texte par seul nom de police, sans regrouper aussi par portée de ressource, fusionnera deux polices visuellement différentes qui se trouvent partager un nom, et l'erreur ne fera surface que lorsque quelqu'un remarquera des chiffres de la mauvaise police assis à l'intérieur de ce qui était censé se lire comme une police cohérente
for I:= 0 to Count- 1 do
if (States[I].OperatorName= 'Tj')and States[I].TextAdvanceResolved then
RecordGlyphRun(States[I].FontResource, States[I].FontResourceScope,
States[I].FontObjectNumber, States[I].ContentDepth);
Lire FormTraversalStatus dans votre propre pipeline
FormTraversalStatus transforme chaque instantané Do en un petit rapport de diagnostic à lui seul, et un pipeline qui l'ignore jette précisément l'information qui expliquerait une extraction incomplète. ftsNotApplicable signifie que l'instruction n'a jamais été une invocation Form résolue en premier lieu ; ftsNotRequested signifie que la récursion était désactivée pour cet appel ; ftsEnumerated signifie que le Form a été analysé et parcouru avec succès ; ftsDepthLimit et ftsCycle marquent les deux façons dont une descente est délibérément écourtée ; et ftsMalformed couvre tout le reste qui a arrêté le parcours — une référence de flux non résolvable, un /Matrix ou /BBox qui a échoué à s'analyser, ou une exception levée en exécutant le propre contenu du Form. Ce dernier cas compte opérationnellement, car un parcours imbriqué échoué annule quelle que soit la sortie partielle qu'il avait déjà produite pour cette branche, si bien qu'un appelant n'a jamais à deviner si un Form était réellement vide ou a simplement explosé deux instructions dans son flux de contenu
var
Tally: array[TPDFlibContentFormTraversalStatus] of Integer;
Status: TPDFlibContentFormTraversalStatus;
begin
for Status:= Low(Tally) to High(Tally) do
Tally[Status]:= 0;
for I:= 0 to Count- 1 do
Inc(Tally[States[I].FormTraversalStatus]);
if (Tally[ftsCycle]> 0)or (Tally[ftsMalformed]> 0) then
FlagForManualReview(SourceFileName, Tally[ftsCycle], Tally[ftsMalformed]);
end;
Limites, coûts, et où cela s'articule
Le flux de contenu d'un Form est décodé et analysé exactement une fois par appel d'énumération quel que soit le nombre de fois où le Form est invoqué, car PDFlibPas met en cache la liste d'instructions analysée contre l'objet de flux sous-jacent plutôt que de la ré-analyser à chaque appel sœur — le tampon à trois coins de l'exemple d'ouverture est décodé une fois et parcouru trois fois, pas décodé trois fois. Ce qui est effectivement reconstruit à chaque invocation individuelle est tout ce qui diffère légitimement d'un point d'appel au suivant : le tracker enfant, la CTM concaténée, le découpage intersecté, et la portée de ressource. Cette comptabilité de CTM et de découpage par invocation est la même mécanique derrière le tracker d'état de CTM et de découpage de flux de contenu de PDFlibPas, qui mérite d'être lu aux côtés de celui-ci pour tout parcours de flux de contenu qui va au-delà de la récursion de Form elle-même
Deux limites méritent que l'on fixe des attentes avant que cette API n'entre dans un pipeline plus large. Le plafond de profondeur de 64 n'est pas un bouton de réglage pour des documents légitimement profonds, car de véritables factures, relevés, et modèles de rapport n'imbriquent essentiellement jamais des Form à plus de trois ou quatre niveaux de profondeur — un document qui atteint réellement ftsDepthLimit est bien plus susceptible d'être malformé ou adversarial qu'inhabituellement élaboré, et mérite d'être journalisé comme un signal de qualité de données plutôt que silencieusement réessayé avec un nombre plus grand. EnumPageContentStatesEx est aussi une API d'analyse côté lecture : elle rapporte ce que fait un flux de contenu, pas si un Form devrait être visible du tout, ce qui est une question distincte à laquelle répond l'état de visibilité des groupes de contenu optionnel lorsqu'un tampon ou un Form de filigrane se trouve derrière une couche qu'une visionneuse pourrait avoir désactivée. La détection de cycle par chaîne d'appel, l'isolation par invocation, et le scoping de ressources constituent ensemble un coin de la surface d'inspection de flux de contenu dans le composant PDFlibPas pour Delphi et C++Builder