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

от автора

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

Не так давно мне пришлось столкнуться с типичной задачей – формировать документы с пользовательскими данными на основе шаблонов 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. Прошу прощения за отвратительное форматирование.

ссылка на оригинал статьи http://habrahabr.ru/post/195160/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *