PDF Library for Delphi odstraňuje operátor identity text matrix, 1 0 0 1 0 0 Tm, pri optimalizácii content streamov peephole počas ukladania len vtedy, keď sú text matrix aj text line matrix už identické: tesne po BT alebo tesne po predchádzajúcom identickom Tm. Identické cm sa naďalej zahodí vždy, lebo cm násobí CTM, zatiaľ čo Tm obe textové matice rovno nahradí. Od v3.539.28 ostanú v streame všetky ostatné identické Tm
Chyba, ktorú to opravuje, je tichého typu. Generátor reportov vyprodukuje BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET a spolieha sa na identické Tm, aby druhý reťazec vrátil do počiatku textového priestoru skôr, než si aplikuje vlastnú pozicovaciu logiku. Starší optimizer videl šesť čísel hláskujúcich identickú maticu, rozhodol, že operátor nemôže nič zmeniť, a vymazal ho. Nič nezlyhalo, nič nezalogovalo varovanie a uložená strana nakreslila „Total“ hneď za „Invoice“ na tom istom baseline, čo je presne tá trieda defektov, ktorej si nikto nevšimne, kým zákazník nevytlačí PDF
Prečo nie je 1 0 0 1 0 0 Tm vždy no-op?
Identické Tm je no-op len vtedy, keď by nahradilo dve matice, ktoré už identitu držia, a to je vlastnosť operátorov pred ním, nie jeho vlastných operandov. ISO 32000-1 §9.4.1 hovorí, že BT inicializuje text matrix (Tm) aj text line matrix (Tlm) na identitu a §9.4.2 definuje Tm ako nastavenie oboch na dané hodnoty, nie ich napojenie. Porovnajte to s cm (§8.4.4), ktoré násobí aktuálnu transformačnú maticu sprava: násobenie identitou nechá každé CTM nezmenené, takže 1 0 0 1 0 0 cm je bezpečné vymazať hocikde. Vnútri textového objektu je obraz iný. Td, TD, T* a neidentické Tm všetky hýbu s Tlm a každý operátor zobrazujúci text (Tj, TJ, ', ") posunie Tm o šírku nakreslených glyfov. Po ktoromkoľvek z nich je identické Tm skutočný reset do počiatku. Ak ste textové pozície niekedy vysledovali ručne cez state tracker CTM a text matrix v content streame, je to to isté rozlíšenie medzi napojením stavu a jeho nahradením
Ako backward scan rozhoduje, ktoré identické Tm zahodiť
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices teraz prechádza od každého identického Tm dozadu a zahodí ho, len ak sa sken skôr dospeje k BT alebo k ďalšiemu identickému Tm. Predchádzajúce identické Tm sa počíta, či už je ponechané, alebo bolo samo práve naplánované na vymazanie, lebo oboma spôsobmi nechalo obe matice na identite, presne ako to robí BT. Pravidlo roztriedi každý operátor, ktorý stretne, do jednej z dvoch skupín:
- Zastav a Tm ponechaj:
Td,TD,T*, neidentickéTm,Tj,TJ,',",ET, akýkoľvek operátor, ktorý parser nepozná, alebo začiatok streamu - Prejdi cez a skenuj ďalej: operátory, ktoré na Tm ani Tlm nesiahnu, ako
Tf,Tc, nastavovače farieb,gs, operátory marked-content acm
Konzervatívne prípady sú zámerné. Neznámy operátor môže byť čokoľvek, takže sken odmieta uvažovať za neho. ET uzatvára textový objekt, takže Tm po ňom nemá BT, ktoré by ručilo za hodnoty matíc. Sken pracuje navyše vždy len s jedným content streamom, čo má význam na stranách, ktorých /Contents je pole: vrstva, ktorá začína v strede textového objektu bez vlastného BT, si nechá svoje identické Tm, aj keď by ju predchádzajúca vrstva urobila redundantnou. Stojí to pár bajtov na zvláštnych súboroch a nikdy nepohne glyfom. Ak upravujete text strany na úrovni inštrukcií, ako v sprievodcovi mapovaním znakov na content bajty, optimizer prepisuje presne ten istý naparsovaný model TPDFContentProgram
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; // poškodený stream: bajty nechaj na pokoji
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // vráti počet odstránených inštrukcií
finally
Optimizer.Free;
end;
Result := Prog.Emit; // jedna inštrukcia na riadok
finally
Prog.Free;
end;
end;
// Odstránené: Tm tesne po BT, druhé z dvoch identických Tm za sebou
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Ponechané: Tm po Td, po Tj, po neidentickom Tm alebo mimo BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Spustite helper na invoice streame z úvodu a identické Tm prežije, lebo backward scan narazí na Tj skôr, než dôjde k BT. Vložte /F1 12 Tf, 2 Tc a 0 g medzi BT a identické Tm a aj tak zmizne, keďže žiaden z nich na textové matice nesiahne. Sekvencia ako BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm prichádza o presne jeden operátor: prvé identické Tm resetuje maticu, ktorú pohnulo Td, a redundantné je len to druhé
Kedy peephole optimizer naozaj beží?
Optimizer beží len počas kompresného priebehu, vnútri TPDFPageTree.Compress, a len na content streamoch, ktoré už nie sú Flate-komprimované. Predvolené je TPDFlib.SetOptimizeContentStreams(1) a ten istý prepínač je vystavený ako pole OptimizeContentStreams v TPDFlibSaveOptions; rešpektujú ho aj CompressContent, aj CompressPage. Stream, ktorého /Filter je už /FlateDecode, sa preskočí celý, takže načítanie existujúceho komprimovaného PDF a opätovné uloženie neprepíše jeho operátory. Ak sa stream nepodarí naparsovať, komprimujú sa pôvodné dekódované bajty bez zmeny. TPDFlib.NormalizeContentStreams parsuje a emituje obsah nanovo s kanonickými medzerami a číslami, ale optimizer nikdy nezavolá, čo z neho robí užitočný baseline, keď chcete vidieť, koľko na veľkosti pridávajú peephole pravidlá, vedľa väčších úspor popísaných v článku o optimalizácii veľkosti PDF s font subsettingom
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Nekomprimované streamy prejdu peephole pravidlami, potom Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Tá istá voľba cez priložené save options; False sa odhlási
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;
Čo ten starý regresný test vlastne garantoval?
Starý regresný test garantoval jediný tvar: identické Tm tesne po BT sa odstráni. Peephole_RemovesIdentityTextMatrix podá optimizeru BT 1 0 0 1 0 0 Tm (hello) Tj ET a asertuje, že nezostane žiadne Tm. Skoršie vydanie si už vtedy poznamenalo, že zahodenie identického Tm nie je bezpečné, keď Tlm nie je identita, a správanie aj tak nechalo, lebo ho test „uzamkol“. Pri pozornom čítaní test o identickom Tm po Td alebo po zobrazenom texte nepovie nič; brať pokrytie jednej vzorky ako kontrakt celého pravidla bola tá pravá chyba. Oprava nechá pôvodný prípad prechádzať a pridá šesť prípadov, ktoré priklincujú odstrániteľné tvary aj ponechávané, vrátane Tm mimo akéhokoľvek textového objektu a takého, ktoré nasleduje po ET
Kompromis sa dá ľahko prijať, keď je napísaný. Generátory, ktoré každý textový objekt obalia ako BT 1 0 0 1 0 0 Tm ..., sa toho redundantného operátora aj naďalej zbavia, a práve odtiaľ pochádzali takmer všetky úspory. Optimizer sa teda vzdáva občasného identického Tm uprostred textového objektu, hrstky bajtov na stranu ešte predtým, než ich vôbec uvidí Flate, výmenou za garanciu, ktorú hlavička modulu hovorí nahlas: každá transformácia je výstupne ekvivalentná a viditeľnú stranu nikdy nezmení. Veľkostný optimizer, ktorý hýbe textom, nie je optimizer, je to rendering bug s dobrými kompresnými pomermi
Content-stream parser, peephole optimizer aj kompresné voľby pri ukladaní popísané tu dodávame v PDF Library for Delphi and C++Builder, ktorá navyše vystavuje NormalizeContentStreams, CompressContent a TPDFlibSaveOptions na doladenie toho, ako sa každý dokument zapíše