Когда у страницы PDF нет TrimBox, её эффективный TrimBox — это CropBox страницы, а когда нет и CropBox — MediaBox. BleedBox и ArtBox следуют тому же правилу. PDFlibPas, PDF Library for Delphi, применяет эту цепочку дефолтов единообразно в GetPageBox, HasPageBox и CapturePageEx с v3.539.44 и игнорирует производственные боксы, положенные на узел /Pages, потому что ISO 32000-1 не разрешает им наследоваться
Пока не займёшься спуском полос, это звучит как сноска. Представьте книжный блок с MediaBox 6,25 × 9,25 дюйма, CropBox под обрез 6 × 9 дюймов и без TrimBox — потому что экспортёру и в голову не пришло его записать. Спросите обрезной бокс — получите медиа-бокс, и каждая ячейка на печатном листе тащит восьмую дюйма вылетов и служебных полей в соседнюю. У PDFlibPas были дефекты ровно в этой области, исправленные в v3.539.42 и v3.539.44, и то, как их чинили, кое-что говорит о том, как семантику боксов страниц стоит реализовывать в любой PDF-библиотеке
Какой бокс действует, когда у страницы нет TrimBox?
Ответ — фиксированная цепочка дефолтов из ISO 32000-1 §14.11.2: CropBox по умолчанию равен MediaBox, а BleedBox, TrimBox и ArtBox каждый по умолчанию равен CropBox. Прямо к MediaBox по умолчанию откатывается только CropBox. Страница, определившая один 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 запрос обрезного бокса захватывал весь медиа-бокс. 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, когда нужно знать, откуда взялось значение, — префлайт-отчёт обычно хочет именно этого
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 (левый нижний угол, как в user space PDF) и пункты в качестве единиц, так что размерность 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. Опциональная починка: обрез 6 x 9 дюймов внутри медиа-бокса 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;
// Эффективный размер обреза страницы 1 (раскладка предполагает единый обрез)
Lib.SelectPage(1);
TrimW := Lib.GetPageBox(4, 2);
TrimH := Lib.GetPageBox(4, 3);
// Дописываем и размеряем первый лист; NewPage выбирает новую страницу
Lib.NewPage;
Lib.SetPageDimensions(2 * TrimW, TrimH);
// Каждый захват удаляет страницу 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;
Захват по обрезу отсекает всё вне 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
- От узлов
/Pagesнаследуются толькоResources,MediaBox,CropBoxиRotate(§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 встречаются с допечатными допусками, меряемыми долями миллиметра, и библиотека либо применяет эти дефолты одинаково всюду, либо выдаёт два ответа на один вопрос. Полный API боксов, захвата и Form XObject задокументирован на странице продукта PDFlibPas PDF Library for Delphi