Не так давно мне пришлось столкнуться с типичной задачей – формировать документы с пользовательскими данными на основе шаблонов ODT средствами PHP. Звучит весьма тривиально, но намучиться пришлось сильно. Дело в том, что ни одно из доступных средств, так или иначе, не подошло. Одни библиотеки формировали документ криво, другие не поддерживали русские шрифты, третьи – двигали картинки в стиле Harlem Shake. Вот и пришлось «велосипедить».
Итак, задача вкратце:
- Обработать ODT шаблон. Заменить placeholder’ы на пользовательские значения
- Конвертировать в pdf. Показать пользователю
ЭТАП 1. Обработать ODT шаблон. Заменить placeholder’ы
Ни для кого не секрет, что ODT — это обычный архив с xml на борту. Все картинки прячутся в папке, название которой может быть любым, лишь бы на нее ссылались в файле описаний. Не будем вдаваться в подробности: достаточно лишь сказать, что за основной контент документа отвечает content.xml, за «описательную» часть – manifest.xml. Обращаю внимание, что стили текста нас не интересуют (по крайней мере, в условиях данной задачи). Копнув чуть глубже эти xml’ки выводим алгоритм:
- Распаковать архив
- Для подмены текста: парсим content.xml, заменяем placeholder’ы на нужные значения
- Для изображений: загружаем свои изображения в папку (создаем ее внутри распакованного .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.'"/>
- Архивируем результат обратно в 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/
Добавить комментарий