Teknisk artikel

Identity Tm i PDF content streams: Sikker peephole-fjernelse

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

PDFlibPas behandler 1 0 0 1 0 0 cm og 1 0 0 1 0 0 Tm forskelligt: cm right-multiplicerer CTM'en og er en no-op hvor som helst, mens Tm erstatter Tm og Tlm fuldstændigt, og hver Tj rykker Tm frem med den bredde, den tegnede, så en identity Tm efter vist tekst er en ægte nulstilling
Rapportgeneratoren regnede med den nulstilling: at slette identity Tm tegnede Total umiddelbart efter Invoice på samme baseline, og intet fejlede, loggede eller advarede på vejen til kundens printer

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-identity Tm, 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 og cm
PDFlibPas RemoveIdentityMatrices går baglæns fra hver identity Tm: Tf, Tc, farvesættere, gs og cm trådes over, mens Td, TD, T*, en ikke-identity Tm, Tj, TJ, en ukendt operator eller ET standser scanneen og beholder Tm'en, og BT går ind for at droppe den
En tidligere identity Tm standser også scanneen, for uanset om den beholdes eller allerede er planlagt til sletning, efterlod den begge matricer ved identiteten — uanset hvad flytter optimizeren aldrig en glyf

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

PDFlibPas kører peephole-optimizeren kun inde i komprimeringspasset ved gemning: TPDFPageTree.Compress overholder SetOptimizeContentStreams, en stream, der allerede er filtreret med /FlateDecode, springes helt over, en uparsbar stream komprimeres med sine oprindelige bytes uændret, og NormalizeContentStreams kalder slet ikke optimizeren
De oversprungne komprimerede streams er den stille del: indlæs en eksisterende PDF, gem den igen, og dens operatorer kommer ud urørte, for optimizeren omskriver kun streams, den først har dekodet
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