Обработка 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)

   Источник статьи