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

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

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

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

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

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


    Сохраняем наш файл. В моем примере это файл 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, то никто не запрещает подготовить заранее архив, а при запросе пользователя заменять/добавлять в него этот файл.

    Скачать исходник целиком
    Поделиться публикацией
    Ой, у вас баннер убежал!

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

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

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

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

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

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

                        Самое читаемое