PHP

индекс
206,63

Простейшая генерация odt файла из существующего

Как-то раз передо мной стояла задача реализовать генерацию договоров для клиентов с нашего корпоративного сайта.
Сначала задача была решена просто ужасно — был заготовлен html шаблон договора, а пользователю выдавалась конвертация шаблона в pdf. Само собой это выливалось в кучу неудобств, в том числе, если требовалось что-то поменять в договоре.

Следующим решением было генерация odt документа. Это позволило редактировать документ нашим менеджерам независимо от сайта и программистов.
Полностью генерировать с нуля смысла нет. Почему бы не поработать с уже имеющимся файлом (отредактированным в OpenOffice) и просто заменить в нем необходимые элементы?

Этим мы с вами и займемся.

Но для начала…
UPD! Вторая статья с исправлением ошибок этой — habrahabr.ru/blogs/php/87254/
Пару комментариев:
— Мы сильно ограничим нашу задачу изменением данных только в тексте документа, а также только текстовых переменных.
— Для решения данной задачи лично я использовал SimpleXML, ZIPArchive. Никто Вам не запрещает пользоваться другими инструментами.
— То, что описано в статье — упрощенный и урезанный пример, а не готовый инструмент.

Создание шаблона в OpenOffice:
Создаем обычный документ odt и в нужных местах вставляем пользовательские переменные:
Меню «Вставка» -> Поля -> Дополнительно
Вкладка «Переменные»
Выбираем «поле пользователя» и добавляем/вставляем поля как показано на изображении.
image
Сохраняем наш файл. В моем примере это файл test.odt.

Загружаем файл на сервер:
ODT как и любой ODF файл, как многим наверно известно, является обычным ZIP архивом.

upload.php
<?php
 
//путь к временному архиву
$tmpfile='upload/temp.zip';
 
//сохраняем полученный документ
if (isset($_FILES['document']) and move_uploaded_file($_FILES['document']['tmp_name'], $tmpfile)) {
 
// функция удаления директории
    function deleteDirectory($dir) {
        if (!file_exists($dir)) return true;
        if (!is_dir($dir) || is_link($dir)) return unlink($dir);
        foreach (scandir($dir) as $item) {
            if ($item == '.' || $item == '..') continue;
            if (!deleteDirectory($dir . "/" . $item)) {
                chmod($dir . "/" . $item, 0777);
                if (!deleteDirectory($dir . "/" . $item)) return false;
            };
        }
        return rmdir($dir);
    }
 
    // удаляем директорию с содержимым документа и создаем заново
    // при желании, можно перемещать старую версию куда-либо, сделав тем самым версионность документа
    deleteDirectory('doc/');
    mkdir('doc/');
 
 
    // извлекаем архив
    $zip = new ZipArchive;
    if ($zip->open($tmpfile) === TRUE) {
    // Сохраняем пути к файлам в нужной последовательности
    // Это нам понадобится в будущем.
    // Например, по требованию формата odf , файл mimetype должен быть первым в архиве.
        $files=array();
        for($i = 0; $i < $zip->numFiles; $i++) {
            $files[]=$zip->getNameIndex($i);
        }
        file_put_contents("doc.list",implode("\n",$files));
 
        //извлекаем
        $zip->extractTo('doc/');
        $zip->close();
    } else {
        die("zip error");
    }
 
    unlink ($tmpfile);
 
    $print='Файл успешно загружен';
}
else {
    $print='
      <form action="" method="post" enctype="multipart/form-data">
      <input type="file" name="document"><br>
      <input type="submit" value="Загрузить"><br>
      </form>'
;
}
print '<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Загрузка документа</title>
</head>
<body>
'
.$print.'
</body>
</html>'
;
 
?>


При успешной загрузке мы получаем папку с содержимым odt файла и doc.list со списком файлов.

Отдаем измененный файл пользователю:
Нам нужно заменить значения пользовательских полей и сжать все обратно в архив.

download.php

<?php
 
