Обработка PDF с помощью Perl
Adobe PDF является стандартом для обмена текстовыми документами.
Большинство офисных пакетов могут экспортировать данные из внутренних
форматов в pdf, однако функция эта зачастую не обладает широким
набором возможностей и с желаниями более продвинутых пользователей
может не справиться. Например, попробуйте скомбинировать несколько pdf
файлов в один или создать навигационную панель на основе закладок.
Представьте себе, что стоит задача поддерживать документ, содержащий
тематическую выборку статей с perl.com, при этом навигационная панель
так же должна обновляться. Для решения данной проблемы можно было бы
воспользоваться утилитами типа HTMLDOC, но вот если потребуется
добавить статью номер 51, то нужно будет предварительно закачать
первые 50. Скорее всего, даже пройдя через все мучения, Вы не будете
удовлетворены результатом. Данная статья рассказывает, как можно
воспользоваться модулем PDF::Reuse(автор Lars Lundberg) для
создания комбинированных документов и навигационных панелей.
Создание демонстрационных документов
Не стоит рассчитывать на что, что, используя PDF::Reuse, Вы
сможете создавать потрясающие по красоте и наполнению документы.
Возможность данного модуля достаточно ограничены, так что для
полноценной работы рекомендую ознакомиться с такими пакетами, как
PDF::API2 или Text::PDF. Однако, даже базовые возможности
позволяют создать простенький документ, который мы будем использовать
в дальнейших примерах.
examples/create-pdfs.pl
use strict;
use PDF::Reuse;
mkdir "out" if (!-e "out") ;
foreach my $x (1..4) {
prFile("out/file-$x.pdf");
foreach my $y (1..10) {
prText(35,800,"File: file-$x.pdf");
prText(510,800,"Page: $y");
foreach my $z (1..15) {
prText(35,700-$z*16,"Line $z");
}
# add graphics with the prAdd function
# stroke color
prAdd("0.1 0.1 0.9 RG\n");
# fill color
prAdd("0.9 0.1 0.1 rg\n");
my $pos = 750 - ($y * 40);
prAdd("540 $pos 10 40 re\n");
prAdd("B\n");
if ($y < 10) {
prPage();
}
}
prEnd();
}
Как видно из данного примера, для открытия файла используется функция
prFile($filename), а для закрытия - prEnd(). В промежутке между этими
двумя вызовами мы добавили некоторый текст с использованием функции
prText. Так же Вы можете работать с графикой посредством функции
prAdd, используя стандартный синтаксис pdf для работы. По умолчанию
вся работа ведется на текущей странице. Для того, чтобы перейти на
другую или создать новую страницу, вызовите prPage(). Функция prFile
автоматически создает одну пустую страницу, так что каждую новую нужно
создавать вручную.
В примере мы создали красный прямоугольник с синими границами с
использованием функции prAdd. Как Вы поняли, это низкоуровневый вызов,
использующий нотацию самого pdf. Если у Вас появится желание и большое
количество свободного времени, можно более детально изучить информацию
в разделе PDF reference manual. Для более комфортной работы с
графикой желательно воспользоваться PDF::API2 или Text::PDF,
которые предоставляют высокоуровневые абстракции для манипуляций с
графикой.
Комбинируем документы
Основное предназначение PDF::Reuse - обработка уже существующих
документов. Давайте создадим один большой файл на основе полученных
ранее pdf-ов.
examples/combine-pdfs.pl
use strict;
use PDF::Reuse;
prFile("out/resultat.pdf");
prDoc('out/file-1.pdf',1,4);
prDoc('out/file-2.pdf',2,9);
prDoc('out/file-3.pdf',8);
prDoc('out/file-4.pdf');
prEnd();
Как и ранее, prFile открывает новый файл. Затем, используя
prDoc($filename, $firstPage, $lastPage), мы добавляем в новый файл
различное количество страниц из исходных документов. Например, из
file-1.pdf мы взяли первые 4, из file-3.pdf - только восьмую страницу.
Аргументы $firstPage и $lastPage являются опциональными. Их полное
отсутствие подразумевает, что будет использоваться весь документ.
Используем существующие pdf файлы
К достаточно полезным функциям модуля[19] PDF::Reuse можно отнести
создание документов на основе pdf-шаблонов. Например, у нас есть файл
customers.txt, который содержит список имен пользователей, которым
нужно будет отправить именные письма. У Вашей компании есть свои
собственные шаблоны таких писем, созданные в том же OpenOffice , но
нет желания готовить документы для всей 1000 клиентов вручную.
Используя perl, очень легко разобрать файл с именами и создать набор
документов:
examples/reuse-letter.pl
use PDF::Reuse;
use Date::Formatter;
use strict;
my $date = Date::Formatter->now();
$date->createDateFormatter("(DD).(MM). (YYYY)");
my $n = 1;
my $incr = 14;
my $infile = 'examples/customer.txt';
prFile("examples/sample-letters.pdf");
prCompress(1);
prFont('Arial');
prForm("examples/sample-letter.pdf");
open (my $fh, "<$infile") || die "Couldn't open $infile, $!\n aborts!\n";
while (my $line = <$fh>) {
my $x = 60;
my $y = 760;
my ($first, $last, $street, $zipCode, $city, $country) = split(/,/, $line);
last unless $country;
prPage() if $n++ > 1 ;
prText($x, $y, "$first $last");
$y -= $incr;
prText($x, $y, $street);
$y -= $incr;
prText($x, $y, $zipCode);
prText(($x + 40), $y, $city);
$y -= $incr;
prText($x, $y, $country);
prText(60, 600, "Dear $first $last,");
prText(400, 630, "Berlin, $date");
}
prEnd();
close $fh;
Сразу после открытия файлы с помощь prFile, мы включаем компрессию
данных, вызвав prCompress(1). Устанавливаем шрифт по умолчанию,
используя prFont. Стандартными значениями являются Times-Roman,
Times-Bold, Times-Italic, Times-BoldItalic, Courier, Courier-Bold,
Courier-Oblique, Courier-BoldOblique, Helvetica, Helvetica-Bold,
Helvetica-Oblique и Helvetica-BoldOblique. Все последующие действия
представляют собой элементарные итерации через данные, хранимые в
файле с их последующей печатью через prText.
Добавляем нумерацию страниц
Иногда бывает необходимо внести лишь небольшие изменения в документ,
например, пронумеровать страницы:
examples/sample-numbers.pl
use PDF::Reuse;
use strict;
my $n = 1;
prFile('examples/sample-numbers.pdf');
while (1) {
prText(550, 40, $n++);
last unless prSinglePage('sample-letters.pdf');
}
prEnd();
prSinglePage извлекает следующую страницу открытого документа и
возвращает количество оставшихся страниц.
Низкоуровневые команды формата PDF
Если по счастливой случайности или в результате работы Вы
познакомились с синтаксисом pdf,[20] PDF::Reuse даст возможность
воспользоваться этими знаниями. Команда prAdd позволит вводить команды
непосредственно в тело документа, однако следует быть осторожным, так
как модуль не проводит проверки на корректность синтаксиса. Приведем
небольшой пример, показывающий, как нарисовать небольшие залитые
цветом прямоугольники:
use PDF::Reuse;
use strict;
prFile('examples/sample-rectangle.pdf');
my $x = 40;
my $y = 50;
my @colors;
foreach my $r (0..5) {
foreach my $g (0..5) {
foreach my $b (0..5) {
push @colors,
sprintf("%1.1f %1.1f %1.1f rg\n",
$r * 0.2, $g * 0.2, $b * 0.2);
}
}
}
while (1) {
if ($x > 500) {
$x = 40; $y += 40;
last unless @colors;
}
# a rectangle
my $string = "$x $y 30 30 re\n";
$string .= shift @colors;
# fill and stroke
$string .= "b\n";
prAdd($string);
$x += 40;
}
prEnd();
Создание оглавления
Одним из неоспоримых преимуществ формата pdf является наличие в нем
механизма ссылок, который позволяет создавать оглавления. Очень многие
приложение не умеет создавать оглавления или делают это некорректно, в
результате чего документ становиться практически бесполезным.
PDF::Reuse позволяет решить эту проблему путем использования
функции prBookmark($reference).
Переменная $reference представляет собой ссылку на массив или хеш,
который может иметь следующую структуру:
{ text => 'Document-Text',
act => 'this.pageNum = 0; this.scroll(40, 500);',
kids => [ { text => 'Chapter 1',
act => '1, 40, 600'
},
{ text => 'Chapter 2',
act => '10, 40, 600'
}
]
}
где act является кодом на JavaScript, который отрабатывается каждый
раз после того, как пользователь нажал на ссылку. Насколько мне
известно, в настоящий момент существует только одно приложение,
корректно отрабатывающее данный callback(приложение делает компания
Adobe), поэтому мы покажем в дальнейшем, как это исправить.
Другие, достаточно интересные примеры, такие как вставка изображений,
можно найти в документе PDF::Reuse::Tutorial.
Консольное приложение для объединения pdf файлов
Для того, чтобы избавиться от утомительной и достаточно неблагодарной
задачи каждый раз редактировать исходные тексты, задавая имена файлов
и номера страниц, я написал небольшой скрипт, который будет выполнять
обработку pdf-ов, принимая всю необходимую информацию через параметры
командной строки. Чтобы сделать приложение более модульным, я вынес
основной код в новый модуль, получивший название CombinePDFs. Это даст
нам возможность применять его не только в консольном варианте
программы, но и в более дружественных к пользователю, использующих
GUI(такой как Perl/Tk).
Приложение app-combine-console-pdfs.pl не использует модуль
PDF::Reuse напрямую, оно лишь обрабатывает параметры командной
строки с помощью модуля Getopt::Long. Это своего рода стандарт de
facto для данных задач в мире perl. Мы разбираем и сохраняем список
файлов и количество страниц в два массива одинаковой длинны.
Пользователь так же может указать имя выходного документа и файл,
содержащий описание ссылок. Главная функция приложения, которая
обрабатывает входные параметры и вызывает CombinePDFs::createPDF:
sub main {
GetOptions("infile=s" => \@infiles,
"outfile=s" => \$outfile,
"pages=s", => \@pages,
'overwrite' => \$overwrite,
'bookmarks:s' => \$bookmarks,
'help' => \&help);
help unless ((@infiles and $outfile and @pages) and @pages == @infiles);
checkPages();
checkFiles();
checkBookmarks();
CombinePDFs::createPDF(\@infiles, \@pages, $outfile, $bookmarks);
}
В случае, если пользователь передал некорректное количество входных
аргументов, неправильные имена файлов или неверные диапазоны страниц,
приложение распечатает небольшую справку по использованию. Любое
хорошее консольное приложение должно быть написано в таком стиле.
Модуль CombinePDFs
Основная задача приложения состоит в проверке корректности ввода
данных. Если все введено правильно, вызывается функция
CombinePDFs::createPDF, которой в качестве аргументов передаются
массив файлов, массив, содержащий диапазон страниц и опциональный
параметр, описывающий ссылки.
Диапазон страниц может быть представлен как в формате с запятой в
качестве разделителя - (1-10, 14-22, 44-46), так и в виде отдельных
одиночных страниц или же с помощью специального тега all.
Далее происходит проверка файлов - права на чтение, тест, является ли
файл документом pdf. Последняя проверка достаточно наивна, так как
использует всего-лишь чтение первой строки с ее последующим сравнением
с шаблоном %PDF-1.[0-9].
Модуль PDF::Reuse не является объектно-ориентированным, так что
CreatePDFs не сильно отличается от прародителя.
Передача большого набора параметров, которыми является описание
закладок - достаточно трудоемкая задача, так что я решил
воспользоваться для этого отдельным текстовым файлом следующего
формата:
<level> "bookmarks text" <page>
Отсчет уровня начинается с 0. К корневому уровню с номером 0 примыкают
листья с последующими номерами 1, 2 и 3. Если подряд указывается
несколько одинаковых уровней, то это значит, что первый из них
является корневым, а каждый последующий - вложенным. Всего допускается
уровень вложенности, равный 3.
0 "Folder File 1 - Page 1" 1
1 "File 1 - Page 2" 2
1 "Subfolder File 1 - Page 3" 3
2 "File 1 - Page 4" 4
0 "Folder File 2 - Page 7 " 7
1 "File 2 - Page 7" 7
1 "File 2 - Page 9" 9
Функция обработки файла закладок в называется
CombinePDFs::addBookmarks($filname). Ее логика не представляет из себя
ничего особо сложного. Закладки являются массивом хешей. Хеш включает
в себя следующие ключи: text - название ссылки, act - действие,
которое должно быть выполнено, когда пользователь нажал на закладку, в
нашем случае это номер страницы для перехода. kids содержит список
вложенных ссылок.
После того, как модуль обработает все ссылки из файла описания, они
будут добавлены в общий документ с помощью функции
prBookmarks($reference).
Приведем пример запуска приложения с опциями командной строки:
perl bin/app-combine-pdfs.pl \
--infile out/file-1.pdf --pages 1-6 \
--infile out/file-2.pdf --pages 1-4,7,9-10 \
--bookmarks out/bookmarks.cnt \
--outfile file-all.pdf -overwrite
К сожалению, PDF::Reuse не позволяет управлять настройками
отображения документа после первоначальной загрузки(полный экран или
вид с разделителями), так что пользователю нужно будет вручную открыть
панель навигации, где будут расположены закладки. В следующем релизе
PDF::Reuse данная функция должна появиться, не исключено,что это
уже сделано на момент написания данной статьи.
Как уже говорилось, обрабатывать встроенный JavaScript пока может
только Acrobat Reader. Для того, чтобы остальные программы могли
работать с закладками, нужно заменить ключ act на page(во вложениях Вы
можете найти измененную версию PDF::Reuse, включающую данные
модификации):
$bookmarks = { text => 'Document',
page => '0,40,50;',
kids => [ { text => 'Chapter 1',
page => '1, 40, 600'
},
{ text => 'Chapter 2',
page => '10, 40, 600'
}
]
}
Этот код может быть вставлен в документ с использованием стандартной
prBookmark.
Приложение на Tk для постобработки PDF
Конечно, консольные приложения - вещь сильная, однако воспользоваться
ими могут достаточно продвинутые пользователи. Было бы неплохо
предоставить разным людям возможность управлять параметрами создания
документов из графической среды. Я решил воспользоваться для этого
модулем Perl/Tk. В приложении app-combine-tk-pdfs.pl можно в
графическом режиме выбирать файлы pdf, сортировать эти файлы в
Tk::Tree и задавать диапазоны страниц. Дополнительно приложение может
сохранять параметры генерации в специальных файлах сессий для их
последующей загрузки в будущем. В дополнение app-combine-tk-pdfs.pl
позволяет редактировать закладки и сохранять изменения в отдельном
файле.
Выводы
PDF::Reuse представляет собой неплохой модуль с подробной
документацией, который позволит существенно упростить процесс
создания, комбинирования и редактирования pdf файлов. Для демонстрации
возможностей данного модуля было разработано два приложения. Следует
помнить две вещи - PDF::Reuse не позволяет работать с уже
созданными закладками, а так же может нарушить работу уже существующих
в документе ссылок. В прилагаемых архивах Вы можете найти примеры,
использованные в данной статье и модифицированный код PDF::Reuse,
который позволит использовать page вместо act (решение проблемы с
JavaScript)
Источник статьи