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

Хочу поделиться с сообществом своими наработками по созданию библиотеки по генерации документов с дружелюбным к программисту интерфейсом. Для golang эта ниша является не менее важной, чем очередной веб-тулкит, так как наличие отчетов и инструментов по их генерации повышает привлекательность go для кровавого энтерпрайза. Создание отчета это многоступенчатый процесс. Инструменты по созданию отчетов могут автоматизировать разные этапы создания отчета, работу с БД, управление критериями фильтрации и подсчета значений, вывод в конечный документ. О последнем и поговорим. На данный момент результаты поискового запроса «golang odf» оставляют желать лучшего. Конечно, по запросу «golang pdf» все намного радужнее. Но исходя из собственного опыта разработки бизнес-приложений, которые должны порождать те или иные отчеты в виде офисных документов, могу с уверенностью сказать, что зачастую, после того, как красивый PDF прилетает на компьютер пользователя, он сверяет цифры, видит несовпадение, и звонит в саппорт с просьбой поправить цифру в полученном файле, потому что отчет нужен «уже вчера».Решением может служить генерация документа в формате Word/RTF/ODF/etc или же редактирование PDF (для этого есть готовые инструменты, так что это больше интересно админам, а не программистам). Так же оставим сторонникам проприетарных форматов возможность высказаться в комментариях, а я пока расскажу про генерацию ODF.

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

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

Почему нет? Простой язык, поддержка Google, живое сообщество, незанятые ниши. И потом, это просто весело. А возможность компиляции надежного кода в javascript позволит в некоторых случаях вообще перенести механизмы генерирования отчетов на клиент, что повысит гибкость вашего web-сервиса. К тому же грех не использовать быстрый нативный код для таких тяжелых вещей, как сложные многоуровневые отчеты. С форматом ODF я работаю довольно давно, с 2008 года, когда по работе потребовалось реализовать ODF-генератор отчетов и бланков. Тогда я реализовал компонент на более другом языке для удобного (как мне кажется) программного формирования документов. В целом, результат получился удовлетворительным, мой компонент работает до сих пор.После нескольких лет использования, как это обычно бывает, скопился ряд замечаний, которые я решил исправить кардинально, переписав всю библиотеку с нуля. Так как время сейчас интересное, выбрал экосистему Go для реализации творческого зуда. Но в целом, постарался сохранить интерфейсные решения предыдущей версии, как проверенные временем.

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

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

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

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

The element represents a paragraph, which is the basic unit of text in an OpenDocument file.

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

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

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

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

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

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

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 { //создадим пустую модель документа 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) //закроем файл defer output.Close () } } Более сложные примеры можно найти в файле odf_test.go, demo/report.go в репозитории проекта.Для полновесного примера был сформирован демо-отчет.Если Яндекс-диск перестанет отдавать файл 2075dc84ed7f445fb8b26876a6ae9f70.PNG В конце хотелось бы отметить, что само по себе формирование документа 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

© Habrahabr.ru