Tehnični članak

Enotski Tm v vsebini PDF: varna peephole odstranitev

PDF Library for Delphi odstrani enotski operator tekstovne matrike, 1 0 0 1 0 0 Tm, med peephole optimizacijo tokov vsebine ob shranjevanju le, kadar sta tekstovna matrika in tekstovna matrika vrstice že enotski: takoj za BT ali takoj za prejšnjim enotskim Tm. Enotski cm se še naprej vedno odvrže, ker cm množi CTM, Tm pa obe tekstovni matriki prepiše kar v celoti. Od v3.539.28 vsak drugi enotski Tm ostane v toku

Hrošč, ki ga to popravlja, je tihe vrste. Generator poročil izda BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET in se zanaša na enotski Tm, da pošlje drugi niz nazaj na izhodišče tekstovnega prostora, preden uveljavi lastno logiko pozicioniranja. Starejši optimizator je videl šest števil, ki črkujejo enotsko matriko, sklenil, da operator ne more ničesar spremeniti, in ga izbrisal. Nič ni odpovedalo, nič ni zabeležilo opozorila, shranjena stran pa je narisala »Total« takoj za »Invoice« na isti osnovni črti — točno tista vrsta napake, ki je nihče ne opazi, dokler stranka ne natisne PDF

Zakaj 1 0 0 1 0 0 Tm ni vedno no-op?

