PDFlibPas, рідна бібліотека компонента PDF на VCL для Delphi та C++Builder, відтворює потік вмісту сторінки через свій клас TPDFContentStateTracker, взагалі не торкаючись полотна рендерингу. Подача трекеру по одному розібраному оператору за раз тримає біжучий запис графічного стану — матрицю поточного перетворення, текстову матрицю, межі обтинання та стек збереження q/Q — доступним для знімка до чи після виконання кожного оператора
Запитайте, де насправді приземляється прогін тексту на надрукованій сторінці, і самі сирі числа потоку вмісту щоразу вводитимуть вас в оману. TPDFContentProgram.GetTextRuns уже повідомляє точку прив'язки кожної інструкції показу тексту через поля OriginX та OriginY у TPDFTextRun, і коментарі полів прямо кажуть, що ця точка сидить у просторі тексту, уже згорнута через Tm, Td, TD та T*. Чого все ще бракує, і що ці коментарі кажуть, викликач мусить надати сам, — це CTM, активна саме на цій інструкції, — добуток кожного cm, з'єднаного дотепер, вкладений всередину скількох би пар q/Q не були відкриті в цій точці потоку
Навіщо відтворювати потік вмісту замість того, щоб його рендерити?
PDFlibPas тримає два окремі поняття графічного стану для двох окремих завдань, і цей поділ навмисний. Внутрішній запис стану рендерера несе живий дескриптор полотна пристрою, дескриптор регіону обтинання та кеші растеризації шрифту — реальні ресурси, прив'язані до поверхні, що зараз малюється, і безглузді, щойно ця поверхня зникне. TPDFContentGraphicsState не несе нічого з цього: це звичайний запис, обмежений значеннями, які ISO 32000-1 §8.4 визначає як досяжні лише з операторів потоку вмісту — CTM, стиль лінії, колір, стан тексту та похідні межі обтинання й контуру. Оскільки запис не тримає жодного посилання на полотно й жодного відкритого дескриптора файлу, викликач може розібрати потік вмісту, обійти його з TPDFContentStateTracker і продовжувати використовувати результатні знімки ще довго після того, як те, що вироблило байти, зникло
Як TPDFContentStateTracker будує CTM
TPDFContentStateTracker.Apply конкатенує шість операндів оператора cm у CTM трекера, використовуючи те саме перемноження заздалегідь, що визначає сам PDF: нова матриця M2 поєднується з поточним CTM як M2 × CTM, у конвенції рядкового вектора, де точка перетворюється як P′ = P × M (ISO 32000-1 §8.4). Частина, яку легко зробити неправильно, сидить у терміні перенесення, не в лінійній частині: власне перенесення M2 має пройти через компонент обертання-й-масштабу поточного CTM, перш ніж перенесення поточного CTM додається зверху. Пропустіть цей крок і жорстко закодуйте наївне покомпонентне поєднання натомість, і перший ізольований cm, який ви протестуєте, виглядатиме коректним, тоді як кожна координата нижче за течією від другого чи третього вкладеного cm тихо дрейфуватиме, а це саме той тип помилки, що переживає перегляд коду, бо модульний тест, який би це зловив, потребує принаймні двох з'єднаних перетворень, щоб провалитися
var
Prog: TPDFContentProgram;
Runs: TPDFTextRunArray;
States: TPDFContentGraphicsStateArray;
DeviceX, DeviceY: Double;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
try
Prog.Parse(ContentBytes);
Runs := Prog.GetTextRuns;
// One before-instruction snapshot per operator, computed in a single pass
States := Prog.TraceGraphicsStates(nil, False);
for I := 0 to High(Runs) do
begin
// OriginX/OriginY already fold in Tm/Td/TD/T*; only the CTM active
// at this instruction is still missing (ISO 32000-1 8.4)
with States[Runs[I].InstructionIndex].CTM do
begin
DeviceX := Runs[I].OriginX * M11 + Runs[I].OriginY * M21 + DX;
DeviceY := Runs[I].OriginX * M12 + Runs[I].OriginY * M22 + DY;
end;
LogTextOrigin(Runs[I].Text, DeviceX, DeviceY); // caller-supplied handler
end;
finally
Prog.Free;
end;
end;
Цикл вище відповідає на больову точку із вступу: TPDFContentProgram.GetTextRuns повертає OriginX і OriginY, уже згорнуті через Tm, Td, TD та T*, а TraceGraphicsStates(nil, False) надає одну частину, якої бракує, — CTM до інструкції на точному індексі, на якому був захоплений кожен прогін, — за один лінійний прохід по всій програмі. Передача nil дозволяє методу самому володіти приватним трекером для виклику й звільнити його внутрішньо, що правильний вибір для одноразового сканування; передача наявного екземпляра TPDFContentStateTracker натомість — те, що тримає стан безперервним через сторінку, зібрану з більш ніж одного потоку вмісту, оскільки ISO 32000-1 трактує масив /Contents сторінки як один логічний потік, і стеку q/Q доводиться з цим узгоджуватися
Текстова матриця переживає Q; графічний стан — ні
ISO 32000-1 §9.4.2 визначає Td, TD, Tm та T* як оператори, що будують текстову матрицю та матрицю текстового рядка всередині блоку BT/ET, і PDFlibPas тримає цю відмінність чіткою: Td і TD конкатенують чисте перенесення на матрицю текстового рядка, T* робить те саме, використовуючи від'ємне значення поточного інтерліньяжу, і лише Tm повністю замінює обидві матриці шістьма числами, які їй дано. BT скидає обидві матриці до одиниці, рівно один раз, на початку текстового об'єкта — але q і Q узагалі їх не торкаються. TPDFContentStateTracker.Apply спеціально обробляє coRestoreState саме з цієї причини: перед тим як виштовхнути збережений стан зі стека, він захоплює поточну текстову матрицю, матрицю текстового рядка та прапорець BT/ET, і повторно застосовує їх поверх того, що випадково тримав виштовхнутий стан, бо пара q/Q, огорнута навколо прогону тексту, не мала б повертати позицію тексту назад
var
Tracker: TPDFContentStateTracker;
Prog: TPDFContentProgram;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
Tracker := TPDFContentStateTracker.Create;
try
Prog.Parse('BT 100 700 Td q 2 0 0 2 0 0 cm (A) Tj Q (B) Tj ET');
for I := 0 to Prog.Count - 1 do
begin
Tracker.Apply(Prog[I]);
if Prog[I].Op in [coShowText, coRestoreState] then
LogState(Prog[I].OpName, Tracker.Snapshot); // caller-supplied handler
end;
finally
Tracker.Free;
Prog.Free;
end;
end;
Виконайте цю послідовність, і CTM, повідомлений на другому Tj, повертається до одиничного масштабу, який мав до q, — 2 0 0 2 0 0 cm усередині пари save/restore зникло, як цього вимагає q/Q. TextMatrix.DX на тій самій інструкції, однак, усе ще 100: Td, що його встановив, виконувався до q, тож це не той графічний стан, якого Q взагалі мав право торкатися, а інструмент, що припустив би інакше, повідомив би, що другий прогін гліфів починається з неправильної горизонтальної позиції на сторінці
Що відбувається, коли виконується оператор контуру обтинання?
Оператор W чи W* не звужує обтинання негайно; він лише записує, яке правило заливки використовувати, а фактичний перетин чекає на будь-який оператор малювання контуру, що йде за ним, включно з малювальником-порожньою-дією n, який автори PDF регулярно використовують саме для обтинання без малювання чого-небудь. TPDFContentStateTracker точно віддзеркалює цей двоетапний час: coClip та coClipEvenOdd лише встановлюють очікуваний прапорець правила обтинання, а EndCurrentPath — викликаний кожним оператором малювання контуру — те, що фактично перетинає межі очікуваного контуру в ClipMinX, ClipMinY, ClipMaxX та ClipMaxY. Правильне виконання цього поетапного підходу важить для самого контракту знімка до/після: знімок «до», взятий саме на інструкції W, усе одно має показувати старе, ширше обтинання, бо обтинання ще не набуло чинності в цій точці потоку, а згортання двох кроків в один тихо зламало б кожного викликача, що покладається на те, що стан «до» означає те, що каже
ClipBoundsExact каже викликачу, з якою з двох ситуацій він має справу, і воно лише коли-небудь True для одного прямокутника, вирівняного за осями, побудованого через re на в іншому порожньому контурі, — єдиної форми, яку PDFlibPas може представити точно як чотири числа. Усе інше — повернутий прямокутник, вигнутий контур, складений контур із кількома підконтурами, чи обтинання, побудоване з режиму рендерингу тексту, — усе одно виробляє ClipMinX через ClipMaxY, але з ClipBoundsExact, очищеним у False, чесний сигнал, що чотири числа — безпечна зовнішня межа, а не справжня форма обтинання; викликачі, яким потрібна лише ця межа, наприклад ізоляція прямокутного піддіапазону перед пониженням до пів-тону GDI, описаним у статті рендеринг сторінок PDF у 1-бітний монохром, можуть читати її напряму замість повторного виведення з геометрії сторінки
Криві Безьє: точна межа чи безпечна
Найдешевший спосіб обмежити кубічний сегмент Безьє — взяти опуклу оболонку його чотирьох контрольних точок, і це завжди безпечно, бо крива ніколи не виходить за неї, — але неглибока, широка крива може повідомити обмежувальний прямокутник, набагато більший за той, що крива фактично займає, що послаблює фільтрацію на основі обтинання саме тоді, коли це найбільше важить, на великих декоративних контурах. PDFlibPas натомість розв'язує складнішу задачу: для кожної осі він розв'язує похідну кубічної кривої на корені всередині відкритого інтервалу (0, 1) і обчислює криву на будь-яких знайдених коренях, разом з обома кінцевими точками, що є стандартним замкнутим способом отримати справжню вирівняну за осями протяжність кривої, а не перебільшену оцінку. Точність на кожну криву, однак, не переноситься на саме обтинання: щойно вигнутий контур стає контуром обтинання, ClipBoundsExact для нього все одно падає до False, бо обмежувальний прямокутник, хоч би яким точним, — усе одно не та сама форма, що й крива, яку він обмежує, а трекер стану радше скаже це, ніж дозволить викликачу припустити прямокутник там, де насправді крива
Читання стану до й після кожного оператора
Чи хоче викликач стан до, чи після, повністю залежить від того, що робить оператор: питання малювання чи тестування влучання про контур чи прогін тексту хоче стан таким, яким він був миттю до того, як цей оператор виконався, оскільки саме це фактично визначило, як оператор намалював, тоді як діагностичне питання про оператор, що встановлює стан, на кшталт gs, зазвичай хоче побачити, що він щойно змінив. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) відкриває саме цей вибір як одне булеве значення, обчислюючи один TPDFContentGraphicsState на інструкцію за один лінійний прохід по всій програмі незалежно від того, яка мить запитана. GetGraphicsState(InstructionIndex, AfterInstruction, State) пропонує той самий вибір до/після для однієї інструкції замість усієї програми, але заново відтворює з нульової інструкції при кожному виклику, щоб туди дістатися, тож сканування багатьох індексів викликом його в циклі коштує O(n²) проти одного виклику O(n) TraceGraphicsStates над тією самою програмою
var
Before, After: TPDFContentGraphicsState;
begin
// Same instruction index, two different instants: before vs. after it runs
Prog.GetGraphicsState(CmIndex, False, Before);
Prog.GetGraphicsState(CmIndex, True, After);
// Before.CTM reflects every earlier cm; After.CTM already folds in
// this instruction's own concatenation as well
end;
Життя зі спотвореними потоками вмісту
Два види спотвореного входу достатньо поширені в реальних виробниках PDF, що TPDFContentStateTracker мусить терпіти їх, а не провалюватися на них. Перший — контур, що охоплює межу q/Q: поточний контур, поточна точка та кількість підконтурів — не параметри графічного стану — ISO 32000-1 §8.4 покриває, що q і Q зберігають та відновлюють, а поточний контур, що будується, серед них немає, — тож TPDFContentStateTracker відстежує ці дані повністю поза збереженим станом, і підконтур, розпочатий до q, усе ще там, ненамальований, одразу після відповідного Q. Другий — голий Q без жодного відповідного q ніде раніше в потоці, не рідкість у виводі генераторів, що збирають фрагменти потоку вмісту конкатенацією й помиляються в обліку. TPDFContentStateTracker.RestoreUnderflowCount рахує кожну таку подію замість того, щоб піднімати виняток чи пошкоджувати стан: незіставлений Q просто залишає поточний графічний стан точно таким, яким був, ніби ця інструкція була порожньою дією, тож решта потоку продовжує відтворюватися на здоровому стані, а викликач усе ще може вирішити пізніше, з підрахунку, чи вхід вартий позначення назад тому, хто його виробив
Композиція CTM, незалежність текстової матриці від q/Q та поетапна реалізація контуру обтинання не залежать від того, як чи чи взагалі потік вмісту колись малюється, і це саме та суть: той самий знімок TPDFContentStateTracker коректний незалежно від того, чи сторінку взагалі ніколи не рендерять, чи вона ось-ось буде передана тому бекенду, який PDFlibPas обирає для цього файлу, включно з перемиканням рушія під час виконання, описаним у посібнику з рендерингу PDF у кількох рушіях у PDFlibPas. Аналіз вмісту, відображення координат та інструментарій редагування можуть повністю працювати на виводі трекера, задовго до чи повністю без того, щоб коли-небудь просити рендерер долучитися
Відтворення потоку вмісту через TPDFContentStateTracker — частина фреймворку структурованого редагування вмісту, вбудованого в PDFlibPas, рідну бібліотеку компонента PDF на VCL для Delphi та C++Builder