PDF Library for Delphi премахва identity text matrix оператора 1 0 0 1 0 0 Tm при save-time peephole оптимизацията на content stream-и само когато text matrix и text line matrix вече са identity: веднага след BT или веднага след по-ранен identity Tm. Identity cm и нататък се маха винаги, защото cm умножава CTM, докато Tm заменя и двете текстови матрици изцяло. От v3.539.28 всеки друг identity Tm остава в stream-а
Бъгът, който това поправя, е от тихите. Генератор на отчети излъчва BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET, разчитайки на identity Tm да върне втория низ към началото на text space, преди да приложи собствената си логика за позициониране. По-старият оптимизатор виждаше шест числа, изписващи identity матрица, реши, че операторът не може да променя каквото и да е, и го изтри. Нищо не се провали, нищо не логна предупреждение, а записаната страница нарисува „Total" веднага след „Invoice" на същата базова линия — точно класът дефекти, който никой не забелязва, докато клиент не принтира PDF-а
Защо 1 0 0 1 0 0 Tm не винаги е no-op?
Identity Tm е no-op само когато би заменил две матрици, които вече държат identity, а това е свойство на операторите преди него, не на собствените му операнди. ISO 32000-1 §9.4.1 казва, че BT инициализира и text matrix (Tm), и text line matrix (Tlm) до identity, а §9.4.2 дефинира Tm като задаване и на двете на дадените стойности, не конкатенация върху тях. Сравнете с cm (§8.4.4), който умножава отдясно текущата трансформационна матрица: умножение по identity оставя всяка CTM непроменена, така че 1 0 0 1 0 0 cm може безопасно да се изтрие навсякъде. Вътре в текстов обект картината е друга. Td, TD, T* и не-identity Tm местят Tlm, а всеки текстопоказващ оператор (Tj, TJ, ', ") придвижва Tm с ширината на нарисуваните глифи. След някой от тях identity Tm е истинско зануляване към началото. Ако някога сте трасирали текстови позиции на ръка с state tracker-а за content stream CTM и text matrix, това е същата разлика между конкатениране на състоянието и замяната му
Как обратното сканиране решава кой identity Tm да махне
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices вече върви назад от всеки identity Tm и го маха само ако сканирането стигне първо BT или друг identity Tm. По-ранният identity Tm брои дали е запазен или самият току-що е планиран за триене, защото и в двата случая е оставил двете матрици на identity, точно както прави BT. Правилото разпределя всеки оператор, който може да срещне, в една от две групи:
- Спри и запази Tm:
Td,TD,T*, не-identityTm,Tj,TJ,',",ET, всеки оператор, който парсерът не разпознава, или началото на stream-а - Прескочи и продължи сканирането: оператори, които изобщо не пипат Tm или Tlm, като
Tf,Tc, color setter-и,gs, marked-content оператори иcm
Консервативните случаи са нарочни. Непознат оператор може да е каквото и да е, така че сканирането отказва да разсъждава отвъд него. ET затваря текстовия обект, така че Tm след него няма BT, което да гарантира стойностите на матрицата. Сканирането работи и по един content stream наведнъж, което има значение за страници, чиито /Contents е масив: слой, започващ в средата на текстов обект, без собствен BT, си пази identity Tm, дори предишният слой да би го направил излишен. Струва няколко байта на странни файлове и никога не мести глиф. Ако редактирате текст на страница на ниво инструкция, както в упражнението за character-to-content-byte съпоставяне, същият парснат 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; // повреден stream: остави байтовете на мира
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // връща броя премахнати инструкции
finally
Optimizer.Free;
end;
Result := Prog.Emit; // една инструкция на ред
finally
Prog.Free;
end;
end;
// Премахнато: Tm директно след BT, вторият от два identity Tm един под друг
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// Запазено: Tm след Td, след Tj, след не-identity Tm или извън BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
Пуснете helper-а върху stream-а с фактура от началото и identity Tm оцелява, защото обратното сканиране удря Tj, преди да стигне BT. Сложете /F1 12 Tf, 2 Tc и 0 g между BT и identity Tm и той пак си отива, защото нито един от тях не пипа текстовите матрици. Последователност като BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm губи точно един оператор: първият identity Tm занулява матрицата, която Td е изместил, и само вторият е излишен
Кога действително се пуска peephole оптимизаторът?
Оптимизаторът се пуска само по време на компресиращия проход, вътре в TPDFPageTree.Compress, и само по content stream-и, които не са вече Flate-компресирани. TPDFlib.SetOptimizeContentStreams(1) е по подразбиране, а същият ключ е изложен и като OptimizeContentStreams поле на TPDFlibSaveOptions; и CompressContent, и CompressPage го уважават. Stream с /Filter вече /FlateDecode се прескача изцяло, така че зареждане на компресиран PDF и повторно записване не пренаписват операторите му. Ако stream-ът се провали при парсване, оригиналните декодирани байтове се компресират непроменени. TPDFlib.NormalizeContentStreams парсва и преизлъчва съдържание с канонични интервали и числа, но никога не вика оптимизатора, което го прави полезен базис, когато искате да видите колко от разликата в размера дават peephole правилата — редом с по-големите спестявания, покрити в оптимизацията на размера на PDF файл с font subsetting
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Некомпресирани stream-ове минават през peephole правилата, после Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Същият избор през вградените save опции; False се отказва
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;
Какво всъщност гарантираше старият регресионен тест?
Старият регресионен тест гарантираше само една форма: identity Tm директно след BT се премахва. Peephole_RemovesIdentityTextMatrix подава BT 1 0 0 1 0 0 Tm (hello) Tj ET на оптимизатора и assert-ва, че не е останал Tm. По-ранна версия вече беше отбелязала, че триенето на identity Tm е небезопасно, когато Tlm не е identity, после пак запази поведението, защото тестът го „заключваше". Прочетен внимателно, тестът не казва нищо за identity Tm след Td или след показан текст; отнасянето към покритието на една извадка като към договора на цялото правило беше истинската грешка. Поправката запазва оригиналния случай да минава и добавя шест случая, които заковат както махаемите форми, така и запазените, включително Tm извън всеки текстов обект и такъв след ET
Компромисът е лесен за приемане, щом е записан. Генератори, които увиват всеки текстов обект като BT 1 0 0 1 0 0 Tm ..., и нататък си изкарват излишния оператор премахнат — откъдето идваха почти всички спестявания. Това, което оптимизаторът отстъпва, е повременния identity Tm в средата на текстов обект, шепа байта на страница, преди Flate изобщо да ги види, в замяна на гаранция, която модулният header казва направо: всяка трансформация е output-еквивалентна и никога не променя видимата страница. Size оптимизатор, който мести текст, не е оптимизатор, а rendering бъг с добри коефициенти на компресия
Парсерът на content stream-и, peephole оптимизаторът и save-time опциите за компресия, описани тук, идват всичките с PDF Library for Delphi и C++Builder, която излага и NormalizeContentStreams, CompressContent и TPDFlibSaveOptions за настройка как всеки документ се записва