Pull to refresh

Формирование документов на основе ODT шаблонов. ODT to PDF

PHP *PDF
Sandbox
Здравствуйте, уважаемы хабровчане!

Не так давно мне пришлось столкнуться с типичной задачей – формировать документы с пользовательскими данными на основе шаблонов ODT средствами PHP. Звучит весьма тривиально, но намучиться пришлось сильно. Дело в том, что ни одно из доступных средств, так или иначе, не подошло. Одни библиотеки формировали документ криво, другие не поддерживали русские шрифты, третьи – двигали картинки в стиле Harlem Shake. Вот и пришлось «велосипедить».

Итак, задача вкратце:

  1. Обработать ODT шаблон. Заменить placeholder’ы на пользовательские значения
  2. Конвертировать в pdf. Показать пользователю



ЭТАП 1. Обработать ODT шаблон. Заменить placeholder’ы

Ни для кого не секрет, что ODT — это обычный архив с xml на борту. Все картинки прячутся в папке, название которой может быть любым, лишь бы на нее ссылались в файле описаний. Не будем вдаваться в подробности: достаточно лишь сказать, что за основной контент документа отвечает content.xml, за «описательную» часть – manifest.xml. Обращаю внимание, что стили текста нас не интересуют (по крайней мере, в условиях данной задачи). Копнув чуть глубже эти xml’ки выводим алгоритм:

  1. Распаковать архив
  2. Для подмены текста: парсим content.xml, заменяем placeholder’ы на нужные значения
  3. Для изображений: загружаем свои изображения в папку (создаем ее внутри распакованного .odt документа), парсим content.xml, заменяем placeholder’ы на frame вида

    <draw:frame draw:style-name="a0" draw:name="'.$file_name.'" text:anchor-type="as-char" svg:x="0in" svg:y="0in" svg:width="'.$width.'in" svg:height="'.$height.'in" style:rel-width="scale" style:rel-height="scale">
        <draw:image xlink:href="'.self::_ImgDir.DIRECTORY_SEPARATOR.$file_name.'" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>
       <svg:title/>
       <svg:desc/>
    </draw:frame>
    


    Далее, добавляем в конец manifest.xml блок вида

    <manifest:file-entry manifest:full-path="'.self::_ImgDir.DIRECTORY_SEPARATOR.$file_name.'" manifest:media-type="image/'.$ext.'"/>
    

  4. Архивируем результат обратно в ODT.


Алгоритм явно упрощен, зато действенен и легок. На его основе был написан класс (на коленках) odtFormat. Небольшая справка о том, как им пользоваться:

Инициализация
$odtformat = new odtFormat(“$doc_path”, "$temp_dir");

$doc_path – путь до шаблона .odt
$temp_dir – папка, в которой будут храниться временные файлы.

Вставка текста
$odtformat->SetText(“$name”, “$value”);

$name – имя placeholder’а
$value – пользовательское значение

Вставка изображения
$odtformat->SetImage(“$name”, “$img_path” ,$width, $height);

$name – имя placeholder’a
$img_path – путь до картинки
$width – желаемая ширина изображения в документе (если не задать – ширина оригинала)
$height – желаемая длина изображения в документе (если не задать – длина оригинала)

Сохранение документа
$odtformat->SaveToDisk(“$path_to_save”);

$path_to_save – куда будем сохранять (путь+имя файла)

Остальные методы не привожу, все можно увидеть в исходнике. Тем более, что большинство вспомогательных методов взято из публичных источников. Да и сам код прост, как две копейки. Приведу лишь некоторые настройки:

const _SeporatorLeft = '{{';

Отделяет placeholder от текста слева

const _SeporatorRight = '}}';

Отделяет placeholder от текста справа

const _ImgDir = 'media';

Имя папки с пользовательскими изображениями
Ну вот, можно сказать, что файл .odt, заполненный нужными нам данными, уже сформирован. Время для второго этапа.

Внимание! Чтобы placeholder'ы заменялись корректно, при добавлении их в документ используйте «Очистить формат». Класс использовал для MS World 2013. Но что-то мне подсказывает, что содержание odt одинаково и в других версиях.

ЭТАП 2. Конвертировать в pdf. Показ пользователю

Сразу скажу, что идеального решения так и не нашел. Решений, по существу, практически нет. Перерыв «интернеты», наткнулся на горстку тяжеловесов, zend примочек и просто хлама. Так что, расскажу все, как было.
Первым делом, пытался использовать онлайн-сервисы. Сначала был Google Docs. Тут все ясно. Просто показ документа на странице через iframe, избегая самого конвертирования.
Пример:
<iframe src="http://docs.google.com/viewer?url=http%3A%2F%2F127.0.0.1%2Fa.odt&embedded=true" width="600" height="780" style="border: none;"></iframe>

Минусы:
  • Скорость отображения
  • Кривое форматирование
  • При использовании сторонних библиотек для формирования odt, появлялся странный белый лист вначале документа
  • Не pdf
  • Онлайн

Плюсы:
  • Простота


Очевидно, что данное решение долго не жило. Стоит глянуть в сторону Microsoft с их Office Apps. Интересным хинтом стало конвертирование pdf как версии для печати (встроенная функция онлайн сервиса). Таким образом, можно средствами Microsoft конвертировать файл и тут же его показать.
Пример:
<iframe src="http://co1-word-view.officeapps.live.com/wv/WordViewer/request.pdf?WOPIsrc=http%3A%2F%2Fco1%2D15%2Dview%2Dwopi%2Ewopi%2Elive%2Enet%3A808%2Foh%2Fwopi%2Ffiles%2F%40%2FwFileId%3FwFileId%3Dhttp://127.0.0.1/a.odt&type=printpdf" width="600" height="780" style="border: none;"></iframe>

Минусы
  • Безумно долго
  • Решение, само по себе, кривое
  • Непредсказуемое поведение в разных браузерах
  • Так и не удалось избавиться от окна печати
  • Онлайн

Плюсы
  • Качественное конвертирование


Не найдя больше достойных онлайн-вариантов, решено было использовать средства сервера. С этой задачей хорошо справляется Libreoffice. У него есть встроенный конвертер документов, работающий из командной строки. Идея заключалась в том, чтобы забрасывать сформированные odt в папку, передавать ее ключом к exec, отображать уже готовые pdf, лежащие в той же папке. Положим, что apt-get install libreoffice мы уже сделали. Осталось лишь дописать одну строку кода:

exec(“libreoffice --headless --invisible --convert-to pdf $full_path_to_file --outdir $full_path_to_dir”);

$full_path_to_file – полный путь до файлов (/var/www/*.odt)
$full_path_to_dir – полный путь до папки сохранения (/var/www/result/)

Как показать pdf в iframe, я думаю, вы и сами знаете.

Минусы
  • Нужен доступ к серверу
  • Тяжелый пакет libreoffice
  • exec (подобные команды в коде – дело не очень-то хорошее)

Плюсы
  • Заметный прирост в скорости
  • Качественное форматирование
  • Удобство использования готовых документов
  • Абсолютно локальное решение


Заключение
При всех недостатках решения — цели выполнены. Надеюсь, что эта статья поможет избежать некоторых трудностей при выполнении подобной задачи. Всем удачного кодинга!

Ссылки
Библиотека odtFormat
О LibreOffice

P.S. Прошу прощения за отвратительное форматирование.
Tags:
Hubs:
Total votes 6: ↑6 and ↓0 +6
Views 10K
Comments Comments 8