Technical Article

Как работят PDF графиките: Потоци от съдържание и оператори

PDF страницата не съхранява пиксели и не съхранява дърво от обекти на форми по начина, покоито го прави SVG. Тя съхранява програма. Всяка линия, крива, запълване и разположено изображение на страницата е резултат от изпълнението на поредица от оператори в поток от съдържание (content stream), отгоре надолу, спрямо текущото графично състояние. Разберете ли този факт, по-голямата част от поведението на формата спира да бъде изненадваща: защо запълването се нуждае от отделен оператор за изрисуване след изграждането на пътя, защо цветовете и ширините на линиите изтичат от една форма в следващата, освен ако не ги ограничите, защо един и същ код за чертане може да се приземи на напълно различни места след една-единствена координатна трансформация. Това е обиколка на този модел на изпълнение, дефиниран в ISO 32000: операторите, които срещате, когато отворите поток от съдържание, и правилата, които определят какво се показва на страницата.

Потокът от съдържание е постфиксен байткод

Потокът от съдържание е плоска последователност от байтове от операнди, последвани от оператори. Операндите идат първи, а операторът, който ги консумира, идва последен, което е обратното на извикване на функция и е идентично със стекова машина: избутайте числата, след което дайте командата. Няма влагане, няма синтаксис за изрази, няма променливи. Очертанието на триъгълник се състои от пет реда:

100 100 m    % moveto: start a new subpath at (100, 100)
200 200 l    % lineto: add a segment to (200, 200)
300 100 l    % lineto: add a segment to (300, 100)
h            % closepath: connect back to the start
S            % stroke: paint the path outline

Операторите са кратки по дизайн. Реалната страница съдържа хиляди такива, обикновено компресирани с FlateDecode. Цената на тази компактност е, че потокът не носи структура, която можете да заявите: програмата за преглед не може да попита „къдÐ?е заглавието на тази страницаâ€? тя може само да изпълни програмата и да види къде пада мастилото. Това е основната причина, поради която извличането на текст от произволни PDF файлове е трудно.

Координатното начало е долу вляво, а Y расте нагоре

Преди която и да е координата да придобие смисъл, трябва да знаете къде се намира (0, 0). PDF поставя координатното начало в долния ляв ъгъл на страницата, като X нараства надясно, а Y нараства нагоре, измерено в точки при 72 точки на инч (ISO 32000-2 §8.3.2). На страница с размер US Letter горният ръб се намира при y = 792, а не при y = 0. Всеки, който идва от компютърната графика, където координатното начало е горе вляво и Y расте надолу, бърка това от първия опит и начертава първата линия извън дъното на страницата. Мерната единица също така е независима от носителя: 72 единици са един инч, независимо дали страницата се изобразява на екрана на телефон или на принтер.

Повечето библиотеки за чертане на страници наследяват тази конвенция директно. В HotPDF, например, TextOut и извикванията за пътища измерват от долния ляв ъгъл в точки, така че стойност близо до височината на страницата поставя съдържание в горната част:

// HotPDF, Delphi: y measured from the bottom edge upward, in points
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700);   // near the top of the page
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke;             // emits the moveto/lineto/stroke operators

Тази последователност от извиквания се компилира точно до операторите m, l и S по-горе. Библиотеката е просто „машинописецâ€?за потока от съдържание, нищо повече, и познаването на това какво излъчва тя е това, което ви позволява да разсъждавате върху резултата, когато дадена форма се приземи някъде, където не сте очаквали.

Изградете пътя, след което го изрисувайте

PDF разделя изграждането на пътя (path construction) от неговото изрисуване (path painting) и това разделение не е педантичност. Първо описвате форма с конструктивни оператори, които не добавят нищо видимо, след което издавате един-единствен оператор за изрисуване, който решава какво да прави с натрупания път. Един и същ триъгълник може да бъде очертание, плътно запълване или и двете, в зависимост единствено от командата, с която завършвате.

Операторите за изграждане са малко на брой. m започва нов подпът (subpath) от точка. l добавя прав сегмент. c добавя кубична крива на Безие от шест операнда â€?две контролни точки и крайна точка. re е пряк път, който добавя цял правоъгълник от четворката x, y, ширина, височина. h затваря текущия подпът обратно до неговото начало. Никой от тях не поставя мастило на страницата; те просто натрупват геометрия.

200 250 m                    % start the subpath
300 350 400 450 500 250 c    % cubic Bezier: two control points, then endpoint
150 200 re                   % a 150 x 200 rectangle, added as its own subpath
h                            % close