Enotski Tm je no-op le, kadar bi zamenjal dve matriki, ki že nosita enoto, to pa je lastnost operatorjev pred njim, ne njegovih lastnih operandov. ISO 32000-1 §9.4.1 pravi, da BT inicializira tekstovno matriko (Tm) in tekstovno matriko vrstice (Tlm) na enoto, §9.4.2 pa definira Tm kot nastavitev obeh na dane vrednosti, ne zlivanja nanje. Primerjajte s cm (§8.4.4), ki trenutno transformacijsko matriko množi z desne: množenje z enoto pusti vsak CTM nespremenjen, zato je 1 0 0 1 0 0 cm varno izbrisati kjerkoli. Znotraj tekstovnega objekta je slika drugačna. Td, TD, T* in ne-enotski Tm vsi premaknejo Tlm, vsak operator prikazovanja teksta (Tj, TJ, ', ") pa poveča Tm za širino narisanih glifov. Po katerem koli od njih je enotski Tm pravi povratek na izhodišče. Če ste kdaj ročno sledili položajem teksta z sledilnikom stanja CTM in tekstovne matrike v toku vsebine, je to ista razlika med zlivanjem stanja in njegovo zamenjavo

PDFlibPas ravna z 1 0 0 1 0 0 cm in 1 0 0 1 0 0 Tm različno: cm množi CTM z desne in je no-op kjerkoli, Tm pa Tm in Tlm prepiše v celoti, vsak Tj pa poveča Tm za narisano širino, tako da je enotski Tm po prikazanem tekstu pravi povratek na izhodišče
Generator poročil se je zanašal na ta povratek: brisanje enotskega Tm je narisalo Total takoj za Invoice na isti osnovni črti, nič pa ni odpovedalo, zabeležilo ali opozorilo na poti do tiskalnika stranke

Kako povratni pregled odloča, kateri enotski Tm odvrže

TPDFContentPeepholeOptimizer.RemoveIdentityMatrices zdaj hodi od vsakega enotskega Tm nazaj in ga odvrže le, če pregled najprej doseže BT ali drug enotski Tm. Prejšnji enotski Tm šteje ne glede na to, ali je ohranjen ali je bil sam ravnokar določen za brisanje, ker je v obeh primerih pustil obe matriki na enoti, točno tako kot BT. Pravilo vsakega operatorja, ki ga lahko sreča, razvrsti v eno od dveh skupin:

  • Ustavitev in ohranitev Tm: Td, TD, T*, ne-enotski Tm, Tj, TJ, ', ", ET, vsak operator, ki ga razčlenjevalnik ne prepozna, ali začetek toka
  • Preskok in nadaljevanje pregleda: operatorji, ki se Tm ali Tlm nikoli ne dotaknejo, na primer Tf, Tc, nastavitve barv, gs, operatorji označene vsebine in cm
PDFlibPas RemoveIdentityMatrices hodi nazaj od vsakega enotskega Tm: Tf, Tc, nastavitve barv, gs in cm se preskočijo, Td, TD, T*, ne-enotski Tm, Tj, TJ, neznan operator ali ET pa ustavi pregled in ohrani Tm, BT pa jamči za njegovo odstranitev
Tudi prejšnji enotski Tm ustavi pregled, ker je ohranjen ali že določen za brisanje pustil obe matriki na enoti — kakor koli optimizator nikoli ne premakne glifa

Konzervativni primeri so namerni. Neznan operator je lahko karkoli, zato pregled odkloni sklepanje čez njega. ET zapre tekstovni objekt, zato Tm za njim nima BT, ki bi jamčil za vrednosti matrik. Pregled dela tudi na enem toku vsebine naenkrat, kar je pomembno za strani, katerih /Contents je polje: plast, ki se začne na sredini tekstovnega objekta, brez lastnega BT, obdrži svoj enotski Tm, tudi če bi jo prejšnja plast naredila odvečno. To stane nekaj bajtov na čudnih datotekah in nikoli ne premakne glifa. Če urejate tekst strani na ravni ukazov, kot v vodniku o preslikavi znakov v bajte vsebine, je isti razčlenjeni model TPDFContentProgram tisto, kar optimizator prepiše

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; // pokvarjen tok: bajti ostanejo pri miru
    Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
    try
      Optimizer.Run; // vrne število odstranjenih ukazov
    finally
      Optimizer.Free;
    end;
    Result := Prog.Emit; // en ukaz na vrstico
  finally
    Prog.Free;
  end;
end;

// Odstranjeno: Tm takoj za BT, drugi od dveh enotskih Tm zapored
//   OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Ohranjeno: Tm za Td, za Tj, za ne-enotskim Tm ali zunaj BT
//   OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')

Pomožnik poženite na toku računa iz uvoda in enotski Tm preživi, ker povratni pregled naleti na Tj, preden doseže BT. Postavite /F1 12 Tf, 2 Tc in 0 g med BT in enotski Tm ter tudi tak gre, ker se noben od njih ne dotakne tekstovnih matrik. Zaporedje, kot je BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm, izgubi točno enega operatorja: prvi enotski Tm ponastavi matriko, ki jo je premaknil Td, odvečen pa je samo drugi

Kdaj peephole optimizator sploh teče?

Optimizator teče le med prehodom stiskanja, znotraj TPDFPageTree.Compress, in le na tokovih vsebine, ki še niso Flate stisnjeni. TPDFlib.SetOptimizeContentStreams(1) je privzeto, isto stikalo pa je izpostavljeno kot polje OptimizeContentStreams v TPDFlibSaveOptions; upoštevata ga tako CompressContent kot CompressPage. Tok, katerega /Filter je že /FlateDecode, se v celoti preskoči, zato nalaganje obstoječega stisnjenega PDF in ponovno shranjevanje ne prepisuje njegovih operatorjev. Če toka ni mogoče razčleniti, se izvirni dekodirani bajti stisnejo nespremenjeni. TPDFlib.NormalizeContentStreams razčleni in znova izda vsebino s kanoničnim razmakom in števili, optimizatorja pa nikoli ne pokliče, zato je uporabna izhodiščna črta, kadar želite videti, koliko velikostne razlike prispevajo peephole pravila, poleg večjih dobičkov, pokritih v optimizaciji velikosti datotek PDF s podnabori pisav

PDFlibPas požene peephole optimizator le znotraj stisnjevalnega prehoda ob shranjevanju: TPDFPageTree.Compress upošteva SetOptimizeContentStreams, tok, že filtriran z /FlateDecode, se v celoti preskoči, nerazčlenljiv tok se stisne z izvirnimi bajti nespremenjen, NormalizeContentStreams pa optimizatorja sploh nikoli ne pokliče
Preskočeni stisnjeni tokovi so tihi del: naložite obstoječi PDF, ga shranite znova in njegovi operatorji pridejo ven nedotaknjeni, ker optimizator prepisuje le tokove, ki jih je najprej sam dekodiral
var
  Lib: TPDFlib;
  Options: TPDFlibSaveOptions;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    // Nestisnjeni tokovi gredo skozi peephole pravila, nato Flate
    Lib.SetOptimizeContentStreams(1);
    Lib.CompressContent;
    Lib.SaveToFile('report-optimized.pdf');

    // Isti izbor skozi priložene možnosti shranjevanja; False se odloči proti
    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;

Kaj je stari regresijski preizkus res zagotavljal?

Stari regresijski preizkus je zagotavljal samo eno obliko: enotski Tm takoj za BT se odstrani. Peephole_RemovesIdentityTextMatrix poda BT 1 0 0 1 0 0 Tm (hello) Tj ET optimizatorju in trdi, da noben Tm ne ostane. Zgodnejša izdaja je že opazila, da je odvreči enotski Tm nevarno, kadar Tlm ni enota, obnašanje pa je vseeno ohranila, ker ga je preizkus »zaklenil«. Če ga preberete pozorno, preizkus o enotskem Tm za Td ali za prikazanim tekstom ne pravi nič; obravnavati pokritost enega vzorca kot pogodbo celega pravila je bila prava napaka. Popravek ohrani izvirni primer zelen in doda šest primerov, ki pribijejo odstranljive in ohranjene oblike, vključno s Tm zunaj vsakega tekstovnega objekta in enim, ki sledi ET

Kompromis je lahko sprejeti, ko je enkrat zapisan. Generatorji, ki vsak tekstovni objekt ovijejo kot BT 1 0 0 1 0 0 Tm ..., še vedno dobijo odstranjenega tega odvečnega operatorja, od koder je prišlo skoraj vse privarčevanega. Kar optimizator odstopi, je občasni enotski Tm na sredini tekstovnega objekta, peščica bajtov na stran, preden jih Flate sploh vidi, v zameno za zagotovilo, ki ga glava modula jasno izraža: vsaka transformacija je izhodno enakovredna in nikoli ne spremeni vidne strani. Optimizator velikosti, ki premika tekst, ni optimizator, je izrisovalna napaka z dobrimi razmerji stiskanja

Razčlenjevalnik tokov vsebine, peephole optimizator in možnosti stiskanja ob shranjevanju, opisani tukaj, so vsi del PDF Library for Delphi and C++Builder, ki izpostavlja tudi NormalizeContentStreams, CompressContent in TPDFlibSaveOptions za nastavitev pisanja vsakega dokumenta