Привет, читатель! Хочу поделиться с тобой наболевшим опытом, надеюсь, будет полезен. Сегодня расскажу о том, как разрабатывали систему печати документов в корпоративной системе.
С чего все начиналось
А началось все с разработки ERP-платформы в одной торговой компании примерно 2 года назад. Был выбран Linux, стек С++/Qt, PostgreSql и фронт под web. На C++/Qt был реализован сервер приложений и там же, через прослойку JS интерпретатора писалась бизнес логика. Почему так — это отдельная история, здесь рассмотрим, как разрабатывалась система печати.
Первые пробы пера
HTML
Изначально, пока все документы печатались из 1С (используем связку 1С + своя «ERP» для управленческого учета в компании) все было хорошо, а в планах был переход на свою систему. Тут то и понадобилось прикручивать печать к системе.
Первая идея была верстать форму html, программно заполнять данные, пользователю html-ку в браузер и пусть оттуда печатает.
Сразу выяснились некоторые нюансы:
- Под все документы придется с 0 верстать шаблон
- Конечный пользователь не в состоянии подредактировать html
- Менеджеры вели некоторую аналитику в excel
- Проблемы с переносом строк при печати многостраничных файлов
В итоге был сделан только один шаблон для печати заявки в логистику (одностраничная форма) которым до сих пор успешно пользуются
XLSX
Вторая идея была работать с XLSX документами. Гугл быстро подсказал про библиотеку QtXlsxWriter. Были еще варианты, но в итоге остановились на QtXlsxWriter.
Что умела библиотека:
- Открывать/Создавать xlsx и считывать значение ячеек
- Изменять значение ячеек, сохранять файл
- Работа с форматом ячейки (в том числе обрамление)
- Высота/ширина строк/столбцов
- Объединение ячеек
- Группировка строк/столбцов
- Вставка изображений
Это позволило сразу скоммуниздить взять шаблоны документов в xlsx формате из других систем (Привет 1С), заполнить нужными данными и статикой отдать пользователю xlsx файл, с которым он уже может работать как его душе угодно.
Сразу же появились подводные камни, QtXlsxWriter «некачественно» обрабатывала загрузку документа из шаблона, терялись форматы ячеек (wrap/aling и т.д.). После часов копания «xlsx формата (если кто не знает, xlsx представляет собой zip архив с набором xml документов) выяснили что, в разных версиях boolean атрибуты в xml файлах сохраняются по-разному, либо
<t a="0" b="1" />
либо
<t a="true" b="false" />
а QtXlsxWriter местами парсил только 1/0, местами только true/false, а местами и то и то. Но ничего, руки есть, пофиксили.
Еще был неприятный момент, после формирования xlsx файла через QtXlsxWriter, если его открыть в MS Office, тот начинал ругаться.
В книге „test1.xlsx“ было обнаружено содержимое, которое не удалось прочитать. Попробовать восстановить содержимое книги? Если вы доверяете источнику ...
При этом, после открытия, визуально, все данные были на месте. При этом многие рядовые пользователи (наши клиенты) могли пугаться такого сообщения при открытии наших прайс листов.
После многих часов втыкания в xml файлы MS Office и QtXlsxWriter и поиска того не знаю чего, что MS не нравилось, был придуман костыль. Если взять файл, сгенерированный QtXlsxWriter, и обработать его с помощью LibreOffice получается валидный xlsx файл, со всеми данными первоначального, но на него не ругается MS Office:
libreoffice --headless --invisible --quickstart --convert-to xlsx test.xlsx --outdir valid_xlsx
И жить стало хорошо, под нужные отчеты писался небольшой код по формированию xlsx документа, выгружался пользователям, они с ним работали, если надо печатали и MS Office их больше не пугал. Даже смогли реализовать выгрузку OLAP отчетов в xlsx с группировками и куртизанками.
Автоматизация
Компания росла, клиентов больше, документов еще больше ( заявки, реализации, накладные и т.д. ), печать стала отнимать очень много рабочего времени. При этом часть документов печаталась из 1С часть из нашей системы. Решили это дело как-то автоматизировать. До этого (лет 5-7 назад) был опыт печати через Windows OLE контейнеры (создавался контейнер с Excel, открывался файл, задавались настройки печати и отправлялся на печать), но с этим не очень хотелось связываться, да и платформа крутится на Linux и тащить сюда виндовый модуль не хотелось (хотя как крайний вариант рассматривали принт-сервер на винде).
Все в PDF
В Linux есть CUPS и с этим вроде как все хорошо, командой lpr можно легко отправить на печать pdf файл. Вот только pdf генерировать мы не умеем. Решение было найдено быстро.
libreoffice --convert-to pdf 1.xlsx --headless
Но не все так просто оказалось. Файлы конвертировались со 100% масштабом и ни как не подгонялись к размеру страниц (А4/А3, книжная/альбомная, отступы), точнее все подгонялось по стандартным параметрам (А4, книжная). Выяснилось, что если задать эти настрой через LibreOffice(руками открыть LibreOffice Calc), сохранить в xlsx и конвертировать через libreoffice --convert-to pdf, все работало почти отлично.
- Отступы и настройки страницы обрабатывались корректно.
- Если надо было подгонять по масштабу, то этот параметр игнорировался и конвертировало с масштабом 100%.
- Если стояли настройки подгонять по размеру/ числу страниц, все работало
По поводу пункта 2 отписался в поддержку LibreOffice, жду от них ответа.
Благо пункт 3 работает правильно, решили отталкиваться от него. Теперь надо научить QtXlsxWriter работать с настройками страницы. Расковыряв xml файлы в xlsx документах нашли места, отвечающие за это дело
xl/worksheets/sheet1.xml
<worksheet>
<sheetPr filterMode="false">
<pageSetUpPr fitToPage="false"/>
</sheetPr>
...
...
<pageMargins
left="0.7875"
right="0.7875"
top="1.05277777777778"
bottom="1.05277777777778"
header="0.7875"
footer="0.7875"/>
<pageSetup
paperSize="9"
scale="50"
firstPageNumber="0"
fitToWidth="1"
fitToHeight="1"
pageOrder="downThenOver"
orientation="portrait"
usePrinterDefaults="false"
blackAndWhite="false"
draft="false"
cellComments="none"
useFirstPageNumber="false"
horizontalDpi="300"
verticalDpi="300"
copies="1"/>
...
</worksheet>
Что здесь есть интересного:
pageMargins — думаю с этим все понятно
fitToPage — подгонять под размеры/кол-во страниц или использовать масштаб
fitToWidth — кол-во страниц по ширине
fitToHeight — кол-во страниц по высоте
scale — масштаб в %
paperSize — размер листа (9=А4)
orientation — книжная/альбомная
Добавили работу с этими параметрами в QtXlsxWriter. Осталось только сформировать xlsx документ с отступами в нужных местах, чтобы не печатались куски незавершенного контента на разных листах. С этим не совсем все просто оказалось.
Печать
Рассмотрим ситуацию когда печатаем маршрутный лист на листе А4 книжной ориентации, без отступов.
При этом необходимо чтоб по ширине документ вмещался в 1 страницу. Ставим настройки:
fitToPage=false
fitToWidth=1
fitToHeight=100
pageMargins — все по 0
При таких условиях fitToHeight должно быть заведомо больше числа предполагаемых страниц при печати.
Маршрутный лист представляет собой заголовок с указанием маршрута и список клиентов с доп. информацией, по которым будет производиться доставка.
Если оставить все как есть, велика вероятность что часть блока с информацией о клиенте, попадающие на конец листа будут разбиваться, часть будет в конце первого и часть в начале второго, а это неприемлемо для нас.
В итоги родился следующий подход (возможно костыль).
Нам изначально известен размер листа А4:
Ширина 21 см
Высота 29.7 см
И мы знаем, что наш контент будет подогнан под ширину листа, т.о. можно посчитать относительную степень сжатия контента:
scale = ширина листа / ширину контента
Тут нас ждал сюрприз, чтоб посчитать ширину контента, необходимо сложить ширину всех столбцов, это сделать не сложно
double QXlsx::Document::columnWidth(int column);
Вот только было совершенно непонятно, в каких единицах измерения получается результат. Возможно, правильное решение можно найти тут, но не смогли, в итоги эмпирическим путем было найдено магическое число 5.10238
1 см = 5.10238 е.и.ш.к. (единица измерения ширины колонки)
scale = А4_ширина * 5.10238 / sum(columnWidth)
далее посчитаем размер контента, который мы способны вмести по ширине листа
height=А4_высота * 28.3464567 / scale
Появилось еще одно магическое число, как Вы уже догадались, это для перевода высоты строки в из см. в е.и.в.с (единица измерения высоты строки, на просторах интернета нашел такую информацию „r.Height = ht * 28.3464567 // Convert CM to postscript points“ )
Высоту строки можно найти через:
double QXlsx::Document::rowHeight(int column);
Используя параметр height, мы забиваем контент в xlsx файл пока высота контента <=height. Если при добавлении нового блока Б, мы выходим за границы height, то перед Б вставляем пустую строчку необходимой высоты, чтобы блок Б печатался с новой строки. Высоту пустой строчки можно посчитать зная высоту контента( sum (rowHeight) ) вставленного до блока Б.
Не рассматриваю тут расчет разбивки по страницам с использованием отступов (pageMargins), скажу лишь что в xml данных хранятся значения этих переменных в дюймах (1 дюйм = 2.54 см ).
Таким образом, получается xlsx файл с готовыми настройками и разбивкой по строка для печати. Далее с помощью libreoffice --convert-to pdf конвертируем в pdf и наш документ готов к печати.
Осталось напечатать:
lpr -pFS-4300DN test.pdf
Сейчас делаем автоматизацию печати на МФУ с финишером (степлирование). Уже немного поиграли с тестовым аппаратом и под Linux, оказалось все просто для степлирования.
Печать со степлированием c одной скрепкой в левом верхнем углу:
lpr -P printer_name -o StapleLocation="UpperLeft" order.pdf
Конец
На этом все. Буду рад узнать другие подходы к реализации этой задачи.
Спасибо за внимание )
Автор: alexander_8901