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 единици са един инч, независимо дали страницата се рендира на екрана на телефон или на imagesetter
Повечето библиотеки за рисуване на страници наследяват тази конвенция директно. В 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 отделя изграждането на пътя от рисуването на пътя и разделението не е педантичност. Първо описвате форма с конструкторски оператори, които не добавят нищо видимо, след което издавате един единствен оператор за рисуване, който решава какво да прави с натрупания път. Същият триъгълник може да бъде контур, плътно запълване или и двете, в зависимост само от глагола, с който завършвате
Конструкторските оператори са малко. m стартира нов подпът в дадена точка. 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очертава контура на пътя, като използва текущата ширина на линията и цвят на щрихаfзапълва вътрешността, като използва текущия цвят на запълване и правилото за ненулево навиванеf*запълва с помощта на правилото четно-нечетно, което има значение за самопресичащи се форми и форми с дупкиBзапълва и след това очертава в една операция;bпърво затваря пътяnне рисува нищо, което е начинът, по който даден път се превръща в регион на изрязване, без да оставя видима следа
Правилото за навиване е частта, която хората бъркат. Ненулево (f, B) брои пресичанията със знак на лъч от тестовата точка и запълва навсякъде, където броят не е нула, така че дупка остава празна само ако нейният подпът се навива в посока, обратна на външния. Четно-нечетно (f*, B*) превключва при всяко пресичане независимо от посоката. Ако форма на "поничка" излезе плътна, вътрешният кръг е навит по същия начин като външния и вие или го обръщате, или превключвате към четно-нечетно
Цветът е режим, а не параметър
Цветът в потока от съдържание е лепкав. Задавате цвят и той остава зададен, докато не зададете друг или не възстановите по-ранно състояние, поради което промяна на цвета, която не е в скоби, тихо оцветява всичко, нарисувано след нея. 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 изходните намерения съществуват, за да решат. За критична към цвета работа избирате калибрирано пространство с cs и CS и задавате компоненти със sc и 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
Всичко, което не е самият път, живее в графичното състояние: текуща матрица на трансформация, цветове за запълване и щрих, ширина на линията, модел на пунктир, регион на изрязване, алфа. Състоянието е глобално и изменяемо, така че единственият безопасен начин да се направи локална промяна е да се запази цялото нещо, да се модифицира, да се нарисува и да се върне обратно. Това е, което правят 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 го изпразва. И в двата случая програмата за преглед може да запази стар клип или трансформация в сила и съдържанието да изчезне или да попадне на грешното място. Когато графиките изчезнат без причина, която пътят може да обясни, първо одитирайте стека на състоянието
CTM трансформира всяка координата
Текущата матрица на трансформация (CTM) седи между числата във вашите оператори и действителната страница. Всяка координата се умножава по 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 композира, а не замества, така че трансформациите се натрупват и редът има значение: мащабирането и след това транслирането не е същото като транслирането и след това мащабирането. Второ, ротацията и мащабирането се въртят около текущото начало, а не около центъра на вашата форма, така че, за да завъртите нещо на място, вие го транслирате до началото, завъртате, след това транслирате обратно, всичко това обвито в q/Q. Тази същата матрица е това, което поставя изображения, последното парче, което си струва да се види
Изображенията и съдържанието за многократна употреба са 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
>>
Image XObject се рисува в единичния квадрат: той винаги заема региона от (0, 0) до (1, 1) в потребителското пространство. Не му подавате позиция или размер. Вместо това задавате CTM така, че единичният квадрат да се съпостави с правоъгълника, който искате, след което го извиквате с Do. Ето защо поставянето на изображение винаги е трансформация, последвана от извикване, обвити в запазване/възстановяване, така че мащабът да не изтече в следващата операция:
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 задвижва form XObjects, които държат парче графики за многократна употреба, лого или повтарящ се печат, като свой собствен поток от съдържание с ограничаваща кутия. Дефинирайте го веднъж, извикайте го много пъти с различна 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;
Под този един ред библиотеката записва речника на image XObject, задава CTM за оразмеряване и позициониране на единичния квадрат и извежда Do. Моделът отдолу е този, който си струва да се знае, защото той обяснява всеки странен резултат: разтегнато изображение е CTM с несъответстващи мащабни фактори, идентично лого на четиридесет страници е един form XObject, извикан четиридесет пъти, а изображение, което се рендира с главата надолу, е обръщане на знак в матрицата, а не повреден файл
Накъде води това
Графичният модел е малък, щом видите неговата форма. Потокът от съдържание е постфиксен байткод, работещ срещу изменяемо състояние; координатите започват от долу-ляво и преминават през CTM; пътищата се изграждат безшумно и се рисуват с един преднамерен оператор; настройките за цвят и линия се запазват, докато не ги поставите в скоби с q/Q; изображенията и графиките за многократна употреба са XObjects, поставени чрез трансформиране на единичен квадрат. Почти всеки объркващ резултат от рендирането се свежда до едно от тези пет правила. Ако искате да видите как тези графични оператори седят вътре в по-големия обектен модел, речниците на страниците и таблицата за кръстосани препратки, които сочат към тях, техническият преглед на структурата на PDF файл покрива този слой, а изграждането на прост PDF от нулата обхожда байтовете от край до край. Чертането на текст живее в свое собствено семейство оператори и има свои собствени капани, разгледани в придружаващата статия за обработка на PDF текст и шрифтове
Показаните тук извиквания за рисуване в Delphi, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage и ShowImage, са част от HotPDF Component за Delphi и C++Builder, който извежда тези оператори за поток от съдържание вместо вас