Tehnički članak

Identity Tm u content streamu: sigurno peephole uklanjanje

PDF Library for Delphi briše operator identiteta text matrice, 1 0 0 1 0 0 Tm, tijekom svoje peephole optimizacije content streama pri spremanju samo kad su text matrix i text line matrix već identitet: odmah nakon BT ili odmah nakon ranijeg identity Tm. Identity cm i dalje se uvijek briše, jer cm množi CTM dok Tm objema text matricama u cijelosti zamijeni vrijednost. Od v3.539.28 svaki drugi identity Tm ostaje u streamu

Bug koji ovo popravlja onaj je tih. Generator izvještaja ispušta BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, oslanjajući se na identity Tm da drugi string vrati na ishodište text spacea prije nego primijeni vlastitu logiku pozicioniranja. Stariji optimizer vidio je šest brojeva koji ispisuju identitetnu matricu, zaključio da operator ne može ništa promijeniti i izbrisao ga. Ništa nije palo, ništa nije zapisalo upozorenje, a spremljena je stranica nacrtala „Total" odmah nakon „Invoice" na istoj baselini, a to je upravo vrsta defekta koju nitko ne primijeti dok kupac ne ispiše PDF

Zašto 1 0 0 1 0 0 Tm nije uvijek no-op?

Identity Tm no-op je samo kad bi zamijenio dvije matrice koje već drže identitet, a to je svojstvo operatora ispred njega, ne njegovih vlastitih operanada. ISO 32000-1 §9.4.1 kaže da BT inicijalizira i text matrix (Tm) i text line matrix (Tlm) na identitet, a §9.4.2 definira Tm kao postavljanje obiju na dane vrijednosti, ne nadovezivanje na njih. Usporedite s cm (§8.4.4), koji mnoši s desna trenutnu transformacijsku matricu: množenje identitetom svaki CTM ostavlja nepromijenjenim, pa je 1 0 0 1 0 0 cm sigurno brisati bilo gdje. Unutar text objekta slika je drugačija. Td, TD, T* i ne-identity Tm svi miču Tlm, a svaki text-showing operator (Tj, TJ, ', ") pomakne Tm za širinu glifova koje je iscrtao. Nakon bilo kojeg od njih identity Tm pravi je reset na ishodište. Ako ste ikad ručno pratili pozicije teksta s trackerom stanja CTM-a i text matrice content streama, ovo je ista razlika između nadovezivanja stanja i njegove zamjene

PDFlibPas 1 0 0 1 0 0 cm i 1 0 0 1 0 0 Tm tretira različito: cm množi CTM s desna i no-op je bilo gdje, dok Tm Tm i Tlm u cijelosti zamijeni, a svaki Tj pomakne Tm za širinu koju je iscrtao, pa je identity Tm nakon prikazanog teksta pravi reset
Generator izvještaja računao je na taj reset: brisanje identity Tm nacrtalo je Total odmah nakon Invoice na istoj baselini, i ništa nije palo, zapisalo niti upozorilo na putu do kupčeva pisača

Kako unatražno skeniranje odlučuje koji će identity Tm baciti

TPDFContentPeepholeOptimizer.RemoveIdentityMatrices sada hoda unatrag od svakog identity Tm i baca ga samo ako skeniranje prije stigne do BT ili do drugog identity Tm. Raniji identity Tm važi bilo da je zadržan bilo da je upravo sam zakazan za brisanje, jer je u oba slučaja objema matricama ostavio identitet, točno kao što čini BT. Pravilo svaki operator koji može susresti svrstava u jednu od dviju skupina:

  • Stani i zadrži Tm: Td, TD, T*, ne-identity Tm, Tj, TJ, ', ", ET, bilo koji operator koji parser ne prepozna, ili početak streama
  • Preskoči i nastavi skenirati: operatori koji nikad ne diraju Tm ni Tlm, poput Tf, Tc, postavljača boja, gs, marked-content operatora i cm
PDFlibPas RemoveIdentityMatrices hoda unatrag od svakog identity Tm: Tf, Tc, postavljači boja, gs i cm se preskaču, dok Td, TD, T*, ne-identity Tm, Tj, TJ, nepoznati operator ili ET zaustave skeniranje i zadrže Tm, a BT jamči za brisanje
I raniji identity Tm zaustavlja skeniranje, jer zadržan ili već zakazan za brisanje objema je matricama ostavio identitet — u oba slučaja optimizer nikad ne pomakne glif

Konzervativni su slučajevi namjerni. Nepoznati operator može biti bilo što, pa skeniranje odbija rasuđivati preko njega. ET zatvara text objekt, pa Tm nakon njega nema BT koje jamči za vrijednosti matrica. Skeniranje radi i na jednom content streamu istovremeno, što je bitno za stranice čiji je /Contents polje: sloj koji kreće usred text objekta, bez vlastitog BT, zadržava svoj identity Tm i kad bi ga prethodni sloj učinio suvišnim. To košta par bajtova na čudnim datotekama i nikad ne pomakne glif. Ako uređujete tekst stranice na razini instrukcija, kao u proputovanju mapiranja znakova na content bajtove, isti parsirani TPDFContentProgram model je ono što optimizer prepravlja

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; // oštećen stream: ostavi bajtove na miru
    Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
    try
      Optimizer.Run; // vraća broj uklonjenih instrukcija
    finally
      Optimizer.Free;
    end;
    Result := Prog.Emit; // jedna instrukcija po retku
  finally
    Prog.Free;
  end;
end;

// Uklonjeno: Tm izravno nakon BT, drugi od dva identity Tm u nizu
//   OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Zadržano: Tm nakon Td, nakon Tj, nakon ne-identity Tm, ili izvan BT
//   OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')

Pokrenite helpera na invoice streamu iz uvoda i identity Tm preživi, jer unatražno skeniranje naleti na Tj prije nego dođe do BT. Ubacite /F1 12 Tf, 2 Tc i 0 g između BT i identity Tm pa on i dalje ode, jer nijedan od njih ne dira text matrice. Sekvenca poput BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm gubi točno jedan operator: prvi identity Tm resetira matricu koju je pomaknuo Td, a samo je drugi suvišan

Kada se peephole optimizer stvarno pokreće?

Optimizer radi samo tijekom compression pasa, unutar TPDFPageTree.Compress, i samo na content streamovima koji već nisu Flate-komprimirani. TPDFlib.SetOptimizeContentStreams(1) je zadano, a isti se prekidač izlaže i kao polje OptimizeContentStreams od TPDFlibSaveOptions; njega poštuju i CompressContent i CompressPage. Stream čiji je /Filter već /FlateDecode potpuno se preskače, pa učitavanje postojećeg komprimiranog PDF-a i novo spremanje ne prepravlja njegove operatore. Ako stream padne pri parsiranju, izvorni dekodirani bajtovi komprimiraju se nepromijenjeni. TPDFlib.NormalizeContentStreams parsira i ponovno ispušta sadržaj s kanonskim razmacima i brojevima, ali nikad ne zove optimizer, što ga čini korisnom bazom kad želite vidjeti koliko peephole pravila pridonose razlici u veličini, uz veće dobitke pokrivene u optimizaciji veličine PDF datoteke s font subsettingom

PDFlibPas pokreće peephole optimizer samo unutar compression pasa pri spremanju: TPDFPageTree.Compress poštuje SetOptimizeContentStreams, stream već filtriran s /FlateDecode potpuno se preskače, neparsiriv stream komprimiraju izvorni bajtovi nepromijenjeni, a NormalizeContentStreams nikad ne zove optimizer
Preskočeni komprimirani streamovi su tihi dio: učitajte postojeći PDF, spremite ga ponovno, i njegovi operatori izađu nedirnuti jer optimizer prepravlja samo streamove koje je prvo dekodirao
var
  Lib: TPDFlib;
  Options: TPDFlibSaveOptions;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    // Nekomprimirani streamovi prolaze peephole pravila, pa Flate
    Lib.SetOptimizeContentStreams(1);
    Lib.CompressContent;
    Lib.SaveToFile('report-optimized.pdf');

    // Isti izbor kroz priložene opcije spremanja; False odustaje
    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;

Što je stari regresijski test zapravo jamčio?

Stari je regresijski test jamčio samo jedan oblik: identity Tm izravno nakon BT se uklanja. Peephole_RemovesIdentityTextMatrix daje BT 1 0 0 1 0 0 Tm (hello) Tj ET optimizeru i tvrdi da nijedan Tm ne ostaje. Ranije izdanje već je uočilo da je bacanje identity Tm nesigurno kad Tlm nije identitet, a onda ipak zadržalo ponašanje jer ga je test „zaključao". Pažljivo pročitan, test ne govori ništa o identity Tm nakon Td ili nakon prikazanog teksta; tretirati pokrivenost jednog uzorka kao ugovor cijelog pravila bila je prava pogreška. Ispravak zadržava prolaz tog izvornog slučaja i dodaje šest slučajeva koji pribijaju i uklonjive oblike i zadržane, uključivo Tm izvan svakog text objekta i onaj koji slijedi nakon ET

Kompromis je lako prihvatiti jednom kad je zapisan. Generatori koji svaki text objekt omataju kao BT 1 0 0 1 0 0 Tm ... i dalje dobivaju taj suvišni operator uklonjen, a odakle je dolazila gotovo sva ušteda. Ono čega se optimizer odriče jest povremeni identity Tm usred text objekta, šaka bajtova po stranici prije nego ih Flate uopće vidi, u zamjenu za jamstvo koje header modula izravno kaže: svaka je transformacija output-ekvivalentna i nikad ne mijenja vidljivu stranicu. Optimizer veličine koji pomakne tekst nije optimizer, nego rendering bug s dobrim kompresijskim omjerima

Parser content streama, peephole optimizer i opcije kompresije pri spremanju opisani ovdje svi se isporučuju u PDF Library za Delphi i C++Builder, koji također izlaže NormalizeContentStreams, CompressContent i TPDFlibSaveOptions za ugađanje načina na koji se svaki dokument zapisuje