среда, 18 декабря 2019 г.

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

habr.com


Здравствуйте, уважаемы хабровчане!
Не так давно мне пришлось столкнуться с типичной задачей – формировать документы с пользовательскими данными на основе шаблонов 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 (подобные команды в коде – дело не очень-то хорошее)
Плюсы
  • Заметный прирост в скорости
  • Качественное форматирование
  • Удобство использования готовых документов
  • Абсолютно локальное решение
Заключение
При всех недостатках решения — цели выполнены. Надеюсь, что эта статья поможет избежать некоторых трудностей при выполнении подобной задачи. Всем удачного кодинга!

Комментариев нет:

Отправить комментарий