Оригиналният пример използва вече остарелия вариант y на оператора за крива; c с трите си изрични точки е формата, която ще видите на практика и която трябва да използвате. След като пътят съществува, един оператор за изрисуване го завършва. Този речник е малък и си струва да се запомни, защото всяка форма на всяка страница завършва с едно от тези:

  • `S` очертава пътя (stroke), използвайки текущата ширина на линията и цвят на очертанието.
  • `f` запълва вътрешността (fill), използвайки текущия цвят на запълване и правилото за ненулево навиване (nonzero winding rule).
  • `f*` запълва, използвайки правилото четно-нечетно (even-odd rule), което е важно за самопресичащи се форми и форми с дупки.
  • `B` запълва и след това очертава в една операция; b първо затваря пътя.
  • `n` не изрисува нищо, което е начинът пътят да се превърне в област за изрязване (clip region), без да оставя видима следа.

Правилото за навиване е частта, която хората често бъркат. Nonzero (f, B) броя пресичанията със знак на лъч от тестовата точка и запълва навсякъде, където броят не е нула, така че отворът остава празен само ако неговият подпът е навит в посока, обратна на външния. Even-odd (f*, B*) превключва при всяко пресичане, независимо от посоката. Ако форма тип „поничкаâ€?излезе плътна, вътрешният кръг е навит в същата посока като външния, и трябва или да го обърнете, или да преминете към правилото четно-нечетно.

Цветът е режим, а не параметър

Цветът в потока от съдържание е „лепкавâ€?(sticky). Задавате цвят и той остава в сила, докато не зададете друг или не възстановите предишно състояние â€?ето защо неограничената промяна на цвета тихо оцветява всичко, начертано след нея. PDF също така пази цвета на запълване и цвета на очертанието като две независими настройки, с оператори с малки букви за запълване и главни букви за очертание. Цветовите пространства на устройствата имат свои собствени съкращения:

0.5 g                % DeviceGray fill, mid gray (0 = black, 1 = white)
0.2 0.6 0.8 rg       % DeviceRGB fill
0.8 0.2 0.1 RG       % DeviceRGB stroke (uppercase = stroke)
0.2 0.8 0.0 0.1 k    % DeviceCMYK fill

DeviceRGB е подходящ за изход на екран, DeviceCMYK е това, което се очаква при печат, а DeviceGray е най-икономичният избор за монохромно съдържание. Пространствата на устройствата са удобни, но не са калибрирани: една и съща тройка RGB може да се визуализира различно на два монитора, което е проблемът, който базираните на ICC цветови пространства и изходните профили на PDF/A (output intents) съществуват да решат. За прецизна работа с цветове избирате калибрирано пространство с cs и CS и задавате компоненти с sc and scn, но за обикновени документи съкращенията за устройства вършат основната работа. Библиотеката обвива тези извиквания в типизирани функции. HotPDF, например, приема единствен TColor и излъчва съответните оператори:

Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150);  // x, y, width, height
Pdf.CurrentPage.Fill;

Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50);           // x, y, radius
Pdf.CurrentPage.Fill;

Графичното състояние и q/Q стекът

Всичко, което не е самият път, живее в графичното състояние: текущата матрица на трансформациите (CTM), цветовете на запълване и очертаване, ширината на линията, моделът на щриховане, областта за изрязване, алфа каналът. Състоянието е глобално и променливо, така че единственият безопасен начин да направите локална промяна е да запишете цялото състояние, да го промените, да начертаете формата и да го възстановите обратно. Точно това правят операторите q и Q. q вкарва копие на текущото състояние в стек; Q го извлича, отхвърляйки всяка промяна, направена след съответното q.

q                    % save the entire graphics state
2 0 0 2 100 100 cm   % concatenate a transform: scale 2x, translate to (100,100)
0.8 g                % gray fill, scoped to this block
% ... draw scaled, gray content ...
Q                    % restore: transform and color revert

Небалансираните оператори q и Q са често срещан начин, по който ръчно изграден или съшит поток от съдържание се счупва. Заблуден q без съответстващ Q оставя стека препълнен, когато страницата приключи; допълнителен Q води до недостиг на елементи в стека (underflow). И в двата случая четецът може да запази старо изрязване или трансформация в сила, и съдържанието изчезва или попада на грешно място. Когато графиките изчезнат без видима причина, която пътят може да обясни, първо одитирайте стека на състоянието.

CTM трансформира всяка координата

Текущата матрица на трансформациите (CTM - Current Transformation Matrix) стои между числата във вашите оператори и самата страница. Всяка координата се умножава по CTM преди чертането, така че промяната на матрицата променя мястото и начина на поява на цялото последващо чертане, без да се докосва нито една координата на пътя. Операторът cm обединява нова матрица с текущата, приемайки шест операнда, които съответстват на афинната матрица [a b c d e f]:

1 0 0 1 100 50 cm        % translate by (100, 50): e and f carry the offset
2 0 0 1.5 0 0 cm         % scale x by 2, y by 1.5: a and d are the scale factors
0.707 0.707 -0.707 0.707 0 0 cm   % rotate 45 degrees (cos/sin in a, b, c, d)

Две неща объркват хората. Първо, cm съставя (composes) вместо да заменя, така че трансформациите се натрупват и редът има значение: мащабирането, последвано от преместване, не е същото като преместване, последвано от мащабиране. Второ, ротацията и мащабирането се извършват около текущото координатно начало, а не около центъра на вашата форма, така че за да завъртите нещо на място, го премествате до началото, завъртате и след това премествате обратно â€?всичко това обвито в q/Q. Тази съща матрица служи и за разполагане на изображения â€?последното парче, което си струва да се види.

Изображенията и повторно използваемото съдържание са XObjects

Растерните изображения не живеят директно в потока от съдържание. Те се съхраняват като графични XObjects (image XObjects) â€?външни обекти със собствен речник, описващ ширината, височината, дълбочината на цвета, цветовото пространство и компресиращия филтър, а потокът от съдържание само препраща към тях. Снимка, подкрепена с JPEG, се декларира така:

/Photo <<
  /Type /XObject
  /Subtype /Image
  /Width 640
  /Height 480
  /BitsPerComponent 8
  /ColorSpace /DeviceRGB
  /Filter /DCTDecode        % the image data is a JPEG stream
>>

Изображението XObject се рисува в единичния квадрат (unit square): то винаги заема областта от (0, 0) до (1, 1) в пространството на потребителя. Не му подавате позиция или размер. Вместо това настройвате CTM така, че този единичен квадрат да се напасне към желания от вас правоъгълник, след което го извиквате с Do. Ето защо поставянето на изображение винаги представлява трансформация, последвана от извикване, обвито в save/restore, така че мащабът да не изтече към следващата операция:

q
640 0 0 480 50 300 cm    % map the unit square to a 640x480 box at (50, 300)
/Photo Do                % paint the image XObject
Q

Същият механизъм Do управлява и формата XObjects (form XObjects), който съдържа повторно използваемо парче графики, лого или повтарящ се печат, като негов собствен поток от съдържание с ограничаваща кутия (bounding box). Дефинирайте го веднъж, извикайте го многократно с различен CTM и байтовете ще се появят във файла само веднъж. Повечето библиотеки скриват това зад едно извикване за разполагане: HotPDF регистрира растерно изображение с AddImage и го разполага с ShowImage, приемайки изрични x, y, ширина и височина, вместо да ви кара да изграждате матрицата на ръка:

var
  Bmp: TBitmap;
  ImgIndex: Integer;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.LoadFromFile('logo.bmp');
    ImgIndex := Pdf.AddImage(Bmp, icFlate);
    // x, y (bottom-left), width, height, rotation angle
    Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
  finally
    Bmp.Free;
  end;
end;

Под този единствен ред библиотеката записва речника на графичния XObject, настройва CTM за оразмеряване и позициониране на единичния квадрат и излъчва Do. Този базов модел си струва да се знае, защото обяснява всеки странен резултат: разтегнато изображение е CTM с несъответстващи фактори на мащаба, идентично лого на четиридесет страници е една форма XObject, извикана четиридесет пъти, а обърнато наопаки изображение е просто обръщане на знака в матрицата, а не повреден файл.

Накъде води това

Графичният модел е малък, след като видите формата му. Потокът от съдържание е постфиксен байткод, изпълняван спрямо променливо състояние; координатите започват от долния ляв ъгъл и преминават през CTM; пътищата се изграждат тихо и се изрисуват с един конкретен оператор; настройките за цвят и линия остават в сила, докато не ги оградите с q/Q; изображенията и повторно използваемите графики са XObjects, разположени чрез трансформиране на единичен квадрат. Почти всеки объркващ резултат при изобразяване се свежда до едно от тези пет правила. Ако искате да видите как тези графични оператори се намират в рамките на по-големия обектен модел â€?речниците на страниците и таблицата с кръстосани препратки, които сочат към тях â€?техническият преглед на структурата на PDF файлове покрива този слой, а изграждането на обикновен PDF от нулата проследява байтовете от началото до края. Чертането на текст живее в собствена фамилия оператори и има свои собствени капани, разгледани в придружаващата статия за работа с текст и шрифтове в PDF.

Показаните тук извиквания за чертане в Delphi â€?MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage и ShowImage â€?са част от HotPDF Component за Delphi и C++Builder, който генерира тези оператори на потока от съдържание вместо вас.