PDF Library for Delphi retire un opérateur de matrice de texte identité, 1 0 0 1 0 0 Tm, pendant son optimisation peephole des flux de contenu à la sauvegarde, uniquement quand la matrice de texte et la matrice de ligne de texte valent déjà l'identité : juste après BT, ou juste après un Tm identité antérieur. Un cm identité, lui, est toujours jeté, parce que cm multiplie la CTM tandis que Tm remplace carrément les deux matrices de texte. Depuis la v3.539.28, tout autre Tm identité reste dans le flux
Le bug corrigé ici est du genre discret. Un générateur de rapports émet BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, en comptant sur le Tm identité pour renvoyer la seconde chaîne à l'origine de l'espace texte avant d'appliquer sa propre logique de positionnement. L'ancien optimiseur voyait six nombres qui épellent la matrice identité, décidait que l'opérateur ne pouvait rien changer, et le supprimait. Rien n'échouait, rien ne journalisait d'avertissement, et la page sauvegardée dessinait « Total » immédiatement après « Invoice » sur la même ligne de base, exactement la classe de défaut que personne ne remarque avant qu'un client n'imprime le PDF
Pourquoi 1 0 0 1 0 0 Tm n'est-il pas toujours un no-op ?
Un Tm identité n'est un no-op que s'il va remplacer deux matrices qui portent déjà l'identité, et c'est une propriété des opérateurs qui le précèdent, pas de ses propres opérandes. ISO 32000-1 §9.4.1 dit que BT initialise à la fois la matrice de texte (Tm) et la matrice de ligne de texte (Tlm) à l'identité, et §9.4.2 définit Tm comme le fait de mettre les deux aux valeurs données, pas de les concaténer. Comparez avec cm (§8.4.4), qui multiplie à droite la matrice de transformation courante : multiplier par l'identité laisse toute CTM inchangée, donc 1 0 0 1 0 0 cm peut être supprimé n'importe où. Dans un objet texte, le tableau est différent. Td, TD, T* et un Tm non identité déplacent tous Tlm, et chaque opérateur d'affichage de texte (Tj, TJ, ', ") avance Tm de la largeur des glyphes qu'il a peints. Après n'importe lequel d'entre eux, un Tm identité est une vraie remise à zéro vers l'origine. Si vous avez déjà tracé des positions de texte à la main avec le suivi d'état CTM et matrice de texte des flux de contenu, c'est la même distinction entre concaténer un état et le remplacer
Comment le scan arrière décide quel Tm identité jeter
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices marche maintenant à reculons depuis chaque Tm identité et ne le jette que si le scan atteint BT ou un autre Tm identité en premier. Le Tm identité antérieur compte, qu'il soit gardé ou qu'il vienne lui-même d'être programmé pour suppression, car dans les deux cas il a laissé les deux matrices à l'identité, exactement comme BT. La règle range chaque opérateur qu'elle peut rencontrer dans l'un de deux groupes :
- S'arrêter et garder le Tm :
Td,TD,T*, unTmnon identité,Tj,TJ,',",ET, tout opérateur que le parseur ne reconnaît pas, ou le début du flux - Passer outre et continuer à scanner : les opérateurs qui ne touchent jamais Tm ni Tlm, tels que
Tf,Tc, les réglages de couleur,gs, les opérateurs de contenu marqué etcm
Les cas conservateurs sont délibérés. Un opérateur inconnu pourrait être n'importe quoi, donc le scan refuse de raisonner au-delà. ET ferme l'objet texte, donc un Tm après lui n'a aucun BT pour plaider les valeurs de matrice. Le scan travaille aussi sur un flux de contenu à la fois, ce qui compte pour les pages dont /Contents est un tableau : une couche qui démarre au milieu d'un objet texte, sans BT à elle, garde son Tm identité même si la couche précédente l'aurait rendu redondant. Ça coûte quelques octets sur des fichiers bizarres et ne déplace jamais un glyphe. Si vous éditez du texte de page au niveau des instructions, comme dans le parcours du mapping caractère-vers-octet de contenu, c'est le même modèle TPDFContentProgram parsé que l'optimiseur réécrit
uses
PDFlibContentModel, PDFlibContentOptimize;
function OptimizeSnippet(const Source: AnsiString): AnsiString;
var
Prog: TPDFContentProgram;
Optimizer: TPDFContentPeepholeOptimizer;
begin
Result := Source;
Prog := TPDFContentProgram.Create;
try
if not Prog.Parse(Source) then
Exit; // flux endommagé : on laisse les octets tranquilles
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // renvoie le nombre d'instructions retirées
finally
Optimizer.Free;
end;
Result := Prog.Emit; // une instruction par ligne
finally
Prog.Free;
end;
end;
// Retiré : Tm juste après BT, le second de deux Tm identité d'affilée
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Gardé : Tm après Td, après Tj, après un Tm non identité, ou hors BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Faites tourner le helper sur le flux de la facture du début et le Tm identité survit, parce que le scan arrière bute sur Tj avant d'atteindre BT. Placez /F1 12 Tf, 2 Tc et 0 g entre BT et le Tm identité et il part quand même, car rien de tout cela ne touche les matrices de texte. Une séquence telle que BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm perd exactement un opérateur : le premier Tm identité remet à zéro la matrice que Td avait déplacée, et seul le second est redondant
Quand l'optimiseur peephole tourne-t-il réellement ?
L'optimiseur ne tourne que pendant la passe de compression, dans TPDFPageTree.Compress, et seulement sur les flux de contenu qui ne sont pas déjà compressés en Flate. TPDFlib.SetOptimizeContentStreams(1) est la valeur par défaut, et le même interrupteur est exposé comme champ OptimizeContentStreams de TPDFlibSaveOptions ; CompressContent et CompressPage l'honorent tous les deux. Un flux dont le /Filter est déjà /FlateDecode est carrément sauté, donc charger un PDF compressé existant et le sauvegarder à nouveau ne réécrit pas ses opérateurs. Si le flux refuse de se parser, les octets décodés d'origine sont compressés tels quels. TPDFlib.NormalizeContentStreams parse et réémet le contenu avec espacement et nombres canoniques mais n'appelle jamais l'optimiseur, ce qui en fait une base utile quand vous voulez voir combien les règles peephole contribuent au gain de taille, à côté des gains plus gros couverts dans l'optimisation de taille des fichiers PDF avec sous-ensembles de polices
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Les flux non compressés passent par les règles peephole, puis Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Le même choix via les options de sauvegarde groupées ; False désactive
Options.CompressContent := True;
Options.CompressFonts := True;
Options.CompressImages := True;
Options.Linearize := False;
Options.KeepModDate := False;
Options.OptimizeContentStreams := False;
Options.GarbageCollect := False;
Options.PackObjectStreams := True;
Lib.SaveToFileOptions('report-plain.pdf', Options);
finally
Lib.Free;
end;
end;
Que garantissait réellement l'ancien test de régression ?
L'ancien test de régression ne garantissait qu'une seule forme : un Tm identité juste après BT est retiré. Peephole_RemovesIdentityTextMatrix passe BT 1 0 0 1 0 0 Tm (hello) Tj ET à l'optimiseur et affirme qu'il ne reste aucun Tm. Une version antérieure avait déjà noté que jeter un Tm identité n'est pas sûr quand Tlm n'est pas l'identité, puis avait gardé le comportement quand même parce que le test le « verrouillait ». Relu avec attention, le test ne dit rien d'un Tm identité après Td ou après du texte affiché ; traiter la couverture d'un échantillon comme le contrat de toute la règle était la vraie erreur. La correction garde ce cas d'origine au vert et ajoute six cas qui épinglent à la fois les formes amovibles et les formes gardées, dont un Tm hors de tout objet texte et un qui suit ET
Le compromis est facile à accepter une fois écrit. Les générateurs qui emballent chaque objet texte en BT 1 0 0 1 0 0 Tm ... se font toujours retirer cet opérateur redondant, et c'est de là que venait presque tout le gain. Ce à quoi l'optimiseur renonce, c'est l'occasionnel Tm identité au milieu d'un objet texte, une poignée d'octets par page avant même que Flate les voie, en échange d'une garantie que l'en-tête du module énonce clairement : chaque transformation est équivalente en sortie et ne change jamais la page visible. Un optimiseur de taille qui déplace du texte n'est pas un optimiseur, c'est un bug de rendu avec de bons ratios de compression
Le parseur de flux de contenu, l'optimiseur peephole et les options de compression à la sauvegarde décrits ici sont livrés dans la PDF Library for Delphi and C++Builder, qui expose aussi NormalizeContentStreams, CompressContent et TPDFlibSaveOptions pour régler l'écriture de chaque document