Teknisk artikkel

Identity Tm i PDF content streams: trygg peephole-fjerning

PDF Library for Delphi fjerner en identitets-tekstmatrise-operator, 1 0 0 1 0 0 Tm, i sin peephole-optimalisering av content streams ved lagring, bare når tekstmatrisen og tekstlinjematrisen allerede er identiteten: rett etter BT, eller rett etter en tidligere identitets-Tm. En identitets-cm blir fortsatt alltid droppet, for cm multipliserer CTM-en mens Tm erstatter begge tekstmatrisene fullstendig. Siden v3.539.28 blir alle andre identitets-Tm igjen i strømmen

Bug-en denne fikser, er den stille typen. En rapportgenerator sender BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET og stoler på identitets-Tm-en for å sende den andre strengen tilbake til tekstrommets origo før den anvender sin egen posisjoneringslogikk. Den eldre optimalisatoren så seks tall som staver identitetsmatrisen, bestemte at operatoren umulig kunne endre noe, og slettet den. Ingenting feilet, ingenting logget en advarsel, og den lagrede siden tegnet «Total» rett etter «Invoice» på samme grunnlinje, som er nøyaktig den klassen av defekter ingen legger merke til før en kunde skriver ut PDF-en

Hvorfor er 1 0 0 1 0 0 Tm ikke alltid en no-op?

