Техническа статия

Състояние на потока от съдържание в PDFlibPas: проследяване на CTM и клипинга

PDFlibPas, нативната VCL PDF библиотека за 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 вътре в двойката за запазване и възстановяване е изчезнал, както изисква 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, тоест честен сигнал, че четирите числа са безопасна външна граница, а не истинската форма на клипа; извикващите, които се нуждаят само от тази граница, например за изолиране на правоъгълна област преди GDI преобразуването към по-ниска разрядност, описано в изобразяването на PDF страници като 1-битови монохромни изображения, могат да я прочетат директно, вместо да я извеждат отново от геометрията на страницата

Криви на Безие: точна или безопасна граница

Най-евтиният начин да ограничите кубичен сегмент на Безие е да вземете изпъкналата обвивка на четирите му контролни точки и това винаги е безопасно, защото кривата никога не излиза от нея, но плитка и широка крива може да даде ограничителна рамка, много по-голяма от реално заеманото пространство, което отслабва филтрирането по клип точно когато то е най-важно, при големи декоративни пътища. PDFlibPas решава по-строгата задача: за всяка ос решава производната на кубичната крива за корените в отворения интервал (0, 1) и изчислява кривата при намерените корени заедно с двете крайни точки, което е стандартният затворен начин за получаване на истинския осово подравнен обхват на крива, а не надценена граница. Точността за всяка крива обаче не се пренася върху самия клип: когато извита линия стане клипиращ път, ClipBoundsExact все още се нулира за нея, защото ограничителната рамка, колкото и стегната да е, не е същата форма като кривата, която ограничава, а проследяващият обект предпочита да каже това ясно, вместо да позволи на извикващия да приеме правоъгълник там, където всъщност има крива

Четене на състоянието преди и след всеки оператор

Дали извикващият код се нуждае от състоянието преди или след инструкцията зависи изцяло от действието на оператора: въпрос за изчертаване или попадение върху път или текстов фрагмент изисква състоянието в мига непосредствено преди изпълнението, тъй като именно то е определило начина на изчертаване, докато диагностичен въпрос за оператор, задаващ състояние, като gs, обикновено иска да види какво току-що е променил. TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) предоставя точно този избор като един булев параметър, като изчислява по един TPDFContentGraphicsState за всяка инструкция в един линеен проход през цялата програма, независимо кой момент е поискан. GetGraphicsState(InstructionIndex, AfterInstruction, State) предлага същия избор преди или след за една инструкция, а не за цялата програма, но при всяко извикване възпроизвежда потока от инструкция нула до нужния индекс, затова обхождането на много индекси в цикъл чрез него струва O(n²) вместо едно извикване на TraceGraphicsStates със сложност O(n) за същата програма

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, нативната VCL PDF библиотека за Delphi и C++Builder