PDF Library for Delphi odstraňuje operátor identity text matice, 1 0 0 1 0 0 Tm, při save-time peephole optimalizaci content streamu jen tehdy, když jsou text matrix i text line matrix už teď identity: hned po BT nebo hned po dřívější identity Tm. Identity cm se pořád maže vždycky, protože cm násobí CTM, zatímco Tm obě text matice rovnou nahrazuje. Od v3.539.28 zůstává v streamu každý jiný identity Tm
Bug, který tahle oprava řeší, je ten tichý druh. Generátor reportů vyšle BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET a spoléhá na identity Tm, která pošle druhý řetězec zpátky na počátek textového prostoru, než si aplikuje vlastní poziční logiku. Starší optimizer viděl šest čísel, které hlásají identitní matici, usoudil, že operátor nemůže změnit vůbec nic, a smazal ji. Nic neselhalo, nic nezalogovalo varování a uložená stránka nakreslila „Total“ hned za „Invoice“ na stejné baseline — přesně ta třída vad, které si nikdo nevšimne, dokud zákazník PDF nevytiskne
Proč není 1 0 0 1 0 0 Tm vždy no-op?
Identity Tm je no-op jen tehdy, když nahrazuje dvě matice, které už identitu drží, a to je vlastnost operátorů před ní, ne jejích vlastních operandů. ISO 32000-1 §9.4.1 říká, že BT inicializuje text matrix (Tm) i text line matrix (Tlm) na identitu, a §9.4.2 definuje Tm jako nastavení obou na zadané hodnoty, ne jejich násobení. Srovnejte s cm (§8.4.4), které aktuální transformační matici násobí zprava: násobení identitou jakoukoli CTM nechá beze změny, takže 1 0 0 1 0 0 cm se smí smazat kdekoliv. Uvnitř text objektu je obraz jiný. Td, TD, T* a ne-identity Tm všechny hýbají Tlm a každý text zobrazující operátor (Tj, TJ, ', ") posouvá Tm o šířku namalovaných glyfů. Po kterýchkoli z nich je identity Tm doopravdy reset na počátek. Pokud jste někdy ručně trasovali pozice textu s trackerem stavu CTM a text matrix v content streamu, je to tentýž rozdíl mezi skládáním stavu a jeho nahrazováním
Jak zpětný scan rozhoduje, který identity Tm smaže
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices teď od každé identity Tm jde zpátky a maže ji, jen když scan dřív narazí na BT nebo jinou identity Tm. Dřívější identity Tm počítá, ať je ponechána, nebo je sama právě přihlášena ke smazání, protože oběma cestami nechala obě matice v identitě, úplně jako BT. Pravidlo zařazuje každý operátor, který může potkat, do jedné ze dvou skupin:
- Zastavit a Tm ponechat:
Td,TD,T*, ne-identityTm,Tj,TJ,',",ET, jakýkoli operátor, který parser nezná, nebo začátek streamu - Přeskočit a scanovat dál: operátory, které se Tm ani Tlm nedotýkají, jako
Tf,Tc, nastavovače barev,gs, marked-content operátory acm
Konzervativní případy jsou záměr. Neznámý operátor může být cokoliv, takže scan odmítá uvažovat za ním. ET zavírá text objekt, takže Tm za ním nemá žádné BT, které by za hodnoty matic ručilo. Scan navíc pracuje vždy s jedním content streamem, což je důležité u stránek, jejichž /Contents je pole: vrstva, která startuje uprostřed text objektu a žádné vlastní BT nemá, si identity Tm ponechá, i kdyby ji předchozí vrstva učinila redundantní. To stojí pár bajtů na podivných souborech a nikdy nepohne glyfem. Pokud editujete text stránky na úrovni instrukcí, jako ve průvodci mapováním znaků na content bajty, tenhle parsovaný model TPDFContentProgram je přesně to, co optimizer přepisuje
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škozený stream: bajty necháme na pokoji
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // vrátí počet odebraných instrukcí
finally
Optimizer.Free;
end;
Result := Prog.Emit; // jedna instrukce na řádek
finally
Prog.Free;
end;
end;
// Odstraněno: Tm hned po BT, druhá ze dvou identity Tm za sebou
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Ponecháno: Tm po Td, po Tj, po ne-identity Tm nebo mimo BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Pusťte helper na invoice stream z úvodu a identity Tm přežije, protože zpětný scan narazí na Tj dřív než na BT. Vložte /F1 12 Tf, 2 Tc a 0 g mezi BT a identity Tm a pořád zmizí, protože se žádného z nich text matice nedotýká. Sekvence jako BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm ztratí přesně jeden operátor: první identity Tm resetuje matici, kterou hyla Td, a redundantní je až ta druhá
Kdy peephole optimizer doopravdy běží?
Optimizer běží jen během kompresní fáze, uvnitř TPDFPageTree.Compress, a jen na content streamy, které ještě nejsou Flate komprimované. TPDFlib.SetOptimizeContentStreams(1) je default a stejný přepínač je vystavený jako pole OptimizeContentStreams v TPDFlibSaveOptions; ctí ho i CompressContent a CompressPage. Stream, jehož /Filter je už /FlateDecode, se přeskočí úplně, takže načtení existujícího komprimovaného PDF a jeho nové uložení operátory nepřepíše. Když se stream nepodaří parsovat, původní dekódované bajty se zkomprimují beze změny. TPDFlib.NormalizeContentStreams parsuje a znovu vydává obsah s kanonickými mezerami a čísly, ale optimizer nikdy nevolá, takže je to užitečný baseline, když chcete vidět, jak velký podíl na úspoře mají peephole pravidla, vedle větších zisků popsaných v optimalizaci velikosti PDF souboru se subsettingem fontů
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Nekomprimované streamy projdou peephole pravidly, pak Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Tatáž volba přes přibalené save options; False se odhlásí
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;
Co ten starý regresní test doopravdy garantoval?
Starý regresní test garantoval jediný tvar: identity Tm hned po BT se odstraní. Peephole_RemovesIdentityTextMatrix podstrčí optimizeru BT 1 0 0 1 0 0 Tm (hello) Tj ET a asertuje, že žádné Tm nezbylo. Některé dřívější vydání si už poznamenalo, že mazání identity Tm je nebezpečné, když Tlm identitou není, a chování si přesto nechalo, protože ho test „zamkl“. Čtené pozorně, test o identity Tm po Td nebo po zobrazeném textu neříká vůbec nic; vzít pokrytí jednoho vzorku za kontrakt celého pravidla byla ta skutečná chyba. Oprava nechává tenhle původní případ procházet a přidává šest případů, které přibíjí jak odebratelné tvary, tak ponechané, včetně Tm mimo jakýkoli text objekt a jedné, která následuje po ET
Kompromis se dá přijmout hned, jakmile je sepsaný. Generátory, které každý text objekt balí jako BT 1 0 0 1 0 0 Tm ..., si tu redundantní operaci nechají odebrat i dál, a právě odtud přišla téměř veškerá úspora. Optimizer se vzdává občasné identity Tm uprostřed text objektu, hrstky bajtů na stránku, než je vůbec uvidí Flate, výměnou za garanci, kterou hlavička modulu říká na rovinu: každá transformace je výstupně ekvivalentní a viditelnou stránku nikdy nezmění. Size optimizer, který hýbe textem, není optimizer, je to rendering bug s dobrým kompresním poměrem
Content-stream parser, peephole optimizer i save-time kompresní volby popsané tady sjíždějí v PDF Library for Delphi and C++Builder, která navíc vystavuje NormalizeContentStreams, CompressContent a TPDFlibSaveOptions pro dolaďování toho, jak se každý dokument zapíše