Pull to refresh

Генератор документов Open Document (ODF) на Go

Reading time 7 min
Views 13K
Хочу поделиться с сообществом своими наработками по созданию библиотеки по генерации документов с дружелюбным к программисту интерфейсом. Для golang эта ниша является не менее важной, чем очередной веб-тулкит, так как наличие отчетов и инструментов по их генерации повышает привлекательность go для кровавого энтерпрайза.

Создание отчета это многоступенчатый процесс. Инструменты по созданию отчетов могут автоматизировать разные этапы создания отчета, работу с БД, управление критериями фильтрации и подсчета значений, вывод в конечный документ. О последнем и поговорим.

Введение


На данный момент результаты поискового запроса «golang odf» оставляют желать лучшего. Конечно, по запросу «golang pdf» все намного радужнее. Но исходя из собственного опыта разработки бизнес-приложений, которые должны порождать те или иные отчеты в виде офисных документов, могу с уверенностью сказать, что зачастую, после того, как красивый PDF прилетает на компьютер пользователя, он сверяет цифры, видит несовпадение, и звонит в саппорт с просьбой поправить цифру в полученном файле, потому что отчет нужен «уже вчера».

Решением может служить генерация документа в формате Word/RTF/ODF/etc или же редактирование PDF (для этого есть готовые инструменты, так что это больше интересно админам, а не программистам). Так же оставим сторонникам проприетарных форматов возможность высказаться в комментариях, а я пока расскажу про генерацию ODF.

Формат ODF


Формат Open Document является открытым форматом редактируемых офисных документов. Из известных офисных пакетов, которые его поддерживают можно выделить OpenOffice и LibreOffice. Сейчас действует стандарт на формат версии 1.2, но от версии 1.0 отличий немного, и они в основном организационного порядка. Учитывая, что стандарту уже несколько лет, его можно считать стабильным и опираться на него в своей работе. Формат предусматривает различные типы документов, текстовые документы, электронные таблицы, презентации и т.д. Приоритетными форматами для хранения отчетов являются, по моему опыту, текстовые (odt) и табличные (ods) документы.

Стандарт описывает правила структурирования модели документа и сохранения этой модели в формат XML. Технически, файл документа должен представлять собой либо один большой XML, либо zip-архив с несколькими обязательными файлами и неограниченным числом других файлов. Такой формат удобен для встраивания изображений и прочих файлов, поэтому я буду рассматривать только документы в формате zip-архива.

Также не будет лишним отметить, что ODF является государственным стандартом в РФ. И хотя Microsoft Office крепко удерживает позиции в организациях, Open Office нередко ставится рядом и служит запасным аэродромом.

Почему Go?


Почему нет? Простой язык, поддержка Google, живое сообщество, незанятые ниши. И потом, это просто весело. А возможность компиляции надежного кода в javascript позволит в некоторых случаях вообще перенести механизмы генерирования отчетов на клиент, что повысит гибкость вашего web-сервиса. К тому же грех не использовать быстрый нативный код для таких тяжелых вещей, как сложные многоуровневые отчеты.

Библиотека


С форматом ODF я работаю довольно давно, с 2008 года, когда по работе потребовалось реализовать ODF-генератор отчетов и бланков. Тогда я реализовал компонент на более другом языке для удобного (как мне кажется) программного формирования документов. В целом, результат получился удовлетворительным, мой компонент работает до сих пор.

После нескольких лет использования, как это обычно бывает, скопился ряд замечаний, которые я решил исправить кардинально, переписав всю библиотеку с нуля. Так как время сейчас интересное, выбрал экосистему Go для реализации творческого зуда. Но в целом, постарался сохранить интерфейсные решения предыдущей версии, как проверенные временем.

Далее я буду рассказывать более специфичные вещи про формат. Для ознакомления с ним можно прочитать введение в стандарт ODF.
В чем заключается основная трудность при работе с ODF с точки зрения программиста? Она заключается в том, что модификация видимого содержимого документа неявно приводит к модификации нескольких областей модели документа. Изменения затрагивают область содержимого, область описания стилей, содержимое zip-пакета. При этом из-за особенностей XML в формат ODF были введены некоторые средства по борьбе с невидимыми символами, что накладывает дополнительные обязанности на инструмент генерации.

Еще один важный момент это повторное использование стилей. Компонент должен был следить за стилями, которые формирует пользователь и сохранять их наиболее оптимально, без дублирования.

Реализация


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

Для модели документа в прежней версии использовался чистый DOM, который был избыточен, поэтому в новой версии используется упрощенная структура данных подобная DOM, которая затем транслируется в XML полуручным маршаллингом из стандартной библиотеки Go. Для работы с моделью документа применяется паттерн Carrier-Rider-Mapper (CRM). Carrier это носитель данных, в данном случае дерево узлов. Для прохода по дереву и его модификации используются Rider'ы — бегунки, особенность заключается в том, что в деревянной структуре данных бегунок становится на позицию узла, а бегает по списку потомков и по атрибутам этого узла. Mapper в данной схеме это высокоуровневый механизм, который с помощью бегунков работает с моделью документа, как не трудно догадаться, в нашей схеме это Форматтер.

