Tehnički članak

Identity Tm u PDF-u: kada peephole sme da ga obriše

PDF Library for Delphi uklanja operator jedinične tekst matrice, 1 0 0 1 0 0 Tm, tokom peephole optimizacije content streamova pri čuvanju samo kada su text matrix i text line matrix već jedinične: odmah posle BT, ili odmah posle ranije jedinične Tm. Jedinični cm se i dalje uvek briše, jer cm množi CTM dok Tm obe tekst matrice zamenjuje iz temelja. Od v3.539.28 svaka druga jedinična Tm ostaje u streamu

Bug koji se ovime ispravlja je onaj tihi. Generator izveštaja izbaci BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, računajući da će jedinična Tm drugi string vratiti na koordinatni početak text space-a pre nego što primeni svoju logiku pozicioniranja. Stariji optimizer je video šest brojeva koji ispisuju jediničnu matricu, zaključio da operator nužno ne menja ništa, i obrisao ga. Ništa nije puklo, ništa nije upisalo upozorenje, a sačuvana stranica je nacrtala „Total“ odmah iza „Invoice“ na istoj liniji, a to je baš ona vrsta defekta koju niko ne primećuje dok kupac ne odštampa PDF

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

Jedinična Tm je no-op samo kada bi zamenila dve matrice koje već drže jediničnu vrednost, a to je osobina operatora ispred nje, ne njenih sopstvenih operanada. ISO 32000-1 §9.4.1 kaže da BT inicijalizuje i text matrix (Tm) i text line matrix (Tlm) na jediničnu, a §9.4.2 definiše Tm kao postavljanje obe na date vrednosti, a ne nadovezivanje na njih. Uporedite sa cm (§8.4.4), koji desnim množenjem uvršta matricu trenutne transformacije: množenje jediničnom ne menja nijedan CTM, pa se 1 0 0 1 0 0 cm može bezbedno obrisati bilo gde. Unutar tekst objekta slika je drugačija. Td, TD, T* i ne-jedinična Tm pomeraju Tlm, a svaki operator za prikaz teksta (Tj, TJ, ', ") pomera Tm za širinu iscrtranih glifova. Posle bilo kog od njih, jedinična Tm je pravi reset na koordinatni početak. Ako ste ikada ručno praćili pozicije teksta uz praćivač CTM i text matrix stanja content streama, ovo je ista razlika između nadovezivanja i zamene stanja

PDFlibPas postupa drugačije sa 1 0 0 1 0 0 cm i 1 0 0 1 0 0 Tm: cm desnim množenjem uvršta CTM i no-op je bilo gde, dok Tm Tm i Tlm zamenjuje iz temelja, a svaki Tj pomera Tm za širinu koju je iscrtao, pa je jedinična Tm posle prikazanog teksta pravi reset
Generator izveštaja računao je na taj reset: brisanjem jedinične Tm Total je nacrtan odmah iza Invoice na istoj liniji, i ništa na putu do štampača nije puklo, logovalo se niti upozorilo

Kako unazad skeniranje odlučuje koju jediničnu Tm da obriše

TPDFContentPeepholeOptimizer.RemoveIdentityMatrices sada šeta unazad od svake jedinične Tm i briše je samo ako sken prvo stigne do BT ili do druge jedinične Tm. Ranija jedinična Tm se računa bez obzira na to da li je zadržana ili je i za nju već zakazano brisanje, jer je u oba slučaja ostavila obe matrice jediničnim, tačno kao što to čini BT. Pravilo svaki operator koji može da sretne svrstava u jednu od dve grupe:

  • Zaustavi se i zadrži Tm: Td, TD, T*, ne-jedinična Tm, Tj, TJ, ', ", ET, bilo koji operator koji parser ne prepoznaje, ili početak streama
  • Preskoči i nastavi sa skeniranjem: operatori koji nikada ne dodiruju Tm ili Tlm, poput Tf, Tc, postavljača boje, gs, marked-content operatora i cm
PDFlibPas RemoveIdentityMatrices šeta unazad od svake jedinične Tm: Tf, Tc, postavljači boje, gs i cm se preskaču, dok Td, TD, T*, ne-jedinična Tm, Tj, TJ, nepoznat operator ili ET zaustavljaju sken i zadržavaju Tm, a BT garantuje za brisanje
I ranija jedinična Tm zaustavlja sken, jer je zadržana ili već zakazana za brisanje ostavila obe matrice jediničnim — ma kako da je, optimizer nikada ne pomera glif

Konzervativni slučajevi su namerni. Nepoznat operator može biti bilo šta, pa sken odbija da zaključuje dalje od njega. ET zatvara tekst objekat, pa Tm posle njega nema BT koji garantuje za vrednosti matrice. Sken radi i na jednom content streamu istovremeno, što je bitno za stranice čiji je /Contents niz: sloj koji počinje usred tekst objekta, bez sopstvenog BT-a, zadržava svoju jediničnu Tm i kad bi je prethodni sloj učinio suvišnom. To košta par bajtova na čudnim fajlovima i nikada ne pomera glif. Ako stranicu tekst uređujete na nivou instrukcija, kao u propitku 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: bajtovi ostaju kakvi jesu
    Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
    try
      Optimizer.Run; // vraća broj uklonjenih instrukcija
    finally
      Optimizer.Free;
    end;
    Result := Prog.Emit; // jedna instrukcija po redu
  finally
    Prog.Free;
  end;
end;

// Uklonjeno: Tm odmah posle BT, druga od dve jedinične Tm zaredom
//   OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Zadržano: Tm posle Td, posle Tj, posle ne-jedinične Tm, ili van BT
//   OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')

Pustite pomoćnu funkciju na stream fakture iz uvoda i jedinična Tm preživi, jer unazad sken naleti na Tj pre nego što dođe do BT-a. Ubacite /F1 12 Tf, 2 Tc i 0 g između BT-a i jedinične Tm pa i dalje ide, jer nijedan od njih ne dodiruje tekst matrice. Sekvenca poput BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm gubi tačno jedan operator: prva jedinična Tm resetuje matricu koju je Td pomerila, a samo je druga suvišna

Kada se peephole optimizer zapravo pokreće?

Optimizer se pokreće samo u kompresionom prolazu, unutar TPDFPageTree.Compress, i samo na content streamovima koji već nisu Flate komprimovani. TPDFlib.SetOptimizeContentStreams(1) je podrazumevano stanje, a isti prekidač je izložen i kao OptimizeContentStreams polje TPDFlibSaveOptions; i CompressContent i CompressPage ga poštuju. Stream čiji je /Filter već /FlateDecode potpuno se preskače, pa učitavanje postojećeg komprimovanog PDF-a i njegovo ponovno čuvanje ne prepravlja njegove operatore. Ako stream ne uspe da se parsira, originalni dekodovani bajtovi komprimuju se nepromenjeni. TPDFlib.NormalizeContentStreams parsira i ponovo izbacuje sadržaj sa kanonskim razmacima i brojevima, ali nikada ne zove optimizer, što ga čini korisnom osnovnom linijom kad želite da vidite koliko peephole pravila doprinose razlici u veličini, uz veće uštede pokrivene u tekstu o optimizaciji veličine PDF fajla uz font subsetting

PDFlibPas pokreće peephole optimizer samo unutar kompresionog prolaza pri čuvanju: TPDFPageTree.Compress poštuje SetOptimizeContentStreams, stream već filtriran sa /FlateDecode potpuno se preskače, stream koji se ne može parsirati komprimuje se sa originalnim bajtovima nepromenjenim, a NormalizeContentStreams nikada ne zove optimizer
Preskočeni komprimovani streamovi su tihi deo priče: učitajte postojeći PDF, sačuvajte ga ponovo, i njegovi operatori izlaze nedirnuti jer optimizer prepravlja samo streamove koje je prvo dekodovao
var
  Lib: TPDFlib;
  Options: TPDFlibSaveOptions;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    // Nekomprimovani streamovi prolaze kroz peephole pravila, pa Flate
    Lib.SetOptimizeContentStreams(1);
    Lib.CompressContent;
    Lib.SaveToFile('report-optimized.pdf');

    // Isti izbor kroz priložene opcije čuvanja; False isključuje
    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;

Šta je stari regresioni test zaista garantovao?

Stari regresioni test garantovao je jedan oblik i ništa više: jedinična Tm odmah posle BT-a se uklanja. Peephole_RemovesIdentityTextMatrix daje BT 1 0 0 1 0 0 Tm (hello) Tj ET optimizeru i tvrdi da nijedna Tm ne ostaje. Ranije izdanje je već zabeležilo da je brisanje jedinične Tm nesigurno kad Tlm nije jedinična, a ponašanje ipak zadržalo jer ga je test „zaključao“. Pažljivo pročitan, test ne govori ništa o jediničnoj Tm posle Td ili posle prikazanog teksta; tretiranje pokrivenosti jednog uzorka kao ugovora celog pravila bila je prava greška. Popravka čuva taj prvobitni slučaj zelenim i dodaje šest slučajeva koji fiksiraju i uklonjive oblike i zadržane, uključujući Tm van svakog tekst objekta i jednu koja sledi posle ET-a

Kompromis je lako prihvatiti kad se jednom napiše. Generatori koji svaki tekst objekat umotaju u BT 1 0 0 1 0 0 Tm ... i dalje dobijaju taj suvišni operator uklonjen, a odatle je dolazila skoro sva ušteda. Ono čega se optimizer odriče je povremena jedinična Tm usred tekst objekta, šaka bajtova po stranici pre nego što ih Flate uopšte vidi, u zamenu za garanciju koju zaglavlje modula izričito navodi: svaka transformacija je izlazno-ekvivalentna i nikada ne menja vidljivu stranicu. Optimizer veličine koji pomera tekst nije optimizer, nego render bug sa dobrim kompresionim odnosima

Parser content streamova, peephole optimizer i opcije kompresije pri čuvanju opisani ovde isporučuju se u PDF Library for Delphi and C++Builder, koja izlaže i NormalizeContentStreams, CompressContent i TPDFlibSaveOptions za doterivanje načina na koji se svaki dokument upisuje