PDF Library for Delphi fjerner en identity text matrix-operator, 1 0 0 1 0 0 Tm, under sin peephole-optimering af content streams ved gemning, kun når text matrix og text line matrix allerede er identity: lige efter BT eller lige efter en tidligere identity-Tm. En identity-cm dropes derimod stadig altid, fordi cm multiplicerer CTM'en, mens Tm erstatter begge text matrices fuldstændigt. Siden v3.539.28 forbliver alle andre identity-Tm i streamen
Fejlen, dette fixer, er den stille slags. En rapportgenerator udsender BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET og regner med, at identity-Tm'en sender den anden streng tilbage til text-space-origen, før den anvender sin egen positioneringslogik. Den ældre optimizer så seks tal, der staver identitetsmatricen, besluttede, at operatoren umuligt kunne ændre noget, og slettede den. Intet fejlede, intet loggede en advarsel, og den gemte side tegnede "Total" umiddelbart efter "Invoice" på samme baseline, hvilket præcis er den defektklasse, ingen bemærker, før en kunde printer PDF'en
Hvorfor er 1 0 0 1 0 0 Tm ikke altid en no-op?
En identity-Tm er kun en no-op, når den ville erstatte to matricer, der allerede holder identiteten, og det er en egenskab ved operatørerne før den, ikke ved dens egne operander. ISO 32000-1 §9.4.1 siger, at BT initialiserer både text matrix (Tm) og text line matrix (Tlm) til identiteten, og §9.4.2 definerer Tm som at sætte begge til de givne værdier, ikke konkateneret oven på dem. Sammenlign med cm (§8.4.4), som right-multiplicerer den aktuelle transformationsmatrix: at multiplicere med identiteten efterlader enhver CTM uændret, så 1 0 0 1 0 0 cm kan sikkert slettes hvor som helst. Inde i et tekstobjekt er billedet anderledes. Td, TD, T* og en ikke-identity Tm flytter alle Tlm, og hver tekstvisende operator (Tj, TJ, ', ") rykker Tm frem med bredden af de glyffer, den tegnede. Efter en af dem er en identity-Tm en ægte nulstilling til origo. Hvis du nogensinde har sporet tekstpositioner i hånden med content-stream CTM- og text matrix state tracker, er det samme skel mellem at konkateneret tilstand og erstatte den
Sådan beslutter den baglæns scan, hvilken identity Tm der droppes
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices går nu baglæns fra hver identity Tm og dropper den kun, hvis scanneen når BT eller en anden identity Tm først. Den tidligere identity Tm tæller uanset, om den beholdes eller selv lige er planlagt til sletning, for uanset hvad efterlod den begge matricer ved identiteten, præcis som BT gør. Reglen sorterer hver operator, den kan møde, i én af to grupper:
- Stop og behold Tm'en:
Td,TD,T*, en ikke-identityTm,Tj,TJ,',",ET, enhver operator, parseren ikke genkender, eller streamens start - Tråd over og fortsæt scanningen: operatorer, der aldrig rører Tm eller Tlm, som
Tf,Tc, farvesættere,gs, marked-content-operatorer ogcm
De konservative tilfælde er bevidste. En ukendt operator kunne være hvad som helst, så scanneen nægter at ræsonnere forbi den. ET lukker tekstobjektet, så en Tm efter den har ingen BT, der går ind for matrixværdierne. Scanningen arbejder også på én content stream ad gangen, hvilket betyder noget for sider, hvis /Contents er et array: et lag, der starter midt i et tekstobjekt uden egen BT, beholder sin identity Tm, selv når det forrige lag ville have gjort den overflødig. Det koster et par bytes på mærkelige filer og flytter aldrig en glyf. Hvis du redigerer sidetekst på instruktionsniveau, som i gennemgangen af tegn-til-content-byte-mapping, er det samme parsede TPDFContentProgram-model, optimizeren omskriver
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; // ødelagt stream: lad bytesene være
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // returnerer antallet af fjernede instruktioner
finally
Optimizer.Free;
end;
Result := Prog.Emit; // én instruktion pr. linje
finally
Prog.Free;
end;
end;
// Fjernet: Tm direkte efter BT, den anden af to identity Tm i træk
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Beholdt: Tm efter Td, efter Tj, efter et ikke-identity Tm eller uden for BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Kør helperen på faktura-streamen fra indledningen, og identity Tm overlever, fordi den baglæns scan rammer Tj, før den når BT. Sæt /F1 12 Tf, 2 Tc og 0 g mellem BT og identity Tm, og den ryger stadig, da ingen af dem rører text matrices. En sekvens som BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm mister præcis én operator: den første identity Tm nulstiller matricen, som Td flyttede, og kun den anden er overflødig
Hvornår kører peephole-optimizeren faktisk?
Optimizeren kører kun under komprimeringspasset, inde i TPDFPageTree.Compress, og kun på content streams, der ikke allerede er Flate-komprimeret. TPDFlib.SetOptimizeContentStreams(1) er standard, og samme kontakt er eksponeret som OptimizeContentStreams-feltet i TPDFlibSaveOptions; både CompressContent og CompressPage overholder den. En stream, hvis /Filter allerede er /FlateDecode, springes helt over, så indlæsning af en eksisterende komprimeret PDF og gemning igen ikke omskriver dens operatorer. Hvis streamen ikke kan parses, komprimeres de oprindelige dekodede bytes uændret. TPDFlib.NormalizeContentStreams parser og genudsender content med kanonisk mellemrum og tal, men kalder aldrig optimizeren, hvilket gør den til et nyttigt baseline, når du vil se, hvor stor en størrelsesforskel peephole-reglerne bidrager med, ud over de større gevinster, der er dækket i PDF-filstørrelsesoptimering med font subsetting
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Ukomprimerede streams går gennem peephole-reglerne, derefter Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Samme valg gennem de medfølgende save options; False takker nej
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;
Hvad garanterede den gamle regressionstest egentlig?
Den gamle regressionstest garanterede kun én form: en identity Tm direkte efter BT fjernes. Peephole_RemovesIdentityTextMatrix fodrer BT 1 0 0 1 0 0 Tm (hello) Tj ET til optimizeren og assert'er, at ingen Tm resterer. En tidligere udgivelse havde allerede bemærket, at det er usikkert at droppe en identity Tm, når Tlm ikke er identiteten, og beholdt adfærden alligevel, fordi testen "låste" den. Læst omhyggeligt siger testen intet om en identity Tm efter Td eller efter vist tekst; at behandle dækningen af ét eksempel som hele regelens kontrakt var den egentlige fejl. Fixet holder det oprindelige tilfælde kørende og tilføjer seks tilfælde, der låser både de fjernbare former og de beholdte fast, inklusive en Tm uden for ethvert tekstobjekt og en, der følger efter ET
Kompromisset er let at acceptere, når det først er skrevet ned. Generatorer, der pakker hvert tekstobjekt ind som BT 1 0 0 1 0 0 Tm ..., får stadig den overflødige operator fjernet, og dér kom næsten alle besparelserne fra. Det, optimizeren giver afkald på, er den lejlighedsvise identity Tm midt i et tekstobjekt — en håndfuld bytes pr. side, før Flate overhovedet ser dem — til gengæld for en garanti, som module-headeren siger lige ud: hver transformation er output-ækvivalent og ændrer aldrig den synlige side. En størrelsesoptimizer, der flytter tekst, er ikke en optimizer, det er en rendering-fejl med gode kompressionsrater
Content-stream-parseren, peephole-optimizeren og komprimeringsindstillingerne ved gemning, der er beskrevet her, følger alle med i PDF Library for Delphi and C++Builder, som også eksponerer NormalizeContentStreams, CompressContent og TPDFlibSaveOptions til at tune, hvordan hvert dokument skrives