Само содержимое документа записывается в виде узлов с особым именем из нужного пространства имен. Атрибуты узлов также принадлежат к особым пространствам имен. Текстовый редактор при открытии интерпретирует наборы узлов и атрибутов в отображения красивого текста и ровных табличек. Поэтому основная задача форматтера заключается в том, чтобы правильно и в нужном месте записывать узел и его аттрибуты, название и значение которых описано в стандарте. Например:
The <text:p> element represents a paragraph, which is the basic unit of text in an OpenDocument file.
описывается элемент параграф, содержимое которого будет отображено как параграф, к которому будет применен стиль параграфа: выравнивание на странице, межстрочный интервал и т.д.

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

Для повышения расширяемости изначально монолитный форматтер был разделен на несколько более узкозаточенных, которые выполняют функции записи тех или иных разделов документа (узлов дерева). ParaMapper занимается записью содержимого параграфов, TableMapper занимается записью таблиц и их содержимого, при этом записью текста в ячейки занимается ParaMapper. Такой подход позволяет реализовывать нужные функции огромного стандарта точечными усилиями, экономя время и ресурсы проекта.

Атрибуты текста, текст символов, выравнивание параграфа на странице и другие необходимые атрибуты устанавливаются с помощью обобщенного механизма формирования атрибутов.
Для того или иного семейства атрибутов реализован специальный билдер, который позволит задать нужный стиль.
Важной особенностью работы является то, что задание атрибутов еще не значит их фактическую запись в документ. Практически, это приводит к такой схеме работы:
  • Приготовили данные для записи
  • Установили атрибуты будущего содержимого
  • Записали содержимое, которое получает установленные атрибуты
  • Атрибуты записаны в документ

Пока не запишем новые атрибуты или не сбросим их, каждое очередное содержимое будет атрибутироваться этими атрибутами. Дополнительная возможность установки дефолтных атрибутов документа, которыми будет атрибутировано любое содержимое, которому не назначены особые пользовательские атрибуты.

Так как стандарт ODF довольно объемный, в своей работе я реализовал только минимум возможностей, которые могут понадобиться для формирования отчетов. Среди таких возможностей: атрибуты текста (цвет, шрифт, размер), атрибуты параграфа (выравнивание), таблицы и атрибуты ячеек (атрибуты границы, цвет, толщина линии), встраивание изображения.

Основные усилия пришлись на формирование расширяемого каркаса, которое позволяет реализовать нужные атрибуты или даже новые элементы в несколько шагов, даже без модификации кода библиотеки (писать новый код все же придется). Что интересно, формат электронных таблиц для программиста выглядит почти так же, как формат текстового документа. Меняется лишь mimetype и тот факт, что корневым элементом документа не является текст. Механизм записи таблиц работает одинаково в обоих случаях. Это можно увидеть в примерах из модуля odf_test.go.

Пример




Простой пример для демонстрации работоспособности и способа использования.
package main

import (
    "odf/generators"
    "odf/mappers"
    "odf/model"
    _ "odf/model/stub" //не забываем загрузить код скрытой реализации
    "odf/xmlns"
    "os"
)

func main() {
    if output, err := os.Create("demo2.odf"); err == nil {
        //закроем файл потом 
        defer output.Close()
        //создадим пустую модель документа
        m := model.ModelFactory()
        //создадим форматтер
        fm := &mappers.Formatter{}
        //присоединим форматтер к документу
        fm.ConnectTo(m)
        //установим тип документа, в данном случае это текстовый документ
        fm.MimeType = xmlns.MimeText
        //инициализируем внутренние структуры
        fm.Init()
        //запишем простую строку
        fm.WriteString("Hello, Habrahabr!")
        //сохраним файл 
        generators.GeneratePackage(m, nil, output, fm.MimeType)
    }
}

Более сложные примеры можно найти в файле odf_test.go, demo/report.go в репозитории проекта.
Для полновесного примера был сформирован демо-отчет.
Если Яндекс-диск перестанет отдавать файл


Заключение


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

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

Если вы пишете на Go и вам нужны отчеты, то вам сюда. Дух opensource и коллективной разработки может вывести библиотеку в обширную нишу. А еще из ODF можно получить PDF в пакетном режиме, что может заметно усилить возможности по редактированию циферок и подбиванию отчетов.

Ссылки


Описание формата ODF: docs.oasis-open.org/office/v1.2/OpenDocument-v1.2.html
Репозиторий проекта: github.com/kpmy/odf
Демо: yadi.sk/i/RghkBDHIgcey2
Tags:
Hubs:
+14
Comments 6
Comments Comments 6

Articles