En identitets-Tm er en no-op bare når den ville erstatte to matriser som allerede holder identiteten, og det er en egenskap ved operatorene før den, ikke ved egne operander. ISO 32000-1 §9.4.1 sier at BT initialiserer både tekstmatrisen (Tm) og tekstlinjematrisen (Tlm) til identiteten, og §9.4.2 definerer Tm som å sette begge til de gitte verdiene, ikke å konkatenere på dem. Sammenlign med cm (§8.4.4), som høyremultipliserer den gjeldende transformasjonsmatrisen: å multiplisere med identiteten lar enhver CTM være uendret, så 1 0 0 1 0 0 cm er trygt å slette hvor som helst. Inne i et tekstobjekt er bildet annerledes. Td, TD, T* og en ikke-identitets-Tm flytter alle Tlm, og hver tekstvisende operator (Tj, TJ, ', ") flytter Tm fremover med bredden av glyfene den tegnet. Etter en av dem er en identitets-Tm en ekte tilbakestilling til origo. Hvis du noen gang har sporet tekstposisjoner for hånd med content stream CTM- og tekstmatrise-sporeren, er dette samme skillet mellom å konkatenere tilstand og erstatte den

PDFlibPas behandler 1 0 0 1 0 0 cm og 1 0 0 1 0 0 Tm forskjellig: cm høyremultipliserer CTM-en og er en no-op hvor som helst, mens Tm erstatter Tm og Tlm fullstendig, og hver Tj flytter Tm fremover med bredden den tegnet, så en identitets-Tm etter vist tekst er en ekte tilbakestilling
Rapportgeneratoren regnet med den tilbakestillingen: å slette identitets-Tm-en tegnet Total rett etter Invoice på samme grunnlinje, og ingenting feilet, logget eller advarte på veien til kundens skriver

Hvordan bakover-skanningen bestemmer hvilken identitets-Tm som droppes

TPDFContentPeepholeOptimizer.RemoveIdentityMatrices går nå baklengs fra hver identitets-Tm og dropper den bare hvis skanningen når BT eller en annen identitets-Tm først. Den tidligere identitets-Tm-en teller enten den beholdes eller selv nettopp er planlagt slettet, for uansett etterlot den begge matriser i identiteten, nøyaktig som BT gjør. Regelen sorterer hver operator den kan treffe, inn i én av to grupper:

  • Stopp og behold Tm-en: Td, TD, T*, en ikke-identitets-Tm, Tj, TJ, ', ", ET, enhver operatoren parseren ikke gjenkjenner, eller starten på strømmen
  • Gå videre og fortsett å skanne: operatorer som aldri rører Tm eller Tlm, slik som Tf, Tc, fargesettere, gs, marked-content-operatorer og cm
PDFlibPas RemoveIdentityMatrices går baklengs fra hver identitets-Tm: Tf, Tc, fargesettere, gs og cm steges over, mens Td, TD, T*, en ikke-identitets-Tm, Tj, TJ, en ukjent operator eller ET stopper skanningen og beholder Tm-en, og BT garanterer for å droppe den
En tidligere identitets-Tm stopper også skanningen, for beholdt eller allerede planlagt slettet etterlot den begge matriser i identiteten — uansett flytter optimalisatoren aldri en glyf

De konservative tilfellene er bevisste. En ukjent operator kan være hva som helst, så skanningen nekter å resonnere forbi den. ET lukker tekstobjektet, så en Tm etter den har ingen BT som garanterer for matriseverdiene. Skanningen jobber også med én content stream om gangen, noe som betyr noe for sider hvis /Contents er en array: et lag som starter midt i et tekstobjekt, uten egen BT, beholder sin identitets-Tm selv om forrige lag ville ha gjort den overflødig. Det koster noen byte på rare filer og flytter aldri en glyf. Hvis du redigerer pagetekst på instruksjonsnivå, som i gjennomgangen av tegn-til-content-byte-mapping, er samme parsede TPDFContentProgram-modell det optimalisatoren 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 strøm: la bytene være i fred
    Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
    try
      Optimizer.Run; // returnerer antallet fjernede instruksjoner
    finally
      Optimizer.Free;
    end;
    Result := Prog.Emit; // én instruksjon per linje
  finally
    Prog.Free;
  end;
end;

// Fjernet: Tm rett etter BT, den andre av to identitets-Tm på rad
//   OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Beholdt: Tm etter Td, etter Tj, etter en ikke-identitets-Tm eller utenfor BT
//   OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')

Kjør hjelperen på fakturastrømmen fra innledningen, så overlever identitets-Tm-en, for bakover-skanningen treffer Tj før den når BT. Sett /F1 12 Tf, 2 Tc og 0 g mellom BT og identitets-Tm-en, så forsvinner den likevel, siden ingen av dem rører tekstmatrisene. En sekvens som BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm mister nøyaktig én operator: den første identitets-Tm-en tilbakestiller matrisen som Td flyttet, og bare den andre er overflødig

Når kjører peephole-optimalisatoren egentlig?

Optimalisatoren kjører bare under kompresjonspasset, inne i TPDFPageTree.Compress, og bare på content streams som ikke allerede er Flate-komprimert. TPDFlib.SetOptimizeContentStreams(1) er standardverdien, og samme bryter er eksponert som OptimizeContentStreams-feltet i TPDFlibSaveOptions; både CompressContent og CompressPage respekterer den. En strøm hvis /Filter allerede er /FlateDecode hoppes helt over, så å laste en eksisterende komprimert PDF og lagre den på nytt omskriver ikke operatorene. Feiler strømmen å parse, komprimeres de opprinnelige dekodede bytene uendret. TPDFlib.NormalizeContentStreams parser og sender ut content med kanonisk mellomrom og tall, men kaller aldri optimalisatoren, noe som gjør den til et nyttig utgangspunkt når du vil se hvor mye av størrelsesforskjellen peephole-reglene bidrar med, ved siden av de større gevinstene dekket i PDF-filstørrelse-optimalisering med font subsetting

PDFlibPas kjører peephole-optimalisatoren bare inne i kompresjonspasset ved lagring: TPDFPageTree.Compress respekterer SetOptimizeContentStreams, en strøm allerede filtrert med /FlateDecode hoppes helt over, en uparsbar strøm komprimeres med sine opprinnelige byte uendret, og NormalizeContentStreams kaller aldri optimalisatoren
Oversprungne komprimerte strømmer er den stille delen: last en eksisterende PDF, lagre den igjen, og operatorene kommer ut urørt fordi optimalisatoren bare omskriver strømmer den dekodet først
var
  Lib: TPDFlib;
  Options: TPDFlibSaveOptions;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    // Ukomprimerte strømmer går gjennom peephole-reglene, så Flate
    Lib.SetOptimizeContentStreams(1);
    Lib.CompressContent;
    Lib.SaveToFile('report-optimized.pdf');

    // Samme valg gjennom de medfølgende lagringsvalgene; False takker nei
    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;

Hva garanterte den gamle regresjonstesten egentlig?

Den gamle regresjonstesten garanterte én form alene: en identitets-Tm rett etter BT fjernes. Peephole_RemovesIdentityTextMatrix mater BT 1 0 0 1 0 0 Tm (hello) Tj ET til optimalisatoren og hevder at ingen Tm gjenstår. En tidligere utgivelse hadde allerede bemerket at å droppe en identitets-Tm er utrygt når Tlm ikke er identiteten, og beholdt likevel oppførselen fordi testen «låste» den. Lest nøye, sier testen ingenting om en identitets-Tm etter Td eller etter vist tekst; å behandle dekningen av én prøve som kontrakten til hele regelen var selve feilen. Fiksen holder det opprinnelige tilfellet grønt og legger til seks tilfeller som fester både de fjernbare formene og de beholdte, inkludert en Tm utenfor ethvert tekstobjekt og én som følger ET

Avveiningen er lett å akseptere når den er skrevet ned. Generatorer som pakker hvert tekstobjekt som BT 1 0 0 1 0 0 Tm ... får fortsatt den overflødige operatoren fjernet, og det er der nesten alle besparelsene kom fra. Det optimalisatoren gir slipp på, er den innimellom-identitets-Tm-en i et tekstobjekt, en håndfull byte per side før Flate engang ser dem, i bytte mot en garanti modulhodet sier rent ut: hver transform er output-ekvivalent og endrer aldri den synlige siden. En størrelsesoptimalisator som flytter tekst, er ikke en optimalisator, det er en rendering-bug med gode kompresjonsrater

Content stream-parseren, peephole-optimalisatoren og lagringstidens kompresjonsvalg omtalt her følger alle med i PDF Library for Delphi og C++Builder, som også eksponerer NormalizeContentStreams, CompressContent og TPDFlibSaveOptions for å styre hvordan hvert dokument skrives