Коли на сторінці PDF немає TrimBox, її ефективний TrimBox — це CropBox сторінки, а коли немає і CropBox — це MediaBox. BleedBox і ArtBox дотримуються того ж правила. PDFlibPas, PDF Library для Delphi, застосовує цей усталений ланцюг послідовно в GetPageBox, HasPageBox і CapturePageEx з v3.539.44, і він ігнорує виробничі бокси, поставлені на вузол /Pages, бо ISO 32000-1 не дозволяє їм успадковуватися
Це звучить як виноска, поки ви не спускуєте роботу в верстку. Уявіть книжковий блок із MediaBox 6,25 × 9,25 дюйма, CropBox, виставленим на 6 × 9 дюймів обрізу, і без TrimBox, бо той, хто його експортував, навіть не подумав написати один. Попросіть trim-бокс — отримайте media-бокс, і кожна клітинка на вашому друкарському аркуші тягне восьму частину дюйма вивороту та обрізкового поля до сусіда. У PDFlibPas були дефекти точно в цій зоні, полагоджені у v3.539.42 і v3.539.44, і те, як їх полагодили, каже щось про те, як семантику боксів сторінок варто реалізовувати в будь-якій PDF-бібліотеці
Який бокс застосовується, коли на сторінці немає TrimBox?
Відповідь — фіксований усталений ланцюг з ISO 32000-1 §14.11.2: CropBox усталюється до MediaBox, а BleedBox, TrimBox і ArtBox кожен усталюється до CropBox. Ніщо, крім CropBox, не усталюється прямо до MediaBox. Сторінка, що визначає лише MediaBox, отже має п'ять ідентичних боксів, а сторінка з MediaBox плюс CropBox має чотири бокси, рівні CropBox
| Бокс | BoxType у PDFlibPas | Усталене за відсутності | Успадковується з /Pages |
|---|---|---|---|
| MediaBox | 1 | Немає, запис обов'язковий | Так |
| CropBox | 2 | MediaBox | Так |
| BleedBox | 3 | CropBox | Ні |
| TrimBox | 4 | CropBox | Ні |
| ArtBox | 5 | CropBox | Ні |
Двоступеневий ланцюг важить тому, що CropBox може сам бути успадкованим. Ефективний TrimBox сторінки без власних TrimBox і CropBox — це CropBox найближчого предка, який його має, а в разі невдачі — успадкований MediaBox. Специфікація додає ще одне правило, яке легко забути: crop-, bleed-, trim- і art-бокси не мають виходити за media box, а якщо виходять, вони фактично скорочуються до свого перетину з ним. PDFlibPas звітує кожен бокс таким, як він збережений у файлі, тож валідатор, що обробляє недовірений вхід, має сам притискати до MediaBox
Які атрибути сторінки вузол /Pages може передавати вниз?
Рівно чотири: Resources, MediaBox, CropBox і Rotate. ISO 32000-1 §7.7.3.4 визначає успадкування атрибутів, а Table 30 позначає як успадковувані лише ті чотири записи об'єкта сторінки. BleedBox, TrimBox і ArtBox належать листковій сторінці. TrimBox, записаний у вузол /Pages, — це не успадковане значення; це нестандартний ключ, який конформний читач ігнорує
Нестандартні файли такі існують, зазвичай з одним TrimBox на кореневому вузлі дерева сторінок як стенографією для «кожна сторінка має цей обріз». Стенографія виглядає правильно в будь-якому інструменті, що проходить /Parent для кожного ключа, і в тому й проблема: файл тепер означає дві речі залежно від того, хто його читає. Читач, що слідує специфікації, не бачить TrimBox і користується CropBox, тоді як читач, що успадковує все, бачить батьківське значення. У препрес-конвеєрі та амбівалентність закінчується на друкарському аркуші
Робочі процеси PDF/X (ISO 15930) спираються на TrimBox для розміру в світі, а профілі PDF/X вимагають, щоб кожна сторінка оголошувала TrimBox чи ArtBox. Бокс, припаркований на вузлі /Pages, не задовольняє ту вимогу, бо ключ ніколи не досягає об'єкта сторінки. Preflight має позначати такі файли, а не тихо читати їх так чи інакше
Що PDFlibPas робив неправильно до v3.539.44?
У PDFlibPas було три окремі дефекти, усі в проміжку між тим, що каже специфікація, і тим, що робили два незалежні шляхи коду. Перший полагодили у v3.539.42, два інші — у v3.539.44
Виробничі бокси усталювалися до MediaBox при захопленні
До v3.539.42 внутрішня підпрограма, що готує сторінку до захоплення (вона копіює успадковані записи на сторінку і доповнює відсутні бокси), давала BleedBox, TrimBox і ArtBox значення MediaBox, коли ті відсутні. CapturePageEx з опціями 2–4 читає свій обмежувальний прямокутник рівно з тих доповнених записів, тож на сторінці, що визначає лише CropBox, прохання trim-бокса захоплювало весь media box. GetPageBox уже застосовував усталення до CropBox, і довідник CapturePageEx завжди казав, що коли запитуваного бокса немає, використовується crop box; код захоплення розходився з обома. З v3.539.42 три виробничі бокси усталюються до CropBox сторінки, який на той момент уже на сторінці (власний, скопійований з предка чи доповнений з MediaBox), і лише сам CropBox падає назад до MediaBox
Два шляхи успадкування, одне семантичне правило
Другий дефект — саме нестандартне успадкування, і тонкість була в тому, що PDFlibPas розрішував бокси двома незалежними шляхами. Запити боксів (GetPageBox і HasPageBox) проходили ланцюг /Parent через один хелпер, а захоплення — через окремий локальний хелпер. Обидва успадковували кожен ключ, включно з виробничими боксами. Полагодження лише одного з них дало б суперечність усередині одного документа: з TrimBox завширшки 180 пунктів на вузлі /Pages і CropBox завширшки 380 пунктів на сторінці GetPageBox досі звітував би ширину обрізу 180, тоді як CapturePageEx будував би форму завширшки 380. У v3.539.44 обидва шляхи обмежують прохід /Parent чотирма успадковуваними ключами, виробничі бокси читаються лише з листка, а зайвий батьківський запис лишається у файлі недоторканим — ані видалений, ані переписаний
HasPageBox пропускав прямі батьківські масиви
HasPageBox повертає 0, коли на сторінці немає боксу запитуваного типу, 1 — коли сторінка має власний бокс (збережений прямо чи через непряме посилання), і 2 — коли MediaBox чи CropBox успадкований від предка. Стара відповідь повертала 2 лише тоді, коли успадковане значення було непрямим посиланням, тож успадкований прямий масив повертав 0. Виправлення відділяє розіменування від тесту на масив, і обидві тепер повертають 2. З v3.539.44 HasPageBox для BleedBox, TrimBox чи ArtBox може повернути лише 0 чи 1
Урок узагальнюється далеко за межі боксів сторінок. Коли один шматок семантики специфікації має в бібліотеці два вхідні пункти реалізації, полагоджуйте їх разом і тестуйте як матрицю, а не одним happy-path файлом. Регресійний набір PDFlibPas перетинає дві репрезентації батьківського боксу (прямий і непрямий масив) з трьома станами листка (відсутній, прямий масив, непрямий масив) і трьома опціями захоплення (bleed, trim, art), що дає 18 сценаріїв, і кожен перевіряє результат запиту, захоплені межі, легітимне успадкування MediaBox і CropBox та недоторканий батьківський запис
Як прочитати ефективний TrimBox у Delphi?
Викличте GetPageBox(4, Dimension) на вибраній сторінці. PDFlibPas застосовує усталений ланцюг за вас, тож результат — це ефективний TrimBox, чи є він на сторінці чи ні. Поєднуйте з HasPageBox, коли треба знати, звідки прийшло значення, чого зазвичай хоче звіт preflight
uses
System.SysUtils, PDFlibrary;
const
BOX_CROP = 2;
BOX_TRIM = 4;
DIM_LEFT = 0;
DIM_WIDTH = 2;
DIM_HEIGHT = 3;
DIM_BOTTOM = 5;
function DescribeTrim(Lib: TPDFlib; Page: Integer): string;
var
Source: string;
begin
Lib.SelectPage(Page);
if Lib.HasPageBox(BOX_TRIM) = 1 then
Source := 'own TrimBox'
else if Lib.HasPageBox(BOX_CROP) <> 0 then // 1 = власний, 2 = успадкований
Source := 'defaulted to the CropBox'
else
Source := 'defaulted to the MediaBox';
Result := Format('page %d: trim %.2f x %.2f pt at (%.2f, %.2f), %s',
[Page,
Lib.GetPageBox(BOX_TRIM, DIM_WIDTH),
Lib.GetPageBox(BOX_TRIM, DIM_HEIGHT),
Lib.GetPageBox(BOX_TRIM, DIM_LEFT),
Lib.GetPageBox(BOX_TRIM, DIM_BOTTOM),
Source]);
end;
var
Lib: TPDFlib;
Page: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('interior.pdf', '') = 1 then
for Page := 1 to Lib.PageCount do
Writeln(DescribeTrim(Lib, Page));
finally
Lib.Free;
end;
end.
І GetPageBox, і SetPageBox працюють у поточних налаштуваннях координат документа. Приклади тут ганяються з усталеними: початок 0 (лівий нижній кут, як у PDF user space) і пункти як одиниця вимірювання, тож вимір Top — це верхній край, виміряний угору від низу сторінки. Після SetOrigin(1) виміри Top і Bottom вимірюються вниз від верху сторінки, а після SetMeasurementUnits(1) кожне значення повертається в міліметрах. Ширина і висота не залежать від початку координат
Пошук виробничих боксів, скинутих на вузли /Pages
З v3.539.44 боксова API більше не бачить TrimBox на вузлі /Pages, що правильно, але інструмент preflight зазвичай хоче звітувати такий файл, а не тихо читати його способом специфікації. Вузли дерева сторінок — звичайні об'єкти, тож низькорівнева об'єктна API може їх знайти: пройдіть номери об'єктів до GetMaxObjectNumber, прочитайте кожен через GetObjectToString і шукайте словник /Pages, що несе ключ виробничого боксу. Друга половина перевірки — потестування по сторінках, якого хоче PDF/X, і HasPageBox тепер відповідає на нього так, як відповів би валідатор PDF/X, бо батьківський TrimBox більше не лічиться
procedure PreflightTrim(Lib: TPDFlib; Log: TStrings);
const
ProductionKeys: array[0..2] of string = ('/BleedBox', '/TrimBox', '/ArtBox');
var
ObjNum, K, Page, Missing: Integer;
Src: string;
begin
// 1. Виробничі бокси на вузлах дерева сторінок: нестандартні й ігноруються
for ObjNum := 1 to Lib.GetMaxObjectNumber do
begin
Src := ''; // вільні номери не повертають тексту
Src := string(Lib.GetObjectToString(ObjNum));
if Pos('/Type /Pages', Src) = 0 then
Continue;
for K := Low(ProductionKeys) to High(ProductionKeys) do
if Pos(ProductionKeys[K] + ' ', Src) > 0 then
Log.Add(Format('object %d: %s on a /Pages node is not inheritable',
[ObjNum, ProductionKeys[K]]));
end;
// 2. PDF/X: кожна сторінка потребує власного TrimBox чи ArtBox
Missing := 0;
for Page := 1 to Lib.PageCount do
begin
Lib.SelectPage(Page);
if (Lib.HasPageBox(4) = 0) and (Lib.HasPageBox(5) = 0) then
begin
Inc(Missing);
Log.Add(Format('page %d: no TrimBox or ArtBox', [Page]));
end;
end;
// 3. Необов'язковий ремонт: trim 6 x 9 дюймів усередині media box 6.25 x 9.25 дюйма
// (пункти, початок у лівому нижньому куті: Left, Top, Width, Height)
if Missing > 0 then
Log.Add(Format('TrimBox written on %d pages',
[Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;
Текстовий збіг — прагматична перевірка, а не парсер. Він спирається на те, що PDFlibPas серіалізує кожен запис словника як ключ, один пробіл і значення, що тримається для об'єктів, прочитаних назад через GetObjectToString. Крок ремонту заслуговує на рішення, а не на рефлекс: випадкове батьківське значення цілком може бути тим, що мав на увазі автор, але підтвердьте його проти замовлення на друк, перш ніж робити офіційним. SetPageBoxRange з порожнім діапазоном застосовує бокс до кожної сторінки і повертає кількість оновлених сторінок. Коли наявний бокс сторінки — непрямий масив, який може ділити інша сторінка чи вузол /Pages, SetPageBox дає тій сторінці новий прямий масив замість переписування спільного об'єкта. Встановлення BleedBox, TrimBox чи ArtBox ще й піднімає розблокований документ до PDF 1.3 — версії, що ввела ті записи
Спуск сторінок на TrimBox через CapturePageEx
CapturePageEx(Page, 3) перетворює сторінку на Form XObject, чия обмежувальна коробка — ефективний TrimBox сторінки, а DrawCapturedPage кладе ту форму на іншу сторінку в будь-якому розмірі. З v3.539.42 опція 3 на сторінці без TrimBox дає вам CropBox, як описує довідник, замість MediaBox з усім його обрізковим полем
Дві властивості захоплення формують код. Захоплення деструктивне: захоплена сторінка прибирається з документа, а документ ніколи не може впасти до нуля сторінок, тож додайте перший вихідний аркуш перед захопленням чого завгодно. Захоплення також працює лише в межах одного документа, тож стягніть спершу всі входи в один документ; техніки з зшивання та переплетіння джерел PDF за один прохід застосовуються прямо
procedure ImposeTwoUp(const InFile, OutFile: string);
var
Lib: TPDFlib;
Captures: array of Integer;
SourceCount, I: Integer;
TrimW, TrimH: Double;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(InFile, '') <> 1 then
raise Exception.Create('Cannot open ' + InFile);
SourceCount := Lib.PageCount;
// Ефективний trim-розмір сторінки 1 (це компонування припускає однаковий trim)
Lib.SelectPage(1);
TrimW := Lib.GetPageBox(4, 2);
TrimH := Lib.GetPageBox(4, 3);
// Додаємо й розміряємо перший аркуш; NewPage вибирає нову сторінку
Lib.NewPage;
Lib.SetPageDimensions(2 * TrimW, TrimH);
// Кожен capture прибирає сторінку 1, тож наступна вихідна сторінка зсувається вгору
SetLength(Captures, SourceCount);
for I := 0 to SourceCount - 1 do
begin
Captures[I] := Lib.CapturePageEx(1, 3); // 3 = TrimBox
if Captures[I] = 0 then
raise Exception.CreateFmt('Capture of source page %d failed', [I + 1]);
end;
// Лишився лише аркуш: дві обрізані сторінки на аркуш, поруч
Lib.SelectPage(1);
for I := 0 to SourceCount - 1 do
begin
if (I > 0) and (I mod 2 = 0) then
Lib.NewPage; // того самого розміру, що поточний аркуш
// Усталений початок: Top — верхній край, виміряний від низу
Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
end;
Lib.SaveToFile(OutFile);
finally
Lib.Free;
end;
end;
Захоплення за trim обрізає все поза TrimBox, що й треба для цифрового пробного відбитка чи макета cut-and-stack. Для друкарського аркуша, який обрізається після друку, захоплюйте з опцією 2, щоб вивороть вижив, і розставляйте клітинки на ширину вивороту. Оскільки захоплення прибирає вихідні сторінки, закладки та посилання, що вказували на них, втрачають цілі, тож спускайте в окремий вихідний файл, а не редагуйте документ, чия навігація вам ще потрібна; заміна сторінок без ламання закладок покриває той бік хірургії сторінок
Коли джерело має лишитися неушкодженим, ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) бере ті самі значення опцій 0–4 (передайте Lib.SelectedDocument для поточного документа), лишає дерево сторінок джерела незмінним, нормалізує успадковану ротацію сторінки в матрицю форми і повертає хендл, який приймає DrawCapturedPage. CapturePageEx не скасовує /Rotate, тож повернений вхід потребує того кроку спершу, а розплющування ротації сторінки без ламання боксів сторінок показує, що стається з кожним боксом, коли ви це робите. Одна обережність для входів, що можуть нести виробничі бокси на вузлах /Pages: шлях імпорту розрішує свій бокс через власний пошук предка, окремий від двох шляхів, вирівняних у v3.539.44, тож перевірте спершу HasPageBox(4) на вихідній сторінці і передавайте опцію 1 (CropBox), коли вона повертає 0. Це тримає результат прив'язаним до специфікації, а не до того, як файл випадково був написаний
Шпаргалка з боксів сторінок
- Ефективний CropBox: власний CropBox сторінки, інакше найближчий успадкований CropBox, інакше ефективний MediaBox (ISO 32000-1 §14.11.2)
- Ефективні BleedBox, TrimBox і ArtBox: власний запис листкової сторінки, інакше ефективний CropBox
- Лише
Resources,MediaBox,CropBoxіRotateуспадковуються з вузлів/Pages(§7.7.3.4, Table 30); виробничі бокси на вузлах/Pagesігноруються GetPageBox(BoxType, Dimension): BoxType 1 MediaBox, 2 CropBox, 3 BleedBox, 4 TrimBox, 5 ArtBox; Dimension 0 Left, 1 Top, 2 Width, 3 Height, 4 Right, 5 BottomHasPageBox(BoxType): 0 — боксу немає, 1 — власний бокс сторінки (прямий чи непрямий), 2 — успадкований MediaBox чи CropBox (прямий чи непрямий)CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox з фолбеком на MediaBox, 2–4 BleedBox, TrimBox чи ArtBox з фолбеком на CropBox- Оновіться до v3.539.44 чи новішої заради послідовних усталень і успадкування між боксами та захопленням
Бокси сторінок — це місце, де тихі усталення PDF зустрічаються з препрес-допусками, виміряними у частках міліметра, і бібліотека або застосовує ті усталення всюди однаково, або вручає вам дві відповіді на одне питання. Повна боксова, захоплювальна і Form XObject API задокументована на сторінці продукту PDFlibPas PDF Library for Delphi