Die PDF Library for Delphi entfernt einen Identity-Textmatrix-Operator, 1 0 0 1 0 0 Tm, während der Peephole-Optimierung des Content-Streams zur Speicherzeit nur dann, wenn die Textmatrix und die Textzeilenmatrix bereits die Identity sind: direkt nach BT oder direkt nach einem früheren Identity-Tm. Ein Identity-cm wird nach wie vor immer entfernt, denn cm multipliziert die CTM, während Tm beide Textmatrizen komplett ersetzt. Seit v3.539.28 bleibt jedes andere Identity-Tm im Stream
Der Bug, den das behebt, ist die stille Sorte. Ein Reportgenerator emittiert BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET und verlässt sich darauf, dass das Identity-Tm den zweiten String zurück zum Ursprung des Textspace schickt, bevor seine eigene Positionierlogik greift. Der ältere Optimizer sah sechs Zahlen, die die Identity-Matrix buchstabieren, entschied, der Operator könne unmöglich etwas ändern, und löschte ihn. Nichts schlug fehl, nichts loggte eine Warnung, und die gespeicherte Seite zeichnete „Total“ unmittelbar nach „Invoice“ auf derselben Grundlinie — genau die Sorte Defekt, die niemand bemerkt, bis ein Kunde die PDF ausdruckt
Warum ist 1 0 0 1 0 0 Tm nicht immer ein No-op?
Ein Identity-Tm ist nur dann ein No-op, wenn es zwei Matrizen ersetzen würde, die bereits die Identity halten, und das ist eine Eigenschaft der Operatoren davor, nicht seiner eigenen Operanden. ISO 32000-1 §9.4.1 sagt, dass BT sowohl die Textmatrix (Tm) als auch die Textzeilenmatrix (Tlm) auf die Identity initialisiert, und §9.4.2 definiert Tm als Setzen beider auf die angegebenen Werte, nicht als Verkettung darauf. Vergleichen Sie das mit cm (§8.4.4), das die aktuelle Transformationsmatrix rechtsmultipliziert: Multiplikation mit der Identity lässt jede CTM unverändert, 1 0 0 1 0 0 cm ist also überall gefahrlos löschbar. Innerhalb eines Textobjekts sieht die Lage anders aus. Td, TD, T* und ein Nicht-Identity-Tm verschieben alle Tlm, und jeder Textanzeige-Operator (Tj, TJ, ', ") rückt Tm um die Breite der gemalten Glyphen weiter. Danach ist ein Identity-Tm ein echter Reset zum Ursprung. Wenn Sie Textpositionen schon einmal von Hand mit dem Content-Stream-CTM- und Textmatrix-State-Tracker nachverfolgt haben, ist das dieselbe Unterscheidung zwischen verkettetem und ersetztem Zustand
Wie der Rückwärtsscan entscheidet, welches Identity-Tm fällt
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices läuft jetzt von jedem Identity-Tm rückwärts und verwirft es nur, wenn der Scan zuerst BT oder ein anderes Identity-Tm erreicht. Das frühere Identity-Tm zählt dabei, egal ob es behalten wurde oder selbst gerade zur Löschung vorgemerkt war, denn in beiden Fällen ließ es beide Matrizen auf der Identity, genau wie BT. Die Regel sortiert jeden Operator, dem sie begegnen kann, in eine von zwei Gruppen:
- Stoppen und Tm behalten:
Td,TD,T*, ein Nicht-Identity-Tm,Tj,TJ,',",ET, jeder Operator, den der Parser nicht kennt, oder der Anfang des Streams - Weitersteppen und weiterscannen: Operatoren, die Tm und Tlm nie anfassen, etwa
Tf,Tc, Farb-Setter,gs, Marked-Content-Operatoren undcm
Die konservativen Fälle sind Absicht. Ein unbekannter Operator könnte alles Mögliche sein, also weigert sich der Scan, über ihn hinweg zu schließen. ET schließt das Textobjekt, einem Tm danach bürgt also kein BT für die Matrixwerte. Der Scan arbeitet außerdem immer nur an einem Content-Stream, was für Seiten zählt, deren /Contents ein Array ist: Eine Ebene, die mitten in einem Textobjekt beginnt und kein eigenes BT hat, behält ihr Identity-Tm, selbst wenn die vorherige Ebene es redundant gemacht hätte. Das kostet auf seltsamen Dateien ein paar Bytes und verschiebt nie eine Glyphe. Wenn Sie Seitentext auf Instruktionsebene editieren, wie im Character-to-Content-Byte-Mapping-Walkthrough, ist dasselbe geparste TPDFContentProgram-Modell das, was der Optimizer umschreibt
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; // beschädigter Stream: die Bytes in Ruhe lassen
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // liefert die Anzahl der entfernten Instruktionen
finally
Optimizer.Free;
end;
Result := Prog.Emit; // eine Instruktion pro Zeile
finally
Prog.Free;
end;
end;
// Entfernt: Tm direkt nach BT, die zweite von zwei Identity-Tm in Folge
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Behalten: Tm nach Td, nach Tj, nach einem Nicht-Identity-Tm oder außerhalb von BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Setzt man den Helfer auf den Invoice-Stream aus der Einleitung an, überlebt das Identity-Tm, denn der Rückwärtsscan trifft auf Tj, bevor er BT erreicht. Schieben Sie /F1 12 Tf, 2 Tc und 0 g zwischen BT und das Identity-Tm, fällt es trotzdem, denn keines davon fasst die Textmatrizen an. Eine Sequenz wie BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm verliert exakt einen Operator: Das erste Identity-Tm setzt die Matrix zurück, die Td verschoben hat, und nur das zweite ist redundant
Wann läuft der Peephole-Optimizer überhaupt?
Der Optimizer läuft nur im Kompressionsdurchlauf, innerhalb von TPDFPageTree.Compress, und nur auf Content-Streams, die nicht bereits Flate-komprimiert sind. TPDFlib.SetOptimizeContentStreams(1) ist der Default, derselbe Schalter liegt auch als OptimizeContentStreams-Feld von TPDFlibSaveOptions offen; sowohl CompressContent als auch CompressPage ehren ihn. Ein Stream, dessen /Filter bereits /FlateDecode ist, wird komplett übersprungen — eine vorhandene komprimierte PDF laden und erneut speichern schreibt ihre Operatoren also nicht um. Scheitert das Parsen des Streams, werden die originalen dekodierten Bytes unverändert komprimiert. TPDFlib.NormalizeContentStreams parst Content und emittiert ihn mit kanonischen Abständen und Zahlen, ruft den Optimizer aber nie auf, was es zu einem nützlichen Baseline-Vergleich macht, wenn Sie sehen wollen, wie viel Größendifferenz die Peephole-Regeln beitragen — neben den größeren Gewinnen aus PDF-Dateigrößenoptimierung mit Font-Subsetting
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Unkomprimierte Streams laufen durch die Peephole-Regeln, dann durch Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Dieselbe Wahl über die gebündelten Save-Options; False meldet ab
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;
Was hat der alte Regressionstest wirklich garantiert?
Der alte Regressionstest garantierte genau eine Form: Ein Identity-Tm direkt nach BT wird entfernt. Peephole_RemovesIdentityTextMatrix füttert den Optimizer mit BT 1 0 0 1 0 0 Tm (hello) Tj ET und stellt sicher, dass kein Tm übrig bleibt. Ein früheres Release hatte bereits vermerkt, dass das Entfernen eines Identity-Tm unsicher ist, wenn Tlm nicht die Identity ist, und das Verhalten trotzdem beibehalten, weil der Test es „einfroren“ hatte. Aufmerksam gelesen sagt der Test nichts über ein Identity-Tm nach Td oder nach angezeigtem Text; die Abdeckung eines einzelnen Samples als Vertrag der ganzen Regel zu behandeln, war der eigentliche Fehler. Der Fix lässt diesen ursprünglichen Fall weiterlaufen und ergänzt sechs Fälle, die sowohl die entfernbaren als auch die behaltenen Formen festnageln, darunter ein Tm außerhalb jedes Textobjekts und eines, das auf ET folgt
Der Trade-off ist leicht zu akzeptieren, einmal aufgeschrieben. Generatoren, die jedes Textobjekt als BT 1 0 0 1 0 0 Tm ... verpacken, bekommen diesen redundanten Operator weiterhin entfernt — daher kam praktisch die gesamte Ersparnis. Worauf der Optimizer verzichtet, ist das gelegentliche Identity-Tm mitten im Textobjekt, eine Handvoll Bytes pro Seite, bevor Flate sie überhaupt sieht, im Tausch gegen eine Garantie, die der Modul-Header klar ausspricht: Jede Transformation ist ausgabeäquivalent und ändert nie die sichtbare Seite. Ein Größen-Optimizer, der Text verschiebt, ist kein Optimizer, sondern ein Rendering-Bug mit guten Kompressionsraten
Der Content-Stream-Parser, der Peephole-Optimizer und die Kompressionsoptionen zur Speicherzeit, die hier beschrieben werden, kommen alle in der PDF Library for Delphi and C++Builder, die auch NormalizeContentStreams, CompressContent und TPDFlibSaveOptions offenlegt, um zu tunen, wie jedes einzelne Dokument geschrieben wird