PDFlibPas, la bibliothèque de composant PDF VCL native pour Delphi et C++Builder, rejoue le flux de contenu d'une page via sa classe TPDFContentStateTracker sans jamais toucher à un canevas de rendu du tout. Alimenter le tracker un opérateur analysé à la fois maintient un enregistrement d'état graphique courant — matrice de transformation actuelle, matrice de texte, limites de découpage, et pile de sauvegarde q/Q — disponible en instantané avant ou après l'exécution de chaque opérateur
Demandez où une suite de texte atterrit réellement sur la page imprimée, et les seuls nombres bruts du flux de contenu vous induiront en erreur à chaque fois. TPDFContentProgram.GetTextRuns rapporte déjà le point d'ancrage de chaque instruction d'affichage de texte via les champs OriginX et OriginY sur TPDFTextRun, et les commentaires de champ sont explicites sur le fait que ce point se trouve dans l'espace de texte, déjà replié à travers Tm, Td, TD, et T*. Ce qui manque encore, et ce que ces commentaires disent qu'un appelant doit fournir, est la CTM active à cette instruction exacte — le produit de chaque cm concaténé jusque-là, imbriqué dans quel que soit le nombre de paires q/Q ouvertes à ce point du flux
Pourquoi rejouer un flux de contenu plutôt que de le rendre ?
PDFlibPas maintient deux notions séparées d'état graphique pour deux tâches séparées, et la séparation est délibérée. L'enregistrement d'état interne du moteur de rendu porte un handle de canevas de périphérique vivant, un handle de région de découpage, et des caches de rastérisation de police — de véritables ressources liées à quelle que soit la surface actuellement peinte, et sans signification une fois cette surface disparue. TPDFContentGraphicsState ne porte rien de tout cela : c'est un simple enregistrement limité aux valeurs qu'ISO 32000-1 §8.4 définit comme atteignables depuis les seuls opérateurs de flux de contenu — la CTM, le style de ligne, la couleur, l'état du texte, et les limites de découpage et de chemin dérivées. Comme l'enregistrement ne détient aucune référence de canevas ni aucun handle de fichier ouvert, un appelant peut analyser un flux de contenu, le parcourir avec TPDFContentStateTracker, et continuer à utiliser les instantanés résultants longtemps après que ce qui a produit les octets a disparu
Comment TPDFContentStateTracker construit-il la CTM
TPDFContentStateTracker.Apply concatène les six opérandes d'un opérateur cm dans la CTM du tracker en utilisant la même prémultiplication que PDF spécifie lui-même : la nouvelle matrice M2 se combine avec la CTM actuelle comme M2 × CTM, dans la convention vecteur-ligne où un point se transforme comme P′ = P × M (ISO 32000-1 §8.4). La partie facile à mal faire se trouve dans le terme de translation, pas dans la partie linéaire : la propre translation de M2 doit passer à travers la composante rotation-et-échelle de la CTM actuelle avant que la translation de la CTM actuelle ne soit ajoutée par-dessus. Sautez cette étape et codez en dur une combinaison naïve composante par composante à la place, et le premier cm isolé que vous testerez aura l'air correct tandis que chaque coordonnée en aval d'un second ou troisième cm imbriqué dérivera silencieusement, ce qui est exactement le genre de bogue qui survit à une relecture de code car le test unitaire qui l'attraperait a besoin d'au moins deux transformations chaînées pour échouer
var
Prog: TPDFContentProgram;
Runs: TPDFTextRunArray;
States: TPDFContentGraphicsStateArray;
DeviceX, DeviceY: Double;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
try
Prog.Parse(ContentBytes);
Runs := Prog.GetTextRuns;
// One before-instruction snapshot per operator, computed in a single pass
States := Prog.TraceGraphicsStates(nil, False);
for I := 0 to High(Runs) do
begin
// OriginX/OriginY already fold in Tm/Td/TD/T*; only the CTM active
// at this instruction is still missing (ISO 32000-1 8.4)
with States[Runs[I].InstructionIndex].CTM do
begin
DeviceX := Runs[I].OriginX * M11 + Runs[I].OriginY * M21 + DX;
DeviceY := Runs[I].OriginX * M12 + Runs[I].OriginY * M22 + DY;
end;
LogTextOrigin(Runs[I].Text, DeviceX, DeviceY); // caller-supplied handler
end;
finally
Prog.Free;
end;
end;
La boucle ci-dessus répond au point douloureux de l'introduction : TPDFContentProgram.GetTextRuns renvoie OriginX et OriginY déjà repliés à travers Tm, Td, TD, et T*, et TraceGraphicsStates(nil, False) fournit la seule pièce manquante, la CTM avant-instruction à l'index exact où chaque suite a été capturée, en une seule passe linéaire sur tout le programme. Passer nil laisse la méthode posséder un tracker privé pour l'appel et le libérer en interne, ce qui est le bon choix pour un scan ponctuel ; passer plutôt une instance TPDFContentStateTracker existante est ce qui garde l'état continu à travers une page assemblée à partir de plus d'un flux de contenu, puisqu'ISO 32000-1 traite le tableau /Contents d'une page comme un seul flux logique et que la pile q/Q doit s'accorder
La matrice de texte survit à Q ; l'état graphique non
ISO 32000-1 §9.4.2 définit Td, TD, Tm, et T* comme les opérateurs qui construisent la matrice de texte et la matrice de ligne de texte à l'intérieur d'un bloc BT/ET, et PDFlibPas garde cette distinction nette : Td et TD concatènent une translation pure sur la matrice de ligne de texte, T* fait de même en utilisant le négatif de l'interligne actuel, et seul Tm remplace entièrement les deux matrices par les six nombres qui lui sont donnés. BT réinitialise les deux matrices à l'identité, exactement une fois, au début de l'objet texte — mais q et Q n'y touchent pas du tout. TPDFContentStateTracker.Apply traite coRestoreState comme un cas spécial précisément pour cette raison : avant de dépiler l'état sauvegardé, il capture la matrice de texte actuelle, la matrice de ligne de texte, et le drapeau BT/ET, et les réapplique par-dessus quel que soit l'état dépilé, car une paire q/Q enveloppant une suite de texte n'est pas censée déplacer la position du texte en arrière
var
Tracker: TPDFContentStateTracker;
Prog: TPDFContentProgram;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
Tracker := TPDFContentStateTracker.Create;
try
Prog.Parse('BT 100 700 Td q 2 0 0 2 0 0 cm (A) Tj Q (B) Tj ET');
for I := 0 to Prog.Count - 1 do
begin
Tracker.Apply(Prog[I]);
if Prog[I].Op in [coShowText, coRestoreState] then
LogState(Prog[I].OpName, Tracker.Snapshot); // caller-supplied handler
end;
finally
Tracker.Free;
Prog.Free;
end;
end;
Exécutez cette séquence et la CTM rapportée au second Tj est revenue à l'échelle identité qu'elle avait avant q — le 2 0 0 2 0 0 cm à l'intérieur de la paire save/restore a disparu, comme q/Q l'exige. TextMatrix.DX à cette même instruction, cependant, vaut toujours 100 : le Td qui l'a défini s'est exécuté avant le q, si bien que ce n'est pas un état graphique que Q ait jamais eu le droit de toucher, et un outil qui aurait supposé le contraire rapporterait la seconde suite de glyphes démarrant depuis la mauvaise position horizontale sur la page
Que se passe-t-il quand un opérateur de chemin de découpage s'exécute ?
Un opérateur W ou W* ne rétrécit pas immédiatement le découpage ; il ne fait qu'enregistrer quelle règle de remplissage utiliser, et l'intersection réelle attend quel que soit l'opérateur de peinture de chemin qui suit, y compris le peintre no-op n que les auteurs PDF utilisent couramment précisément pour découper sans rien dessiner. TPDFContentStateTracker reflète précisément ce timing en deux étapes : coClip et coClipEvenOdd ne définissent qu'un drapeau de règle de découpage en attente, et EndCurrentPath — invoquée par chaque opérateur de peinture de chemin — est ce qui intersecte réellement les limites du chemin en attente dans ClipMinX, ClipMinY, ClipMaxX, et ClipMaxY. Bien faire cet échelonnement compte pour le contrat d'instantané avant/après lui-même : un instantané avant pris exactement à l'instruction W doit encore montrer l'ancien découpage, plus large, car le découpage n'a pas encore pris effet à ce point du flux, et fusionner les deux étapes en une seule casserait silencieusement chaque appelant qui compte sur le fait que l'état-avant signifie ce qu'il dit
ClipBoundsExact indique à un appelant laquelle de deux situations il regarde, et il n'est jamais True que pour un unique rectangle aligné sur les axes construit par re sur un chemin par ailleurs vide — la seule forme que PDFlibPas peut représenter exactement comme quatre nombres. Tout le reste — un rectangle pivoté, un contour courbe, un chemin composé de plusieurs sous-chemins, ou un découpage construit à partir d'un mode de rendu de texte — produit tout de même ClipMinX à ClipMaxY, mais avec ClipBoundsExact réglé sur False, un signal honnête que les quatre nombres sont une borne extérieure sûre et non la véritable forme de découpage ; les appelants qui n'ont besoin que de cette borne, comme isoler une sous-région rectangulaire avant la conversion descendante en demi-teinte GDI décrite dans le rendu de pages PDF en monochrome 1 bit, peuvent la lire directement plutôt que de la redériver de la géométrie de page
Courbes de Bézier : une borne exacte ou une borne sûre
Le moyen le moins coûteux de borner un segment de Bézier cubique est de prendre l'enveloppe convexe de ses quatre points de contrôle, et cela est toujours sûr car la courbe ne la quitte jamais — mais une courbe peu profonde et large peut rapporter une boîte englobante bien plus grande que ce que la courbe occupe réellement, ce qui affaiblit le filtrage basé sur le découpage précisément quand cela compte le plus, sur de grands chemins décoratifs. PDFlibPas résout plutôt le problème plus serré : pour chaque axe, il résout la dérivée de la courbe cubique pour les racines à l'intérieur de l'intervalle ouvert (0, 1) et évalue la courbe à toute racine trouvée, ainsi qu'aux deux extrémités, ce qui est la méthode standard sous forme close pour obtenir l'étendue alignée sur les axes réelle d'une courbe plutôt qu'une surestimation. La précision par courbe ne se reporte cependant pas sur le découpage lui-même : une fois qu'un contour courbe devient un chemin de découpage, ClipBoundsExact retombe encore à False pour lui, car une boîte englobante, aussi serrée soit-elle, n'est toujours pas la même forme que la courbe qu'elle borne, et le tracker d'état préfère le dire plutôt que de laisser un appelant supposer un rectangle là où se trouve réellement une courbe
Lire l'état avant et après chaque opérateur
Le fait qu'un appelant veuille l'état avant ou après dépend entièrement de ce que fait l'opérateur : une question de dessin ou de test d'atteinte sur un chemin ou une suite de texte veut l'état tel qu'il était l'instant avant que cet opérateur ne s'exécute, puisque c'est ce qui a réellement déterminé comment l'opérateur a peint, tandis qu'une question de diagnostic sur un opérateur de définition d'état comme gs veut généralement voir ce qu'il vient de changer. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) expose exactement ce choix comme un simple booléen, calculant un TPDFContentGraphicsState par instruction en une seule passe linéaire sur tout le programme, quel que soit l'instant demandé. GetGraphicsState(InstructionIndex, AfterInstruction, State) offre le même choix avant/après pour une seule instruction plutôt que pour tout le programme, mais il rejoue depuis l'instruction zéro à chaque appel pour y arriver, si bien que scanner de nombreux index en l'appelant dans une boucle coûte O(n²) contre un seul appel O(n) à TraceGraphicsStates sur le même programme
var
Before, After: TPDFContentGraphicsState;
begin
// Same instruction index, two different instants: before vs. after it runs
Prog.GetGraphicsState(CmIndex, False, Before);
Prog.GetGraphicsState(CmIndex, True, After);
// Before.CTM reflects every earlier cm; After.CTM already folds in
// this instruction's own concatenation as well
end;
Vivre avec des flux de contenu malformés
Deux genres d'entrée malformée sont assez courants dans les véritables producteurs PDF pour que TPDFContentStateTracker doive les tolérer plutôt que d'échouer sur eux. Le premier est un chemin qui s'étend à travers une limite q/Q : le chemin actuel, le point actuel, et le compte de sous-chemins ne sont pas des paramètres d'état graphique — ISO 32000-1 §8.4 couvre ce que q et Q sauvegardent et restaurent, et le chemin actuellement construit n'en fait pas partie — si bien que TPDFContentStateTracker suit ces données entièrement en dehors de l'état sauvegardé, et un sous-chemin commencé avant un q est toujours là, non peint, immédiatement après le Q correspondant. Le second est un Q nu sans q correspondant nulle part avant lui dans le flux, pas rare dans la sortie de générateurs qui assemblent des fragments de flux de contenu par concaténation et se trompent dans la comptabilité. TPDFContentStateTracker.RestoreUnderflowCount compte chacun de ces événements au lieu de lever une exception ou de corrompre l'état : un Q sans correspondance laisse simplement l'état graphique actuel exactement tel qu'il était, comme si cette instruction avait été un no-op, si bien que le reste du flux continue de se rejouer sur un état sain et un appelant peut encore décider ensuite, à partir du compte, si l'entrée mérite d'être signalée à celui qui l'a produite
La composition de la CTM, l'indépendance de la matrice de texte par rapport à q/Q, et la réalisation échelonnée d'un chemin de découpage ne dépendent pas de comment ou si le flux de contenu est un jour peint, ce qui est précisément le point : le même instantané TPDFContentStateTracker est correct que la page ne soit jamais rendue du tout ou soit sur le point d'être transmise à quel que soit le back-end que PDFlibPas sélectionne pour ce fichier, y compris le basculement de moteur à l'exécution couvert dans le guide du rendu PDF multi-moteur dans PDFlibPas. L'analyse de contenu, le mappage de coordonnées, et l'outillage de rédaction peuvent tous s'exécuter entièrement sur la sortie du tracker, bien avant ou complètement sans jamais demander à un moteur de rendu de s'impliquer
Le rejeu de flux de contenu via TPDFContentStateTracker fait partie du cadre d'édition de contenu structuré intégré à PDFlibPas, la bibliothèque de composant PDF VCL native pour Delphi et C++Builder