Генерация документов в doc, excel, pdf и других форматах на сервере

    Выгрузка отчетов в различных форматах — типовая задача для многих проектов. И сейчас есть немало инструментов для этого. Среди них есть интересный вариант, который применяется, как мне кажется, не часто, но он однозначно стоит внимания. Потому что позволяет получить документ в нужном формате буквально одной командой. О нем и расскажу.

    image


    Я буду не многословен и сразу скажу, что речь идет о конвертере, встроенном в пакет LibreOffice. Вы можете запустить конвертацию из консоли, чтобы увидеть как это работает:

    libreoffice --headless --writer --convert-to pdf html.html
    

    Эта команда конвертирует файл html.html в pdf файл. Количество поддерживаемых форматов впечатляет.

    Выгода от использования такого инструмента очевидна. Вместо того, чтобы писать код для генерации документов в каждом из нужных форматов, просто создаем обычное html-представление. Далее сгенерированную страницу прогоняем через конвертер.

    Запуск конвертации из PHP


    Для установки конвертера на сервере придется установить пакет libreoffice-core:

    sudo apt-get install libreoffice-core --no-install-recommends

    Чтобы было удобно работать с утилитой из PHP, я написал обертку.

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

    Для работы с оберткой подключаем ее к своему проекту через composer:

    composer require mnvx/lowrapper

    Использовать ее можно так:

    use Mnvx\Lowrapper\Converter;
    use Mnvx\Lowrapper\LowrapperParameters;
    use Mnvx\Lowrapper\Format;
    
    // Создаем объект конвертера
    $converter = new Converter();
    
    // Описываем параметры для конвертера
    $parameters = (new LowrapperParameters())
        // На вход подаем строку с HTML
        ->setInputData('<html>My html file</html>')
        // В каком формате нужен результат
        ->setOutputFormat(Format::TEXT_DOCX)
        // Файл для сохранения результата
        ->setOutputFile('path-to-result-docx.docx');
    
    // Запускаем конвертацию
    $converter->convert($parameters);
    

    В результате будет сформирован docx файл. Больше примеров можно найти на гитхабе.

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

    Несколько граблей


    Будет полезно рассказать про несколько особенностей, с которыми я столкнулся при работе с этой утилитой.

    1. Применение CSS стилей. При преобразовании html в нужный формат имейте ввиду, что такая запись воспринимается корректно:

    <p class=”someclass”></p>

    А такие записи будут обработаны точно так же, как если бы class мы совсем не указали:

    <p class=”some_class”>Some text</p>
    <p class=”class1 class2”>Some text</p>

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

    td, th {
        border: 1px solid black;
    }
    

    Но так работает:

    .td {
        border: 1px solid black;
    }
    ...
    <td class=”td”>...</td>
    

    3. Одно и то же преобразование можно выполнять с помощью разных конвертеров. При этом результат будет существенно отличаться. Если у вас на выходе получится не очень красивый документ, попробуйте принудительно задать используемый модуль, например:

    $parameters = (new LowrapperParameters())
        ->setDocumentType(DocumentType::WRITER);
    

    4. Можно ли настроить ширину строк в таблице — для меня пока загадка. И в целом со стилизацией таблицы при преобразовании html в docx или pdf у меня возникли затруднения. Поэтому на мой взгляд подход трудно будет применять для генерации сложных печатных форм, таких как счет-фактура.

    Вывод


    Инструмент универсален и очень хорош, если на входе у вас не очень сложные по верстке документы. Тогда на выходе вы получаете документы в нужных форматах написав всего несколько строк кода.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 20

      +1
      Тоже недавно писал конвертацию DOCX в PDF, ни один вариант с использованием PHPOffice и PHP-конвертеров не дал приемлемого результата, кроме Libre Office. Причём я сначала хотел использовать unoconv, но он уверенно конвертировал файлы только из консоли (и у него под капотом тоже libreoffice), а из кода ни в какую.
        0

        Да, unoconv по сути аналогичная обертка, но на питоне. Я сперва начал использовать ее, даже написал модуль на php под нее, не найдя подходящего. И для laravel в том числе. Но потом понял, что в unoconv есть свои проблемы и автор его не очень активно поддерживает. Поэтому решил, что лучше сделать обертку поверх libreofffice. Если будут возникать какие-то трудности, то мне их легче будет быстренько пофиксить.

          0
          Unoconv — это небольшая python обертка над unoAPI LibreOffice(OpenOffice). По сути один файл, который можно расковырять и привести к приемлемому функционалу, заменив прием аргументов из нужного источника.
          Так же писал подобный микросервис для себя (из unoconv) и с лета собираюсь написать об этом :)
          +3
          что касается doc И excel — майкрософтовский офис сам их конвертит — достаточно сохранить HTML файлы с расширением docx или, соответственно, xslx

          Что касается граблей — нужно делать inline стили и верстать HTML одной таблицей иначе части документа формируются отдельно и расползаются
            0

            Раньше тоже так делал. Но при редактировании и последующем сохранении такого документа возникало сообщение, мол, переконвертировать ли документ в формат ворда (не помню точно формулировку). Свежий LibreOffice тоже спрашивает. А вот офис от Microsoft давно не использовал. Новые версии MS Office задают такой вопрос?

              +1
              последние не пробовал думаю да — обычный вопрос любой проги если она собирается пересохранить файл с другой структурой
              А вот либреофис как то некоректно открывал
              Точнее майкрософт сразу конвертил и открывал уже как родной а либре по моему открывал как HTML
              но в целом проблем не было даже с бухгалтерсикими отчетомаи типа главной книги и налоговыми накладными
              главное верстать внутри одного table
            +1
            Внедряли на проекте конвертертацию на основе LibreOffice. Нужно было сконвертить около 200000 doc-файлов в html. Процесс периодически зависал, приходилось ручками конвертор перезапускать.
              0

              На такой случай в обертке можно задать таймаут. По умолчанию конвертеру дается 60 секунд, после чего будет выброшено исключение.

              0

              Как на счет формирования документов в формате openoffice(odt, ods). Сами эти файлы представляют zip архив, в котором находятся xml файлы. Правите content.xml, запаковываете обратно в zip, переименовывываете архив обратно в odt или ods. После этого можно конвертировать в любой формат. Это сложнее, т.к. надо разбираться с xml. Зато все возможности, которых нет в html. А разметка в xml очень похожа на html

                0
                У меня была задача конвертировать html в PDF. К сожалению, так и не смог найти подходящий инструмент. Пытался использовать DomPDF, HTML2PDF. У них у всех одна болезнь не понимают даже среднюю верстку. А документ был как раз счет фактурой со сложными таблицами. Пришлось верстать с нуля с помощью fPDF(tPDF).
                Libre office не использовал.

                Вот вопрос. Как хром быстро конвертирует в PDF при печати? Нужно всего лишь выбрать PDF принтер и все, PDF готов, каким бы сложным не был документ.
                  0

                  По моей практике наиболее быстрым и при этом наиболее качественным оказался конвертер wkhtmltopdf. Я работал с ним через https://github.com/barryvdh/laravel-snappy

                    0
                    Посмотрите wkhtmltopdf — использует webkit для конвертирования html в PDF
                      0
                      https://github.com/arachnys/athenapdf вот хорошее решение, сохраняет как печать в PDF в хроме.
                      +1
                      Забыли добавить, что libreoffice для запуска требует установленного JRE
                        0
                        есть поддрежка css 3?
                          +1
                          В плане конвертации из Word/Exel есть еще docx4j. Заявляют, что умеют читать те же docx не хуже ворда, ну и конвертировать куда надо. Никто не пробовал? Если конвертирует не плохо, то можно и оболочки написать. LibreOffice вот не всегда понимает, например, математические формулы.
                            0

                            Спасибо, попробую как будет возможность. Обертка для PHP не помешает.

                            +1
                            Провозился со всеми конвертерами, которые в связке с PHPWord шли — mpdf, dompdf, tcpdf и ни один не смог даже самую простую таблицу отпечатать с отсутствующим border (opacity 0). С кирилицей у 2 из 3 конверторов жестокие проблемы — dompdf, tcpdf и чтобы всё взлетело надо доставлять шрифты, конфигурировать и то не факт, что всё взлетит. Из коробки кирилицу только mpdf подцепил и всё распечатал без ???
                            С изображениями тоже беда — достаточно в редакторе добавить свойство «Under text» изображению и ни один конвертер не отпечатает изображение, от слова совсем. При попытке позиционирования изображения в режиме absolute — +-1 пиксель куда-то не туда и всё, в генерируемом pdf нет изображения.

                            В итоге плюнул я на эту возню и решил, что лучше использовать libreoffice с его конвертером. Правда здесь у меня тоже юмор — рабочая машина mac, сервер — centos… надеюсь, что тестировать это будет не так тяжко, как возиться с кучей полурабочих библиотек.

                            За статью благодарность! За конвертер — отдельно. Хоть велосипеды писать не надо.
                              +1
                              Дополню свой же комментарий, потому что пришлось понаступать на грабли. Конвертил я docx в pdf и при запуске из консоли была ошибка LibreOffice с кодом 77. Пофиксил её вот так:

                              $converter = new Converter('soffice');
                              $params = (new LowrapperParameters())
                                          ->setInputFile($docxInvoice)
                                          ->setOutputFile($pdfInvoice)
                                          ->setOutputFormat(Format::TEXT_PDF);
                              $converter->addOption('-env:UserInstallation=file://'. sys_get_temp_dir(), 'libreoffice');
                              $converter->convert($params);
                              


                              На что обратить внимание:
                              $converter->addOption('-env:UserInstallation=file://'. sys_get_temp_dir(), 'libreoffice');
                              


                              Несмотря на --headless параметр запуска LibreOffice пользовательская конфигурация сохраняется LibreOffice в домашней директории. На сервере (да и у меня локально) пользователь, запускающий процесс LibreOffice не имет доступа к write домашней директории, поэтому, чтобы не давать лишних прав — указываем другое место сохранения настроек. После этого всё заработало как по маслу.
                                +1
                                Дополню свой комментарий, потому что я себе весь мозг вынес этим — проблема в том, что текущая реализация библиотеки (от автора поста, да и в целом имеющихся решений) не позволяет работать с несколькими параллельными (для поисковиков: multiple instances in command line libreoffice) инстансами soffice / libreoffice и, как в моём случае, одновременный запуск генерации двух счетов приводит к выбрасыванию исключения.

                                Как исправить?
                                Где я нашел решение: document-foundation-mail-archive.969070.n3.nabble.com/How-to-run-multiple-instances-of-Writer-simultaneously-tp4225101p4225390.html

                                Цитирую, чтобы текст никуда не исчез:

                                Just wanted to follow up on this thread. Running multiple Writers in the same (single) soffice.bin process seems to incur noticeable performance penalties. Therefore, in order to run several soffice.bin processes:

                                — Get a uuid string.
                                — Create random folder in /tmp/soffice-dir-uuid for the soffice.bin process.
                                — Optional as Matthew Francis suggested: copy a cached soffice folder into /tmp/soffice-dir-uuid (to improve startup performance).
                                — Run soffice --headless --accept=«pipe,name=soffice-pipe-uuid;urp;StarOffice.ServiceManager» -env:UserInstallation=file:///tmp/soffice-dir-uuid (Or call /path/to/soffice.bin directly.)


                                Пилим под себя и получаем вот такой итог (с использованием библиотеки от автора поста):

                                $converter->addOption('--nolockcheck');
                                $converter->addOption('--accept="pipe,name='. uniqid('libreoffice') .';urp;StarOffice.ServiceManager"');
                                $converter->addOption('-env:UserInstallation=file://'. sys_get_temp_dir() . uniqid('libreoffice'));
                                


                                Как предлагали на форуме (возможно в том же треде) — один из вариантов ускорения запуска libreoffice в --headless режиме, так это не создавать каждый раз новую структуру директории для флага UserInstallation, а создать один раз дефолтную и потом её только копировать во временную зону.

                                Хух. Надеюсь это реально кому-то поможет!

                            Only users with full accounts can post comments. Log in, please.