PDF Library for Delphi видаляє оператор identity текстової матриці, 1 0 0 1 0 0 Tm, під час збережувальної peephole-оптимізації потоку вмісту лише тоді, коли текстова матриця і матриця текстового рядка вже є identity: просто після BT чи просто після попереднього identity Tm. Identity cm досі викидається завжди, бо cm домножується на CTM, тоді як Tm обидві текстові матриці замінює начисто. З v3.539.28 кожен інший identity Tm лишається в потоці
Баг, який це виправляє, — тихого сорту. Генератор звітів видає 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 ініціалізує обидві — текстову матрицю (Tm) і матрицю текстового рядка (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 — це справжнє скидання в початок. Якщо ви колись трасували позиції тексту вручну з трекером стану CTM і текстової матриці потоку вмісту, це та сама відмінність між домножуванням стану і заміною
Як зворотний скан вирішує, який identity Tm викинути
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices тепер іде назад від кожного identity Tm і викидає його, лише якщо скан першим досягає BT чи іншого identity Tm. Попередній identity Tm зараховується незалежно від того, чи він збережений, чи сам щойно запланований на видалення, бо будь-як він лишив обидві матриці в identity, точнісінько як BT. Правило розкидає кожен оператор, якого може зустріти, на дві групи:
- Стоп і зберегти Tm:
Td,TD,T*, не-identityTm,Tj,TJ,',",ET, будь-який оператор, якого парсер не розпізнає, або початок потоку - Переступити і сканувати далі: оператори, що ніколи не торкаються Tm чи Tlm, як-от
Tf,Tc, задавачі кольору,gs, оператори marked-content іcm
Консервативні випадки — навмисні. Невідомий оператор може бути чим завгодно, тож скан відмовляється міркувати далі за нього. ET закриває текстовий об'єкт, тож Tm після нього не має BT, який поручився б за значення матриць. Скан також працює з одним потоком вмісту за раз, що важливо для сторінок, чий /Contents — масив: шар, що починається посередині текстового об'єкта, без власного BT, зберігає свій identity Tm, навіть коли попередній шар зробив би його надлишковим. Це коштує кількох байтів на дивних файлах і ніколи не рухає гліф. Якщо ви редагуєте текст сторінки на рівні інструкцій, як у розборі відповідності символів і байтів вмісту, той самий розібраний 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; // пошкоджений потік: байти не чіпаємо
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')
Прогоніть хелпер на інвойсному потоці з початку статті — 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, і лише на потоках вмісту, які ще не стиснуті Flate. TPDFlib.SetOptimizeContentStreams(1) — усталено, і той самий перемикач виставлено як поле OptimizeContentStreams у TPDFlibSaveOptions; обидва — CompressContent і CompressPage — його поважають. Потік, чий /Filter уже /FlateDecode, пропускається повністю, тож завантаження наявного стиснутого PDF і повторне збереження не переписує його операторів. Якщо потік не парситься, оригінальні розкодовані байти стискаються без змін. TPDFlib.NormalizeContentStreams розбирає і перевидає вміст із канонічними пробілами та числами, але ніколи не викликає оптимізатор, що робить його корисною базою порівняння, коли хочеться побачити, скільки розміру дають peephole-правила, поруч із більшими виграшами з оптимізації розміру PDF-файлу субсетингом шрифтів
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// Нестиснуті потоки йдуть через peephole-правила, потім Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// Той самий вибір через комплектні опції збереження; 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 і перевіряє, що жодного Tm не лишилося. Раніший реліз уже відзначав, що викидати identity Tm небезпечно, коли Tlm не identity, але поведінку все одно лишив, бо тест її «зафіксував». Прочитаний уважно, тест не каже нічого про identity Tm після Td чи після показаного тексту; вважати покриття одного зразка контрактом усього правила — ось справжня помилка. Виправлення лишає початковий випадок зеленим і додає шість випадків, які пришпилюють і вилучні форми, і збережені, включно з Tm поза будь-яким текстовим об'єктом і тією, що йде після ET
Компроміс легко прийняти, щойно він записаний. Генератори, що обгортають кожен текстовий об'єкт як BT 1 0 0 1 0 0 Tm ..., досі отримують видалення того надлишкового оператора — звідси йшла майже вся економія. Від чого оптимізатор відмовляється, так це від епізодичної identity Tm посередині текстового об'єкта — кількох байтів на сторінку ще до того, як їх побачить Flate — в обмін на гарантію, яку заголовок модуля формулює просто: кожна трансформація еквівалентна на виході і ніколи не змінює видимої сторінки. Розмірний оптимізатор, який рухає текст, — не оптимізатор, це рендер-баг із хорошими коефіцієнтами стиснення
Парсер потоку вмісту, peephole-оптимізатор і збережувальні опції стиснення, описані тут, входять до PDF Library for Delphi and C++Builder, яка також відкриває NormalizeContentStreams, CompressContent і TPDFlibSaveOptions для тонкого налаштування того, як пишеться кожен документ