Валентин Синицын
Работаем с PDF из Perl
Бытует мнение, что PDF – это закрытый формат, для работы с которым необходимо дорогостоящее ПО. Это не вполне верно: спецификация PDF доступна всем желающим, а для создания и правки файлов можно применять не только настольные издательские системы, но и сценарии Perl.
Модуль PDF::API2 (http://pdfapi2.sf.net) разрабатывается Альфредом Райбенщухом (Alfred Reibenschuh) и распространяется по лицензии GNU GPL. PDF::API2 написан на чистом Perl и имеет минимум зависимостей: Perl 5.8.4 или выше, Encode и Compress::Zlib (если что-то и придется установить, то только последний из них). Модуль использует стандартную объектную нотацию Perl 5 и доступен как через CPAN, так и через репозитарии ActiveState PPM, поэтому может быть установлен любым удобным для вас способом. Последней на момент написания статьи версией была 0.51.
Поставляемая вместе с PDF::API2 документация насчитывает более 160 страниц, большая часть из которых не имеет практической ценности. Для повседневной работы вам потребуются лишь страницы PDF::API2, ::Content, ::Page и ::Util, где перечислены значения различных констант.
PDF::API2 не является средством для редактирования PDF в прямом смысле этого слова – он не предоставляет специальных методов для «поиска и замены» существующих элементов (например, картинок). Однако с его помощью вы легко можете создавать собственные PDF-файлы, а также обрабатывать и дополнять уже существующие. Как мы вскоре увидим, этого достаточно для решения весьма широкого круга задач. Другое ограничение – невозможность работать с зашифрованными PDF-файлами. Видимых препятствий этому нет – программа дешифровки PDF на Perl (http://www.cs.cmu.edu/~dst/Adobe/Gallery/pdfdecrypt.pl) занимает менее двухсот строк кода. «Альтернативный» модуль CAM::PDF (http://search.cpan.org/~clotho/CAM-PDF-1.05), который, по моему мнению, проигрывает PDF::API2 в удобстве использования, также справляется с этой задачей безо всякого труда. Возможно, этот функционал будет реализован в следующих версиях модуля.
Прежде чем мы приступим к обсуждению констант и функций, давайте поближе познакомимся с форматом PDF версии 1.4, с которой и работает PDF::API2.
Экранная модель PDF
Основой для эффективной работы с PDF является понимание его экранной модели (imaging model). Аналогичные концепции лежат в основе многих современных API для работы с векторной графикой, например, Cairo и Arthur, так что кое-какие представления о них в любом случае окажутся нелишними.
Спецификация PDF 1.4 предусматривает четыре типа объектов, которые могут отображаться на странице: контуры (path object), текст (text object) и растр (image object). Особняком стоит область отсечения (current clipping path) – специальный контур, в пределах которого возможна отрисовка объектов. Любые фигуры или их части, выходящие за пределы данной области, отсекаются, откуда и происходит это название.
Для описания объектов всех четырех типов и работы с ними в PDF служит специальный язык, по своим функциям аналогичный PostScript. Например, для отрисовки контура используется оператор S (stroke), а для его заливки – f (fill) (здесь нет никакой опечатки – операторы «языка PDF» в большинстве своем одно- и двухбуквенные). Как нетрудно видеть, у этих операторов нет параметров. Точки, образующие контур, задаются заранее, а такие атрибуты, как толщина соединяющей их линии, ее цвет и цвет заливки определяются текущим графическим состоянием (graphics state). Одним из наиболее важных элементов этого состояния является матрица преобразования системы координат (current transformation matrix, CTM), определяющая текущее положение декартовых осей. Думается, читатели, имеющие опыт программирования трехмерной графики, испытали чувство дежа-вю, и не зря: аналогия с OpenGL/Direct3D налицо, разве что все преобразования координат в PDF будут двумерными. По умолчанию, система координат имеет начало в левом нижнем углу листа, ось X направлена вправо, ось Y – вверх. За единицу измерения принимается 1/72 дюйма, что примерно соответствует типографскому пункту (pt). Вы можете перенести начало отсчета в другую точку (translate), повернуть оси на некоторый угол (rotate), изменить масштаб (scale) или скосить их (skew). Напомним, что порядок применения этих операций имеет значение: перенос и поворот – это совсем не то же самое, что поворот и перенос. Изменения системы координат непосредственно сказываются на выводимых объектах: так, надпись, расположенная вдоль оси X в повернутой системе координат, будет идти под некоторым углом к краю страницы. Если система координат была перенесена на N пунктов влево и M пунктов вверх, окружность, центр которой якобы находится в начале (0,0), будет изображена в окрестности точки с координатами (M,N) и так далее.
Геометрическая плоскость, на которой введена наша система координат, бесконечна, однако в реальном мире все имеет свой предел. Стандарт PDF предусматривает несколько различных типов границ, наиболее важными из которых являются граница физической страницы (MediaBox) и граница видимой области (CropBox). Каждая из них задается четверкой чисел, представляющих собой координаты левого нижнего и правого верхнего углов прямоугольной области в системе координат по умолчанию. Если CropBox не задан явно, он принимается равным MediaBox.
Теперь, когда мы бегло познакомились с экранной моделью PDF, можно переходить к изучению функций модуля PDF::API2, реализующих работу в ней на языке Perl
Здравствуй, мир!
По сложившейся традиции, мы начнем свое рассмотрение с программы «Hello, World». Данный пример был взят из стандартного дистрибутива PDF::API2, но слегка модифицирован, чтобы лучше отражать отечественные реалии. Конечно, при практическом наборе кода номера строк (01:, 02:, ...) необходимо опустить; мы также не используем строгий режим (use strict), чтобы слегка уменьшить объем сценариев и повысить их удобочитаемость.
Пример 1
01:#!/usr/bin/perl
02:use PDF::API2;
03:use constant mm => 25.4/72;
04:use constant cm => 2.54/72;
05:use constant in => 1/72;
06:use constant pt => 1;
07:$pdf = PDF::API2->new;
08:$fnt = $pdf->corefont('Verdana', -encode => 'cp1251');
09:$page = $pdf->page;
10:$page->mediabox('A4');
11:$gfx = $page->gfx;
12:# Выводим текстовую метку
13:$gfx->textlabel(210/mm/2, 297/mm/2, $fnt, 12, 'Здравствуй, мир !');
14:$pdf->saveas('helloworld1.pdf');
15:$pdf->end;
Строки 1-2, будем надеяться, в особых комментариях не нуждаются. В строках 3-6 задаются стандартные константы, выражающие миллиметр (mm), сантиметр (cm), дюйм (in) и пункт (pt) в принятых в PDF единицах измерения. Рекомендую включать их во все ваши скрипты. Наконец, в строке 7 создается объект PDF::API2, с которым мы и будем работать. Каждому такому объекту может соответствовать не более одного PDF-документа. В строке 8 мы выбираем шрифт, который будет использоваться для вывода текста. Параметр -encode может принимать любое значение, известное модулю Encode вашей инсталляции Perl и, конечно, совпадающее с кодировкой символов в вашем сценарии. Метод corefont указывает, что нам нужен один из «базовых» шрифтов, лицензированных компаниями Adobe и Microsoft для свободного распространения, а посему потенциально доступными на любой системе. Помимо Verdana, сюда входят Georgia, Webdings, Wingdings (набор «Windows Fonts»), Courier, Helvetica, Symbol, Times, ZapfDingbats (набор «Adobe Core Fonts») и их разновидности: жирный, курсив и так далее.
К сожалению, текущая версия PDF::API2 умеет корректно отображать русский текст лишь шрифтами Verdana и Georgia – для остальных гарнитур отсутствует информация о ширине символов, в результате чего буквы слипаются (см. врезку «Изъясняемся по-русски»).
Кроме core-шрифтов, PDF::API2 может использовать произвольные шрифты TrueType, PostScript и BDF, предоставленные пользователем в виде файлов. В этом случае шрифт внедряется в PDF-документ.
В строке 9 мы добавляем в конец нашего, пока еще пустого документа чистую страницу, а в строке 10 устанавливаем ее размер, равный странице формата A4 (210x297 мм) – по умолчанию используется стандартный для США размер Letter. Помимо псевдонима «A4» в методе mediabox можно использовать пару (ширина, высота) или ту самую четверку чисел, о которой говорилось в предыдущем разделе. Подготовительные операции завершаются созданием графического объекта $gfx (можете рассматривать его как холст – canvas) в строке 11.
В строке 13 мы вызываем метод textlabel – это самый простой (но не самый гибкий) способ создания текстового объекта, который будет расположен в точке с координатами 210/mm/2, 297/mm/2 (т.е. в центре листа. Обратите внимание на использование константы mm для перевода миллиметров в пункты) и отрисован шрифтом $font (Verdana) высотой 12 пунктов. В строках 14 и 15 происходит запись PDF-документа на диск и разрушение объекта.
На первый взгляд подготовительный этап может показаться чересчур трудоемким, но давайте не будем спешить и займемся усовершенствованием этого, в общем-то, очень простого примера. Заменим строки 12-13 на:
Пример 2:
01:$n = 10;
02:$R = 50;
# $n текстовых меток по кругу
03:$gfx->translate(210/mm/2, 297/mm/2);
04:for $i (1..$n) {
05: $gfx->rotate(360/$n);
06: $gfx->textlabel($R, 0, $fnt, 12, 'Здравствуй, мир !');
07:}
Здесь мы перемещаем начало отсчета в центр листа (строка 3), а затем выводим в цикле 10 текстовых меток, каждый раз поворачивая систему координат на 36 градусов относительно предыдущего положения (строка 5). Что получится в результате? Правильно – десять радиально расходящихся надписей «Здравствуй, мир!». Отступ на $R пунктов по оси X (в повернутой системе координат!) нужен для того, чтобы начальные буквы фразы не накладывались друг на друга (см. рис. 1).
Рисунок 1. Результат работы сценария helloworld1.pl
Полученная нами картинка уже неплоха, но здесь явно напрашивается что-то большее. Давайте добавим в наш документ геометрические фигуры (в терминологии PDF – контуры) и раскрасим их в различные цвета!
Вставьте данный кусочек кода после строки 2 в предыдущем примере:
Пример 3
# Синий фон
01:$gfx->fillcolor('blue');
02:$gfx->rectxy($page->get_mediabox);
03:$gfx->fill(1);
# Желтый круг в центре страницы
04:$gfx->fillcolor(‘yellow’);
05:$gfx->circle(210/mm/2, 297/mm/2, 0.95*$R);
06:$gfx->fill(1);
В строке 1 задается одно из свойств текущего графического состояния – цвет заливки (fillcolor). Строка 2 определяет прямоугольник, в точности совпадающий с нашей страницей (поскольку координаты его вершин возвращаются методом get_mediabox). Наконец, в строке 3 дается команда закрасить ранее определенный контур (прямоугольник) цветом, установленным последним вызовом fillcolor (синим). До вызова метода fill наш прямоугольник не отображается на странице. Более того, заменив вызов fill на stroke, мы получим незакрашенный прямоугольник, построенный по тем же самым точкам. Это является следствием того, что операторы S и f не имеют собственных входных параметров, а используют заданные ранее значения. Аналогичным образом в строках 4-6 строится желтый круг радиуса 0.95*$R.
Сохраните сценарий и запустите его на выполнение. Если вы не допустили ошибок при наборе исходного текста, ваши старания будут вознаграждены! На синем небе ярко засияет солнышко, каждый лучик которого шлет привет этому, а может быть, и другим мирам (см. рис. 2).
Рисунок 2. Результат работы сценария helloworld2.pl
Конечно, возможности PDF::API2 никоим образом не ограничиваются рассмотренными здесь вызовами. Всю необходимую информацию вы можете найти в документации. От себя добавлю, что в сценариях для «серьезной» работы с текстом возможностей, предоставляемых методом textlabel, может оказаться недостаточно. Поэтому если вы намерены составить конкуренцию TeX или Adobe InDesign, советуем ознакомиться со статьей [1], в которой подробно рассмотрен процесс верстки и добавления иллюстраций.
Квитанции, брошюры и все-все-все
Простым созданием документов в формате PDF (пусть даже таких красивых, как наше солнышко), сейчас уже никого не удивишь. Однако модификация существующих файлов по-прежнему остается уделом избранных. Что ж, попробуем приоткрыть завесу тайны и с удивлением обнаружим, что все необходимое у нас уже есть.
Начнем с такой распространенной задачи, как заполнение бланков: счетов в интернет-магазинах, квитанций, анкет и т. п. Для этих целей вполне сгодится наш первый пример. Единственное, что нужно сделать, – заменить создание нового документа открытием уже существующего:
Пример 4
01:#!/usr/bin/perl
02:use PDF::API2;
03:use constant mm => 25.4/72;
04:use constant cm => 2.54/72;
05:use constant in => 1/72;
06:use constant pt => 1;
07:$pdf = PDF::API2->open("blank.pdf");
08:$font = $pdf->corefont("Georgia-Italic", -encode => cp1251);
09:$page = $pdf->openpage(1);
10:$gfx = $page->gfx;
11:$gfx->textlabel(3.20/cm, 297/mm-3.30/cm, $font, 12, "Иванов И.И.");
12:$gfx->textlabel(6.0/cm, 297/mm-3.80/cm, $font, 12, "Голодающим детям Африки");
13:$gfx->textlabel(3.05/cm, 297/mm-4.80/cm, $font, 12, "01.01.1970");
14:$pdf->saveas("receipt.pdf");
15:$pdf->end;
Нетрудно видеть, что вместо метода PDF::API2->new здесь используется open, принимающий в качестве параметра имя существующего PDF-файла. Вызов $pdf->page, создающий новую пустую страницу, уступил место методу $pdf->openpage, открывающему уже существующую. Кстати, номер страницы (в нашем случае 1) может быть и отрицательным – это означает, что вы ведете отсчет не с начала документа, а с конца. Файл blank.pdf мы подготовили в обычном текстовом процессоре с возможностью экспорта PDF, а расположение полей для заполнения определили, используя его «линейки» (297мм – высота листа формата A4, отступы отсчитываются от левой и верхней границ страницы) (см. рис. 3).
Рисунок 3. Заполненная квитанция
Другой весьма широкий класс задач – так называемый пост-процессинг PDF-документов: поворот страниц, обрезание полей (crop), слияние нескольких документов в один и извлечение страниц, а также извечная проблема любителей электронных книг – брошюровка (называемая в народе «верстка книжкой»). Рассмотрим каждую из этих задач по очереди. Во всех последующих сценариях, где это необходимо, подразумевается «преамбула» в объеме строк 1-6 примера 4.
Поворот страниц
01:$pdf = PDF::API2->open("document.pdf");
02:for $i (1..$pdf->pages) {
03: $pdf->openpage($i)->rotate(90);
04:}
05:$pdf->saveas("document_new.pdf");
06:$pdf->end;
Мы пробегаем в цикле все страницы данного документа (их число возвращает метод pages) и для каждой из них вызываем метод rotate. Отметим, что он отличается от рассмотренного нами ранее метода rotate объекта $page->gfx. Во-первых, здесь допустимы повороты только на углы, кратные 90 градусам. Во-вторых, вызов метода $page->rotate не меняет текущую матрицу преобразования (CTM), а устанавливает особый параметр страницы (можете думать о нем, как о флажке «книжной/альбомной» ориентации) и, таким образом, влияет на вид системы координат по умолчанию. Так, в нашем случае ее начало находится в левом верхнем углу страницы, оси направлены вправо и вниз. Попробуйте нарисовать на каждой странице квадрат с вершинами (0,0) – (100,100), и вы поймете, что имеется в виду.
Обрезание полей
01:$pdf = PDF::API2->open("document.pdf");
02:for $i (1..$pdf->pages) {
03: $pdf->openpage($i)->cropbox(3/cm, 3/cm, 210/mm-3/cm, 297/mm-3/cm);
04:}
05:$pdf->saveas("document_new.pdf");
06:$pdf->end;
Нетрудно видеть, что решение этой задачи полностью аналогично предыдущему. Всю работу выполняет метод cropbox, «отхватывающий» по 3 сантиметра от каждого края листа формата A4 (в настоящей программе было бы разумно получать текущие размеры каждой страницы при помощи метода get_mediabox). Если сейчас вы подумали об области отсечения (clipping path), то, к сожалению, ошиблись: она здесь ни при чем. Подобно методу rotate, cropbox всего лишь устанавливает определенный параметр в свойствах страницы.
Слияние документов и извлечение страниц
Чтобы решить эту задачу, нам придется поднапрячься и совершить качественный скачок – начать работать с двумя объектами PDF::API2 одновременно. В общем случае нам необходимо создать документ-источник и документ-приемник, а затем перенести страницы с помощью метода importpage. Если документ-приемник ранее не существовал (то есть был создан в процессе работы сценария), мы имеем дело с извлечением страниц, в противном случае налицо (частичное) слияние двух документов.
01:#!/usr/bin/perl
02:use PDF::API2;
03:$source = PDF::API2->open("document.pdf");
04:$dest = PDF::API2->new;
#или PDF::API2->open("document2.pdf");
05:@pages = (1, -1);
06:for $page (@pages) {
07: $dest->importpage($source, $page, $dest->page);
08:}
09:$dest->saveas("document_new.pdf");
10:$dest->end;
11:$source->end;
В строке 5 мы перечисляем страницы, которые будут импортированы из $source в $dest. В данном случае нас интересуют только первая и последняя. Всю необходимую работу выполняет метод importpage (строка 7), на вход которого подаются объект-источник ($source), номер страницы в исходном документе ($page) и номер страницы в документе-приемнике или объект типа PDF::API2::Page, «в который» будет помещена импортированная страница. Мы используем результат вызова $dest->page, который, напомним, добавляет новую страницу в конец документа $dest. Следует отметить, что созданные таким образом страницы имеют кое-какие ограничения – например, их нельзя импортировать в другой документ (тому есть свои причины) до тех пор, пока они не будут сохранены в реальном файле, а затем заново открыты методом openpage. Если это для вас существенно, создайте пустой одностраничный PDF-документ любым доступным способом (например, в OpenOffice.org) и каждый раз импортируйте его единственную чистую страницу вместо вызова метода page.
Брошюровка
Теперь мы можем перейти к наиболее сложной из заявленных нами задач – брошюровке. Этот процесс состоит из двух этапов. На первом из них создается набор пар – номеров страниц, подлежащих распечатке на одном листе (с двух сторон).
Идея проста: исходный документ дополняется пустыми (или рекламными) страницами так, чтобы их общее число N было кратно 4, затем страницы группируют следующим образом: (N,1), (2, N-1), (N-2, 3) до тех пор, пока первое число в паре меньше второго. Затем каждая пара соответствующим образом уменьшается и копируется на печатный лист.
01:$source = PDF::API2->open("document.pdf");
02:$dest = PDF::API2->new;
03:$left = 1;
04:$right = ($source->pages % 4 == 0) ? $source->pages : $source->pages + (4 - $source->pages % 4);
05:$reversed = 1;
06:while ($left < $right) {
07: $page = $dest->page;
08: $page->mediabox(297/mm, 210/mm);
09: $page->rotate(90);
10: draw_page($source, $left, $dest, -1, $reversed);
11: draw_page($source, $right, $dest, -1, !$reversed) if ($right <= $source->pages);
12: $left++; $right--; $reversed = !$reversed;
13:}
14: $dest->saveas("document_book.pdf");
15:$dest->end;
16:$source->end;
Этот кусочек кода реализует первую половину нашего плана. В переменной $left хранится номер первой, а в $right – второй страницы текущей пары. Строка 4 вкупе с условием if в строке 11 эквивалентна добавлению нужного числа пустых страниц в конец документа $source. Флаг $reversed определяет, где на печатном листе будет расположена страница с меньшим номером – слева или справа. Интерес также представляют строки 7-9: здесь мы добавляем в $dest новую страницу формата «перевернутый A4» и тут же поворачиваем ее на 90 градусов. Таким образом мы получаем обычную страницу формата A4 с необычной системой координат, так что все помещенные на нее объекты будут «лежать на боку». Фактической отрисовкой страницы занимается подпрограмма draw_page, которая принимает пять параметров: объект-источник ($pdf_in) и номер исходной страницы ($in_idx), объект-приемник ($pdf_out) и номер страницы-«печатного листа» ($out_idx), а также позицию на печатном листе (0 – слева, 1 – справа):
01:sub draw_page {
02: my ($pdf_in, $in_idx, $pdf_out, $out_idx, $position) = @_;
03: my $xo = $pdf_out->importPageIntoForm($pdf_in, $in_idx);
04: my $pg_out = $pdf_out->openpage($out_idx);
05: if (my $cropbox = $pdf_in->openpage($in_idx)->find_prop("CropBox")) {
06: $xo->bbox(map {$_->val} $cropbox->elementsof);
07: }
08: my @ps = map {$_->val} $xo->{BBox}->elementsof;
09: my $bbox_width = $ps[2] - $ps[0];
10: my $bbox_height = $ps[3] - $ps[1];
11: my (undef, undef, $page_width, $page_height) = $pg_out->get_mediabox;
12: my $scale_x = $page_width/(2*$bbox_width);
13: my $scale_y = $page_height/$bbox_height;
14: my ($scale, $x, $y);
15: if ($scale_x <= $scale_y) {
16: $scale = $scale_x;
17: $x = 0;
18: $y = ($page_height - $scale*$bbox_height)/2;
19: }
20: else {
21: $scale = $scale_y;
22: $x = ($page_width/2 - $scale*$bbox_width)/2;
23: $y = 0;
24: }
25: $pg_out->gfx->formimage($xo, $x - $ps[0] + ($position ? $page_width/2 : 0), 26:$y - $ps[1], $scale);
27:}
Метод importPageIntoForm в строке 3 возвращает нужную нам страницу исходного документа в виде «непрозрачного» объекта X-Object. С такими объектами можно выполнять различные преобразования и располагать их в любом месте страницы, но узнать, что находится у них внутри, нельзя. Условие в строках 5-7 выясняет, был ли для исходной страницы установлен CropBox (иными словами – были ли обрезаны поля), и, если это так, создает на основе этой информации область отсечения для объекта X-Object (иначе эта информация будет потеряна – X-Object включает в себя лишь содержимое страницы, но не ее свойства, где, как мы помним, находится поле CropBox). К сожалению, в текущей реализации PDF:API2 не существует метода get_cropbox, поэтому данную информацию приходится извлекать таким «низкоуровневым» способом. В строках 8-10 мы находим ширину и высоту импортируемого объекта и затем вычисляем по ним коэффициенты масштабирования (строки 1113, обратите внимание, что подпрограмма может работать с любыми размерами исходных страниц и печатных листов). Условие в строках 15-24 обеспечивает пропорциональность масштабирования, а метод formimage в строке 25 отображает уменьшенную копию страницы на печатном листе.
Ну вот и все! Теперь осталось только распечатать полученную книжку и отнести в ближайший копи-центр для сшивания, а используя этот сценарий вместе с программой для извлечения страниц, можно распечатать и удобно сшить даже самый монументальный труд. Удачи!
Все обсуждаемые примеры можно загрузить с сайта журнала http://www.samag.ru в разделе «Исходный код».
Приложение
«Изъясняемся по-русски»
«Слипание» букв русского алфавита происходит из-за того, что PDF::API2 не обладает достаточной информацией о их ширине в каждом конкретном шрифте. Существует несколько способов исправить «заморский акцент» PDF::API2:
- Использовать только core-шрифты Verdana и Georgia – для них эти сведения имеются.
- Использовать встраиваемые шрифты TrueType или PostScript. Здесь также не исключены проблемы, но они куда менее вероятны – вся информация берется непосредственно из файлов шрифтов.
- Добавить в файлы PDF/API2/Resource/Font/Corefont/*.pm информацию о ширине символов кириллицы (U+0x04NN). Это не так-то просто, но если вы все же справитесь с этой задачей – не забудьте отправить «заплатку» автору модуля, и благодарное сообщество вас не забудет.
Ссылки:
- http://www.printaform.com.au/clients/pdfapi2 – несколько устаревшая, но не потерявшая актуальности статья, детально рассматривающая вопросы верстки текста с помощью PDF::API2.
- http://partners.adobe.com/public/developer/pdf/index_reference.html – здесь можно загрузить официальную спецификацию формата PDF от Adobe.
- http://www.accesspdf.com/pdftk – домашняя страница PDF Toolkit – открытой программы для обработки PDF-файлов.