Сторінка PDF не зберігає пікселі і не зберігає дерево об'єктів фігур так, як це робить SVG. Вона зберігає програму. Кожна лінія, крива, заливка та розміщене зображення на сторінці є результатом виконання послідовності операторів у потоці вмісту зверху вниз у поточному графічному стані. Зрозумійте цей один факт, і більшість поведінки формату перестане вас дивувати: чому заливка потребує окремого оператора малювання після побудови контуру, чому кольори та товщина ліній перетікають з однієї фігури на іншу, якщо їх не брати в дужки, чому той самий код малювання може опинитися в абсолютно різних місцях після єдиного перетворення координат. Це огляд тієї самої моделі виконання, як вона визначена в ISO 32000: оператори, які ви зустрічаєте, коли відкриваєте потік вмісту, та правила, що вирішують, що саме з'явиться на сторінці
Потік вмісту є постфіксним байт-кодом
Потік вмісту - це плоска послідовність байтів з операндів, за якими слідують оператори. Спочатку йдуть операнди, а оператор, який їх споживає, йде останнім, що є зворотним до виклику функції і ідентичним до стекової машини: заштовхніть числа, а потім виконайте дію. Тут немає вкладеності, немає синтаксису виразів, немає змінних. Контур трикутника - це п'ять рядків такого коду:
100 100 m % moveto: почати новий підконтур у (100, 100)
200 200 l % lineto: add a segment to (200, 200)
300 100 l % lineto: add a segment to (300, 100)
h % closepath: з’єднати із початком
S % stroke: намалювати контур шляху
Оператори навмисно стислі. Реальна сторінка складається з тисяч таких операторів, які зазвичай стиснуті за допомогою 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 вимірюється від нижнього краю вгору, у пунктах
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700); // біля верхнього краю сторінки
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke; // виводить оператори moveto/lineto/stroke
Ця послідовність викликів компілюється в ті самі оператори m, l та S, що наведені вище. Бібліотека є лише друкаркою для потоку вмісту, і розуміння того, що вона генерує, дозволяє вам зрозуміти результат, коли фігура опиняється не там, де ви очікували
Побудуйте контур, потім намалюйте його
PDF розділяє побудову контуру та його малювання, і це розділення не є педантизмом. Спочатку ви описуєте фігуру за допомогою операторів побудови, які не додають нічого видимого, а потім видаєте один оператор малювання, який вирішує, що робити з накопиченим контуром. Той самий трикутник може бути контуром, суцільною заливкою або і тим, і іншим, залежно лише від того, яким дієсловом ви завершуєте код
Операторів побудови небагато. m починає новий підконтур з точки. l додає прямий сегмент. c додає кубічну криву Безьє з шести операндів, двох контрольних точок і кінцевої точки. re - це швидкий спосіб додати цілий прямокутник із четвірки параметрів x, y, ширина, висота. h замикає поточний підконтур назад до його початку. Жоден з них не наносить фарбу на сторінку; вони лише накопичують геометрію
200 250 m % почати підконтур
300 350 400 450 500 250 c % кубічна Bezier: дві контрольні точки, потім кінцева точка
150 200 re % прямокутник 150 x 200, доданий як окремий підконтур
h % close
Оригінальний приклад використовував уже застарілий варіант оператора кривої y; c з його трьома явними точками - це форма, яку ви побачите на практиці, і саме до неї слід звертатися. Як тільки контур існує, один оператор малювання завершує його. Словник тут невеликий і його варто запам'ятати, оскільки кожна фігура на кожній сторінці закінчується одним з них:
Sобводить контур, використовуючи поточну товщину лінії та колір обведенняfзаливає внутрішню область, використовуючи поточний колір заливки та правило ненульового обертання (nonzero winding rule)f*заливає за правилом парності-непарності (even-odd rule), що має значення для фігур, які перетинають самі себе, та фігур з отворамиBзаливає і потім обводить за одну операцію;bспочатку замикає контурnне малює нічого, саме так контур стає областю відсікання, не залишаючи видимого сліду
Правило обертання (winding rule) - це те, в чому люди часто помиляються. Ненульове (f, B) підраховує знакові перетини променя від тестової точки і заливає всюди, де кількість не дорівнює нулю, тому отвір залишається порожнім лише тоді, коли його підконтур обертається в протилежному напрямку до зовнішнього. Правило парності-непарності (f*, B*) перемикається на кожному перетині незалежно від напрямку. Якщо фігура "пончик" виходить суцільною, це означає, що внутрішнє коло обертається в тому ж напрямку, що й зовнішнє, і вам потрібно або розгорнути його, або переключитися на правило парності-непарності
Колір - це стан, а не параметр
Колір у потоці вмісту є "липким". Ви встановлюєте колір, і він залишається таким, поки ви не встановите інший або не відновите попередній стан, саме тому зміна кольору без дужок непомітно тонує все, що малюється після неї. PDF також зберігає колір заливки та колір обведення як два незалежних налаштування, використовуючи малі літери для операторів заливки та великі для обведення. Апаратні колірні простори (device color spaces) мають власні скорочення:
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 % зберегти весь графічний стан
2 0 0 2 100 100 cm % concatenate a transform: scale 2x, translate to (100,100)
0.8 g % сіра заливка в межах цього блоку
% ... draw scaled, gray content ...
Q % restore: перетворення та колір повертаються
Незбалансовані q і Q - це найпоширеніша причина, чому зібраний вручну або склеєний потік вмісту працює неправильно. Зайвий q без відповідного Q залишає стек глибоким після завершення сторінки; зайвий Q викликає спустошення стека. У будь-якому випадку програма перегляду може залишити чинним старе відсікання або перетворення, і вміст зникне або опиниться не в тому місці. Коли графіка зникає без видимих на те причин, які можна було б пояснити контуром, спочатку перевірте стек станів
CTM перетворює кожну координату
Поточна матриця перетворень (CTM) знаходиться між числами у ваших операторах і реальною сторінкою. Кожна координата множиться на CTM перед тим, як щось буде намальовано, тому зміна матриці змінює те, де і як з'являтимуться всі наступні малюнки, не торкаючись жодної координати контуру. Оператор cm об'єднує нову матрицю з поточною, приймаючи шість операндів, які відображаються в афінній матриці [a b c d e f]:
1 0 0 1 100 50 cm % перемістити на (100, 50): e та f містять зміщення
2 0 0 1.5 0 0 cm % масштабувати x у 2 рази, y у 1.5: a та d — коефіцієнти масштабу
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
Растрові зображення не живуть безпосередньо у потоці вмісту. Вони зберігаються як об'єкти зображень XObjects, зовнішні об'єкти з власним словником, що описує ширину, висоту, бітову глибину, колірний простір і фільтр стиснення, а потік вмісту лише посилається на них. Фотографія на базі JPEG оголошує себе так:
/Photo <<
/Type /XObject
/Subtype /Image
/Width 640
/Height 480
/BitsPerComponent 8
/ColorSpace /DeviceRGB
/Filter /DCTDecode % дані зображення — потік JPEG
>>
Об'єкт зображення XObject малює в межах одиничного квадрата: він завжди займає область від (0, 0) до (1, 1) в просторі користувача. Ви не передаєте йому позицію або розмір. Натомість ви встановлюєте CTM так, щоб одиничний квадрат відображався на потрібний вам прямокутник, а потім викликаєте його за допомогою Do. Ось чому розміщення зображення - це завжди перетворення, за яким слідує виклик, загорнуте в збереження/відновлення, щоб масштаб не перетікав на наступну операцію:
q
640 0 0 480 50 300 cm % відобразити одиничний квадрат у поле 640x480 у (50, 300)
/Photo Do % намалювати XObject зображення
Q
Цей самий механізм Do керує об'єктами форми 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;
Під цим одним рядком бібліотека записує словник об'єкта зображення XObject, встановлює CTM для визначення розміру та позиціонування одиничного квадрата і видає Do. Модель, що лежить в основі, варто знати, оскільки вона пояснює кожен дивний результат: розтягнуте зображення - це CTM з невідповідними коефіцієнтами масштабування, ідентичний логотип на сорока сторінках - це один об'єкт форми XObject, викликаний сорок разів, а зображення, що рендериться догори дриґом - це зміна знака в матриці, а не пошкоджений файл
До чого це призводить
Графічна модель невелика, коли ви бачите її форму. Потік вмісту - це постфіксний байт-код, що виконується в змінюваному стані; координати починаються зліва внизу і проходять через CTM; контури будуються тихо і малюються одним цілеспрямованим оператором; налаштування кольору та ліній зберігаються, поки ви не візьмете їх у дужки q/Q; зображення та багаторазова графіка - це XObjects, що розміщуються шляхом перетворення одиничного квадрата. Майже кожен незрозумілий результат рендерингу зводиться до одного з цих п'яти правил. Якщо ви хочете побачити, як ці графічні оператори сидять у більшій об'єктній моделі, словниках сторінок та таблиці перехресних посилань, що вказують на них, це розглядається в технічному огляді структури файлу PDF, а створення простого PDF з нуля крок за кроком описує всі байти від початку до кінця. Малювання тексту має власну сімейство операторів і власні підводні камені, які описані в супутній статті про обробку тексту та шрифтів у PDF
Виклики малювання Delphi, показані тут: MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage та ShowImage, є частиною компонента HotPDF для Delphi та C++Builder, який генерує ці оператори потоку вмісту за вас