//путь к временному файлу
$tmpfile='download/doc.odt';
//файл, который будем отдавать
$outname='zayavlenie.odt';
 
 
//удаляем старый файл
unlink($tmpfile);
 
 
//создаем новый архив
$zip = new ZipArchive;
if ($zip->open($tmpfile,ZIPARCHIVE::CREATE) === TRUE) {
//проходимся по структуре нашего архива
    $files=file('doc.list');
    foreach ($files as $filename) {
        $filename=trim($filename);
 
        //если директория - добавляем ее
        if (is_dir('doc/'.$filename)) {
            $zip->addEmptyDir($filename);
        }
        //иначе добавляем файл
        else {
 
        //если нужный файл, то проводим в нем подстановку пользовательских полей
            if ($filename=="content.xml") {
 
            //значения полей
                $vars=array(
                    'ФИО'=>'Иванова И.И.',
                    'Дата'=>date('d.m.Y'),
                    'Планета'=>'Юпитер'
                );
 
                //создаем объект simplexml
                $xml = new SimpleXMLElement(file_get_contents('doc/'.$filename));
 
                //получаем заранее нужные namespace
                $ns=$xml->getNamespaces(true);
 
                // две переменные, необходимые для доступа к элементам xml и к атрибутам
                $usr="user-field-decls";
                $str="string-value";
 
                //проверяем есть ли в файле пользовательские поля
                if ($fields=$xml->children($ns["office"])->body->text->children($ns["text"])->$usr) {
                //если есть, пробегаемся по ним и заменяем их атрибут string-value на новый
                    foreach ($fields->children($ns["text"]) as  $field) {
 
                        if (isset($vars[(string)$field->attributes($ns["text"])->name])) {
                            $field->attributes($ns["office"])->$str = $vars[(string)$field->attributes($ns["text"])->name];
                        }
                    }
 
                }
                //добавляем в архив
                $zip->addFromString($filename, $xml->asXML());
            }
            else {
            //добавляем в архив из файла
                $zip->addFile('doc/'.$filename,$filename );
            }
        }
    }
 
 
    $zip->close();
} else {
    die("zip error");
}
 
//очищаем буфер и выдаем файл
ob_clean();
 
header('Content-Disposition: attachment; filename="'.$outname.'"');
header('Content-type: application/odt');
print file_get_contents($tmpfile);
 
?>
 


Вуаля.

Важные замечания и ссылки:
— В примере я использовал только текстовые поля, но Вы можете использовать также и другие типы полей.
— В ODT есть также возможность использоваться условные элементы (например часть текста показывается или не показывается в зависимости от условия — например значения пользовательского поля)
— В примере я менял значения полей только в content.xml. Но поля могут использоваться и в других файлах, например в styles.xml находятся колонтитулы.
спецификация ODF (точнее Open Document)
— Как и всё в мире — пример можно оптимизировать. Например, если требуется менять только content.xml, то никто не запрещает подготовить заранее архив, а при запросе пользователя заменять/добавлять в него этот файл.

Скачать исходник целиком
+31
14 сентября 2009, 18:53
96

комментарии (23)

+4
kabr #
Вот за что люблю хабр.
Порой бывают очень полезные статьи.
Вчера только задумывался над вопросом генерации документа из шаблона на сайте,
а сегодня уже ответ готовый… Даже подумать как следует не дали :D
+1
dotZero #
можешь реализовать это для ms word'a
0
Irker #
сомневаюсь что это так же легко реализуется для doc. Для docx — я думаю делается так же легко.
0
Goodkat #
Сохраняй шаблон как rtf — это текстовый формат, в нём точно так же можно заменять плэйсхолдеры (я не знал про переменные в тексте, вставлял свои, типа %firstname%, %lastname%...)
0
appy #
сердитый вариант: сгенерировать простой HTML без стилей и отдать как application/msword. Работает в word на ура.

сердитый вариант при сложной структуре: открыть шаблон в word и сохранить как HTML. В полученный HTML скриптом вносить требуемые изменения и отдавать как application/msword. Вместо HTML можно попробовать MHT.
проблемы такого способа в том что конвертить из doc получается только либо руками, либо через VB на windows сервере. Для некоторого круга задач это приемлемо. Еще одна проблема в том что в текстовых процессорах отличных от word все обычно едет к чертям, а в случае с MHT вообще не работает.
0
guessss_who #
Для MS Office наиболее простым вариантом будет работа с нужным нам приложением (Word, Excel, PowerPoint...) через COM-объект. Но для этого на сервере должны присутствовать Windows и MS Office. :(

Еще, конечно, можно отправлять HTML с «правильным» заголовком, но тогда наши возможности ограничиваются возможностями HTML/CSS, а это совсем не то-же самое, что возможности MS Office…
0
bondsman #
Гениально!
Особенно радует простота предложенного решения.
0
GloooM #
В августовском выпуске LinuxFormat как раз эта же тема и почти такое же решение описывали.
0
GloooM #
Радует как все оказывается просто, ато открыл в текстовом редакторе и увидел кашу из букавок, ан нет это зип-архив.
0
Irker #
Не читаю этот журнал. Мое решение работает у меня уже почти год=)
Статью подтолкнула написать другая статья с хабрахабра: habrahabr.ru/blogs/php/69417/

