Техническая статья

Сшивание страниц и шаблоны водяных знаков в PDF в Delphi

Сшивание страниц компонует содержимое существующих страниц на общий холст, что отличается от операции объединения файлов. losLab PDF Library реализует сшивание страниц на основе Form XObject и компонуемые шаблоны водяных знаков для Delphi: методы StitchPageOverlay, StitchPagesSideBySide и StitchPagesVertically размещают захваченные страницы в любом масштабе с полным сохранением векторного качества, а класс TPDFlibCompositeWatermark накладывает текстовые и графические водяные знаки на диапазон страниц за один вызов. Если вам на самом деле нужно добавить целые страницы из нескольких документов в один файл, это семейство функций объединения (merge), описанное в статье о быстром объединении PDF на байтовом уровне; данная статья посвящена объединению содержимого страниц в едином координатном пространстве

Задачи, которые приводят к этому решению, обыденны, но настойчивы. Школе требуется разместить две страницы экзаменационного листа на одном листе, чтобы вдвое сократить расход бумаги. Специалисту по локализации нужно расположить английскую страницу и ее перевод бок о бок на одной странице для синхронной прокрутки. Юридическому отделу необходимо, чтобы каждый черновик был помечен сразу тремя слоями: логотип в углу, диагональная надпись CONFIDENTIAL посередине и небольшая строка футера, и все это только на страницах с 1 по 3. Все три сценария представляют собой одну и ту же фундаментальную проблему: размещение уже сверстанного содержимого страницы в другом месте и другого размера без ухудшения качества до растрового изображения

Почему объединение потоков содержимого PDF приводит к ошибкам?

Простое сшивание путем копирования потока /Contents исходной страницы на целевую страницу терпит неудачу по трем структурным причинам. Во-первых, сталкиваются координаты: оба потока содержимого предполагают свое начало координат в левом нижнем углу своего MediaBox, поэтому вставленное содержимое ложится поверх существующего в неверном месте. Во-вторых, конфликтуют имена ресурсов: каждая страница разрешает такие операторы, как /F1 Tf и /Im0 Do, относительно своего собственного словаря /Resources, и две страницы обычно связывают одно и то же имя с разными шрифтами или изображениями, поэтому одна из них без каких-либо предупреждений отображается с неверным ресурсом. В-третьих, копирование необработанного потока не дает никакого масштабирования; максимум, что вы можете сделать — это сместить координаты

Form XObject решают все три проблемы одновременно, и именно поэтому на них построено сшивание в losLab PDF Library. Стандарт ISO 32000-1 §8.10 определяет Form XObject как автономный поток содержимого со своим собственным словарем /Resources и собственным координатным пространством, помещаемый на страницу путем вызова его имени с использованием текущей активной матрицы преобразования. Метод CapturePage упаковывает всю существующую страницу (содержимое плюс ресурсы) в объект XObject с типом /Subtype /Form, а функции сшивания позиционируют его с помощью матрицы cm: масштабируют, переносят, поворачивают по вашему желанию. Поскольку источник остается векторным, текст остается текстом, а контуры — контурами при любом уровне масштабирования. Это то же самое свойство, которое делает векторную графику и градиенты независимыми от разрешения. Ничего не растрируется, а изолированный словарь ресурсов означает, что захваченная страница не будет конфликтовать с целевой страницей из-за имени /F1

Как объединить две страницы PDF на одной странице?

Метод StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) является прямым решением: он захватывает обе исходные страницы, создает новую, более широкую страницу и размещает два захваченных объекта слева и справа с разделительной полосой (gutter) между ними, масштабируя каждый под размер TargetHeight. Функция возвращает номер новой страницы (начиная с 1) или 0 в случае сбоя, при этом обе исходные страницы захватываются и скрываются в процессе операции. Сценарий с размещением двух экзаменационных листов на одном листе решается буквально в три строки

var
  Lib: TPDFlib;
  NewPage: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('exam.pdf', '');
    // Pages 1 and 2 on one new wider page with an 18pt gutter,
    // both scaled to a 595pt output height (A4 landscape)
    NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
    if NewPage > 0 then
      Lib.SaveToFile('exam-2up.pdf');
  finally
    Lib.Free;
  end;
end;

Для вертикальной компоновки метод StitchPagesVertically(PageRanges, Gap, TargetWidth) объединяет весь диапазон страниц в одну высокую страницу в стиле постера. Параметр PageRanges принимает стандартный синтаксис диапазонов, например '1-3,5'. Каждая исходная страница масштабируется так, чтобы ее ширина соответствовала TargetWidth, поэтому высота каждой страницы на выходе равна ее собственной высоте, умноженной на коэффициент масштабирования, а общая высота выходной страницы представляет собой их сумму плюс по одному промежутку Gap между каждой парой. Это отличный формат для документов непрерывной прокрутки при рецензировании или длинных чеков, собранных из источников формата Letter

Как наложить одну страницу на другую?

Метод StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) накладывает одну существующую страницу на другую существующую страницу вместо создания новой. Исходная страница захватывается как Form XObject и рисуется на целевой странице в указанном прямоугольнике с прозрачностью Opacity от 0 до 1 (1 — полностью непрозрачный). Ширина и высота являются свободными параметрами, поэтому изображение растянется, если ваше соотношение сторон отличается от исходного соотношения сторон страницы; передайте пропорциональные значения, если хотите получить неискаженную миниатюру источника. Типичный пример использования — документ, блок утверждения которого ведется на отдельной странице и накладывается в виде штампа на обложку

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('report.pdf', '');
    // Page 5 holds the approval block; place it on page 1
    // in a 260 x 180 box at 45% opacity. Page 5 is consumed:
    // it is captured and hidden, so pages 6..n become 5..n-1
    if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
      Lib.SaveToFile('report-approved.pdf');
  finally
    Lib.Free;
  end;
