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
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čnaTm,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 icm
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
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