Вообще, я не претендую на изобретение — спецификация описана очень подробно и кто угодно, задавшись целью может легко сделать что-то и лучше и удобнее ;)
0
shadeR #
Мы сейчас разрабатываем систему документооборота на основе шаблонов ODT. Ваш пример очень упрощен но направление верное. Например, если нужно выводить список клиентов и их заказов (число не известно, как не известно и какие из их свойств выводить).
Близкий пример — генерация инвоиса по шаблону. И учтите еще вот какой момент — пользователь может вводить теги не только вашим способом, но и копируя из из одного документа в другой, или вбив текст, похожий на ваш (вбил дата, выделил цветом). И будет справедливо ждать что это сработает. А такой «тег» может быть «разорван» на 2 и более частей в XML — находится в разных нодах.
В общем, все гораздо сложнее.
0
Irker #
вы читали пункт статьи «Пару комментариев»?
0
shadeR #
Я не критикую ваш подход. Просто указываю на то что вы даже в этой простой задаче работаете в идеальных условий, хотя наверное вы и правы. Кстати, где можно прочитать более подробно о «В ODT есть также возможность использоваться условные элементы (например часть текста показывается или не показывается в зависимости от условия — например значения пользовательского поля)»?
0
Irker #
authors.i-rs.ru/OOo3/WG3/0200WG3-Writer%20Guide.pdf
Это документация к OpenOffice Writer. Поиск по «условный текст» приведет почти сразу куда нужно.
0
sky_lord #
К сожалению, в гос.организации, которой программлю и аутсорсю, используют сплошные MS Word/Excel. :-( А тоже очень хотелось генерировать ODF на лету. Промучавшись с попытками адекватно выводить что-то в Ворд/Эксель пошел по пути наименьшего сопротивления — выдавать наружу html с расширением doc/xls и соответствующим mime-type. Получающиеся файлы отлично всасываются хоть MSOffice'ом, хоть OpenOffice'ом, чем все и довольны. Но не оставляю надежду таки доказать представителям гос.организации, что использовать пиратское — нехорошо и лучше перейти на тот же бесплатный OpenOffice. :-)
0
allter #
Можно работать с OO (шаблоны хранить в ODF), а потом из файла генерить MS-совместимые файлы (запуская oo в headless режиме).
0
sky_lord #
Спасибо за совет — буду иметь в виду на будущее. :-) Но, откровенно говоря, городить такой огород на веб-сервере немного лениво… По крайней мере для моих задач…
0
BSDaemon #
хех… У нас у заказчика тоже самое, спрошлой ворд да эксель, но по большей части все-таки отчеты в экселе. Раньше тоже так же отдавал под нужным mime-type html, но потом нашли библиотечку одну php_writeexcel, слегка допилили ее и в общем используем.
0
BSDaemon #
Она генерит в старом формате biff5, но тем не менее — это «настоящий» excel.
0
sky_lord #
Спасибо за ссылку — посмотрю. :-) Заказчика и в том виде устроило, так что без разницы. А вот когда нужно было не таблицы, а именно текстовые файлы для распечатки, причем с разным хитрым форматированием — тогда пришлось PhpRtf использовать. У нее, по-моему, и альтернатив-то нет по сути…
0
BSDaemon #
Да, если говорить про ворд, то однозначно Phprtf.
0
absinthe #
Минусуйте, если совсе не в тему, но интересен вопрос — как(чем?) сгенерить серию jpg/png/gif (да хоть многостраничный tiff или pdf) из этих чертовых doc/docx/xls/xlsx/odf…

сорри за оффтоп, сорри, но наболело
0
errno #
Спасибо, статья пригодится, только мы наверное пойдем дальше, попробуем присобачить OOo в headless режиме, для печати и конвертации в другие форматы (.doc, .pdf).

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.