end;

Что происходит с номерами страниц после захвата?

Метод CapturePage поглощает исходную страницу: ее содержимое переносится в XObject, а сама ставшая пустой страница скрывается, поэтому номера всех последующих страниц сдвигаются на единицу вниз. Это важнейший семантический нюанс во всем API сшивания. losLab PDF Library сама рассчитывает арифметику внутри каждого вызова сшивания: StitchPageOverlay корректирует целевой индекс, когда цель находится дальше источника, а StitchPagesSideBySide сначала захватывает страницу с большим номером, чтобы индекс второго захвата оставался верным. Тем не менее, ваш собственный вызывающий код должен учитывать этот сдвиг между вызовами. Чтобы код множественного сшивания оставался корректным, соблюдайте два правила. Записывайте любые размеры или номера страниц, которые вам понадобятся, до первого захвата, так как после него они изменятся. И когда вы захватываете несколько страниц в цикле самостоятельно, обходите их в порядке убывания номеров страниц, чтобы скрытие последующей страницы не нарушало индекс предыдущей, которую вы еще не захватили

Как применить многослойный водяной знак к диапазону страниц?

Уровень шаблонов водяных знаков в PDFlibWatermark.pas превращает подход "определи один раз, примени ко многим страницам" в объекты. Класс TPDFlibWatermarkBase содержит общие свойства для всех водяных знаков: девятиточечное положение Position (от wpTopLeft до wpBottomRight, плюс wpCustom для точных X/Y координат), угол Angle в градусах, непрозрачность Opacity от 0 до 1 и строку диапазона страниц PageRange, где пустое значение означает все страницы. TPDFlibTextWatermark добавляет текст, идентификатор шрифта (по умолчанию Helvetica), размер и цвет RGB; TPDFlibImageWatermark добавляет ID изображения из ранее добавленного файла, а также ширину и высоту размещения. TPDFlibCompositeWatermark хранит список дочерних водяных знаков и применяет их рекурсивно, а метод TPDFlib.ApplyWatermark служит единой точкой входа, учитывающей собственный диапазон страниц шаблона и возвращающей количество измененных страниц. Трехслойный юридический штамп выглядит следующим образом

var
  Lib: TPDFlib;
  Logo: TPDFlibImageWatermark;
  Diagonal, Footer: TPDFlibTextWatermark;
  Stack: TPDFlibCompositeWatermark;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('draft.pdf', '');
    Stack := TPDFlibCompositeWatermark.Create;
    try
      Logo := TPDFlibImageWatermark.Create;
      Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
      Logo.Position := wpTopRight;
      Logo.Width := 90;
      Logo.Height := 32;
      Stack.Add(Logo);

      Diagonal := TPDFlibTextWatermark.Create;
      Diagonal.Text := 'CONFIDENTIAL';
      Diagonal.Position := wpCenter;
      Diagonal.Angle := 45;
      Diagonal.FontSize := 60;
      Diagonal.Opacity := 0.15;
      Stack.Add(Diagonal);

      Footer := TPDFlibTextWatermark.Create;
      Footer.Text := 'Internal review copy';
      Footer.Position := wpBottomCenter;
      Footer.FontSize := 9;
      Footer.Opacity := 0.6;
      Stack.Add(Footer);

      Stack.PageRange := '1-3';   // children inherit this range
      Lib.ApplyWatermark(Stack);
      Lib.SaveToFile('draft-marked.pdf');
    finally
      Stack.Free;                 // composite frees its children
    end;
  finally
    Lib.Free;
  end;
end;

Две детали компоновки заслуживают отдельного упоминания. Наследование диапазонов является однонаправленным и неразрушающим: дочерний элемент с пустым PageRange временно наследует диапазон композита во время Apply, в то время как дочерний элемент, задающий собственный диапазон, сохраняет его. Благодаря этому вы можете закрепить футер на каждой странице, в то время как диагональный штамп будет покрывать только раздел черновика. И владение следует за композитом: деструктор TPDFlibCompositeWatermark.Destroy освобождает свои дочерние элементы, поэтому вы освобождаете композит один раз, и больше ничего делать не нужно, как показано в примере выше

Ограничения, о которых стоит знать

Существует два объективных ограничения, и оба они следуют из принципа работы механизма. Во-первых, захват упаковывает только поток содержимого и ресурсы страницы. Аннотации, ссылки, поля форм и комментарии живут как отдельные объекты в словаре страницы, а не в потоке содержимого, поэтому они не переносятся в XObject; сшитый макет содержит видимое содержимое страницы, но не интерактивный слой ее источников. Во-вторых, девятиточечное позиционирование текста является приблизительным: без полноценного движка верстки библиотека не может знать точную ширину отображаемой строки, поэтому при разрешении таких позиций, как wpCenter, она оценивает текстовое поле в 40% от ширины страницы. Этого достаточно для центрирования короткой метки, но для попиксельно точного размещения длинных строк переключитесь на wpCustom и вычисляйте координаты самостоятельно

Масштаб здесь не является практическим ограничением, так как захват и сшивание манипулируют ссылками на объекты, а не кодируют заново содержимое страниц; если ваши исходники весят сотни мегабайт, перед сшиванием применяется тот же порядок действий, который описан в руководстве по объединению и разделению больших файлов с прямым доступом. Функции сшивания и классы шаблонов водяных знаков, показанные здесь, поставляются в текущей версии losLab PDF Library для Delphi, C# и VB.NET, наряду с низкоуровневыми примитивами CapturePage, DrawCapturedPage и DrawCapturedPageMatrix на случай, если вам понадобится макет, не покрываемый тремя готовыми функциями сшивания