Программист с усами — сила.
А программист с Mustache
— хоть три подписи в штамп вставит
Moustache - англ. усы),
Mustache - библиотека
шаблонизации.
Представьте: вы — Заказчик, которому иногда требуется слегка поменять штампы PDF в системе. Каждый раз, когда нужно поменять отступ, изменить немного текст, добавить новый штамп или вывести вторую подпись, вы совершаете магический ритуал: зовете разработчика, ждете неделю, надеетесь, что он поймёт всё правильно с первого раза. А потом — перекомпиляция! сборка пакета! публикация! — сложный, запутанный процесс, доступный лишь избранным.
Но однажды Вы понимаете, что 2025 год уже наступил, а код для штампов живет по законам средних веков: HTML код штампа зашит в ресурсах, доступных штампов— ровно два, как глаза у человека (а если нужно три — извините, мутируйте?), вывод нескольких подписей на странице потребует навыков шамана-программиста.
И тогда вы задаетесь вопросом: «А что, если можно иначе? Просто… нажать пару кнопок?».
Спойлер: можно, более того - у некоторых из наших заказчиков это уже работает именно так.
Моим самым первым заданием, когда я пришел работать в СТАРКОВ Групп, было: реализовать штамповку “Согласовано юридическим отделом” в одном из регламентов. На старте задача показалась довольно простой: думал, что «штамповать PDF» — не сложнее чем «нажать кнопку и пойти пить кофе».
Но я только что закончил 815 курс, впервые столкнулся с Directum RX, Aspose, пытался понять “что за ЯКОРЬ ⚓?”. Да, получилось, я всё ещё работаю в СТАРКОВ Групп, но первые впечатления самые сильные, так что штамповка PDF теперь одна из моих любимых задач. После этого на других проектах требовались разные доработки - накладывать информацию о нескольких согласующих, выводить дополнительную информацию в штампе, накладывать штамп регистрации на уже подписанный документ еще до того, как это реализовали в коробке, и так далее. В общем по доработке штампов у меня приличный опыт на данный момент - начиная с RX 3.3 и заканчивая RX 4.12, так что когда у меня начнётся мания величия, попрошу меня называть “Мастер над штампами”.
Со временем появилось желание как-то более цивилизованно это делать, наработки постепенно накапливались. А теперь я покажу вам, как из хаоса родился... ну, не порядок, но хотя бы хаос с удобным интерфейсом. Говорят, идеальный код — это миф, как единорог. Но теперь мой код хотя бы не пугает новых джунов — они просто тихо плачут в уголке. Это уже прогресс!
Сразу предупреждение – в 4.12 вендор внёс много изменений в код штамповки, поэтому теперь есть две ветки кода, которые нужно перекрывать. Для упрощения кода, чтобы показать основную логику – используем только ветку со старым кодом штамповки, поэтому в таблице sungero_docflow_params строка с UseObsoletePdfConversion должна быть true.
Текущие возможности прикладной разработки – можно изменить логотип организации на штампе, текст заголовка и управлять показом даты и времени. Всё остальное уже не поддаётся настройке без привлечения разработчика.
По настройке позиционирования всё гораздо лучше: в данный момент в системе есть возможность указывать координаты штампа по якорям в тексте (⚓^) или по координатам (решения для государственных органов) или просто разместить его на странице мышкой (чудеснейшее дополнение от разработчиков Директум в версии 4.12, кстати, идею подал наш разработчик в статье — https://club.directum.ru/post/368757 ).
Всего в системе есть 2 варианта штампа ЭП – для ПЭП и УКЭП/УНЭП и один для штампа регистрации:
и
Текущие проблемы:
Сразу напрашиваются варианты развития:
У меня есть недостаток – если уж начал
что-то модифицировать, то сложно остановиться.
В итоге как обычно – получился универсальный
комбайн с вертикальным взлётом и встроенным
лунным посадочным модулем.
Моя душа (в прошлом) full-stack разработчика не могла согласиться с ключевой вещью – если HTML код штампа представляет из себя простой текст, зачем использовать настолько кондовые методы работы с текстом, когда есть заметно более гибкие методы, например взять и отрастить усы…
Суть шутки заключается в том, что есть язык разметки Mustache («усы» в переводе), который отлично работает с шаблонами и отлично умеет подставлять внешние переменные в код шаблона, что позволяет удобно работать с шаблонами в HTML.
При этом разработчики базового решения у вендора уже давно используют .Net реализацию – Nustache для работы шаблонизатора почтовых сообщений. Поэтому, чтобы не плодить сущности и зависимости – процесс выбора библиотеки для работы с шаблонами получился очень короткий – «та же самая, что в базовой разработке».
В случае с Mustache язык разметки позволяет:
Собственно, это все особенности языка разметки, дальше остаётся только настроить шаблоны и написать функции сборки необходимых переменных.
Конечно, хотелось бы сделать настройку переменных в таком виде:
Но эти компоненты не доступны прикладному разработчику, поэтому пойдём более простым путём (в лучших традициях бэкенда) – каждый документ будет заполнять коллекцию Dictionary<string,object> с необходимыми значениями переменных, она же годится и для вложенных списков, главное не передавать такое через Remote функции и работать строго внутри своего серверного слоя. При необходимости исходный код будет достаточной документацией :)
А дальше остаётся сделать все необходимые перекрытия и написать достаточно удобную обвязку.
Первый простой этап – боремся с ограничениями на настройки штампов. Для этого потребуется перекрыть справочник «Настройка отметок в PDF» (StampSetting). В качестве простого решения была просто добавлена коллекция «Якорь» - «Шаблон» для хранения любого необходимого количества штампов и якорей для них. Заодно не забываем добавить отключаемость всего этого, чтобы при необходимости легко перейти на коробочные (чекбокс - "Использовать расширенные штампы").
Да, если не хватает фантазии – откуда взять дополнительные «якоря» - https://www.compart.com/en/unicode/U+2693 и просто смотреть соседние.
Обойтись без нашего любимого перекрытия OfficialDocument никак нельзя. Для удобства дальнейшего развития была добавлена функция GetStampVariables, которая вычисляет по документу все необходимые свойства для передачи в шаблон штампа. При этом гибкость дальнейшего развития обеспечивается тем, что её можно перекрыть в любом из наследников и добавить недостающие переменные для шаблона.
/// <summary>
/// Возвращает переменные для использования в PDF штампах, добавлять переменные в наследниках.
/// </summary>
public virtual System.Collections.Generic.Dictionary<string, object> GetStampVariables(IStampSetting stampSettings)
{
var vars = new Dictionary<string, object>();
//Ключевая часть магии - шаблонизатор сам бегает по свойствам документа
vars["doc"] = _obj;
// Условная проверка - на факт регистрации {{#HasRegistered}} {{RegistrationDate}} {{/HasRegistered}}
if (_obj.RegistrationDate != null)
{
vars["IsRegistered"] = new Dictionary<string, object>()
{
{"RegistrationDate", _obj.RegistrationDate.Value.ToShortDateString()},
{"RegistrationNumber", _obj.RegistrationNumber
}
};
}
// Утверждающая подпись основного подписанта
var signatures = PdfStamp.Module.Docflow.PublicFunctions.Module.GetSignaturesForMark(_obj, _obj.LastVersion?.Id ?? 0, false);
if (signatures.Any())
{
var signatory = _obj.OurSignatory;
//подпись подписанта, может быть может быть по замещению
var signature = signatures.Where(x => x.SignatureType == SignatureType.Approval)
.Where(x => signatory != null && (signatory.Equals(x.SubstitutedUser) || signatory.Equals(x.Signatory)))
.OrderBy(x => x.SigningDate)
.FirstOrDefault();
if (signature != null)
{
var signatureStampParams = Sungero.Docflow.PublicFunctions.StampSetting.GetSignatureStampParams(stampSettings, signature.SigningDate, true);
var approveVars = PdfStamp.Module.Docflow.PublicFunctions.Module.GetSignatureMarkForCertificateAsDict(signature, stampSettings);
foreach(var key in approveVars.Keys)
vars[key] = approveVars[key];
signatures.Remove(signature);
}
}
#region Дополнительные согласующие подписи для множественной штамповки.
var approveSignatures = new List<Dictionary<string, string>>();
foreach(var signature in signatures)
{
var signatureStampParams = Sungero.Docflow.PublicFunctions.StampSetting.GetSignatureStampParams(stampSettings, signature.SigningDate, true);
var approveVars = PdfStamp.Module.Docflow.PublicFunctions.Module.GetSignatureMarkForCertificateAsDict(signature, stampSettings);
approveSignatures.Add(approveVars);
}
if (approveSignatures.Any())
vars["ApproveSignatures"] = approveSignatures;
#endregion
return vars;
}
Причем здесь же и скрывается вторая главная часть магии работы с шаблонами - в строке «vars["doc"] = _obj;» - благодаря этому шаблонизатор получает возможность обращаться самостоятельно к любым свойствам документа. Это просто фантастика – если не требуется дополнительных преобразований (отрезать миллисекунды из даты регистрации, например) – можно просто добавить тэг в шаблон и всё.
Еще одна полезная задача этой функции – собираем в удобный для использования вид информацию о подписях - утверждающей от автора/замещающего и всех остальных.
Первая часть - функция ConvertToPdfWithStampsCustom, которая получает на вход коллекцию с уже подготовленным кодом штампов и производит основную работу с использованием стандартных функций из изолированной области, в целом является почти полной копией функции из базовой разработки, но умеет в цикле перебирать якоря для штамповки.
Второй исключительно важный компонент – функция, которая делает из шаблона уже полноценный HTML, приведу её полностью, это ВСЯ магия, которая позволила появиться этой статье:
public virtual string RenderTemplate(string template, System.Collections.Generic.Dictionary<string, object> model)
{
if (string.IsNullOrEmpty(template) || model == null)
return string.Empty;
return Nustache.Core.Render.StringToString(template, model,
new Nustache.Core.RenderContextBehaviour() {
OnException = ex => Logger.Error(ex.Message, ex)
}).Trim();
}
Логика работы в данный момент очень простая – настройки HTML для всех якорей вынесены в отдельную коллекцию в настройках отметок PDF.
Каждый документ при вызове штамповки запускает функцию GetStampVariables, которая генерирует структуру с данными и заодно прикладывает ссылку на себя, так что внутри шаблона мы можем использовать любые свойства, связанные с документом.
Перебираем все якоря из настроек StampSetting, рендерим HTML код (RenderTemplate) для каждого якоря и отправляем в штамповку ConvertToPdfWithStampsCustom.
Пример HTML кода вывода обычного штампа ЭП (сокращено):
<html>
<body>
<div>{{{Logo}}} ЭЦП <b>{{{SignatoryFullName}}}</b></div>
<div>Дата подписания: <b>{{& SigningDateTimeUtc}}</b></div>
{{# Comment}}
<div>Комментарий <b>{{.}}</b></div>
{{/Comment}}
</body>
</html>
Здесь из интересного:
Более интересный пример с множественными штампами (тоже сильно упрощенный):
<html>
<body>
<div style="width: 500px">
{{#ApproveSignatures}}
<div style="float: left; margin: 2px">
ЭЦП <b>{{& SignatoryFullName}}</b> <br />
<span class="tg1">
Дата подписания: <b>{{& SigningDateTimeUtc}}</b>
</span>
</div>
{{/ApproveSignatures}}
</div>
</body>
</html>
Здесь из интересного:
Ну и интересный пример – штамп с адресатами.
В данный момент платформа нам не позволяет управлять отображением списка адресатов при создании документа из шаблона, давайте обойдём это ограничение штампом…
<html>
<body>
<div style="width: 300px; background-color: yellow">
Адресаты:
<ul>
{{# doc.Addressees }}
<li>
{{Correspondent}}
{{# Addressee}}({{.}}){{/ Addressee}}
</li>
{{/ doc.Addressees }}
</ul>
</div>
</body>
</html>
Здесь из интересного:
При необходимости теперь заказчик может настроить отображение штампа, добавив к нему название подразделения, нашей организации, ИНН, даты рождения подписанта и прочего – практически без обращения к разработчику. Более того, консультант, который прошел 816 курс – уже сможет посмотреть в среде разработки все необходимые имена свойств и собрать строку с необходимым именем самостоятельно после чтения страницы справки по Mustache.
Итоговый результат, которым я немного горжусь:
Ну и плюс моё личное идеологическое - вызывая штамповку из любого места - мы всегда получаем одинаковый результат, основанный на текущем состоянии документа, мне не нравятся хрупкие цепочки доштамповки, когда случайной перегенерацией PublicBody могут затираться штампы, наложенные где-то в середине сложной цепочки заданий и сценариев. А на боевых проектах еще и убираем проверку на обязательное наличие подписи - если сотрудник ХОЧЕТ сделать PDF из документа - он МОЖЕТ это сделать, но в текущем примере это не показано.
Что можно дальше с этим сделать:
Полный код решения можно подключить в свою среду разработки для версии RX 4.12 из нашего репозитория:
https://github.com/STARKOV-Group/PdfStampExample
UPD 2025-04-03:
Чтобы подключить решение в среде разработки для изучения - в файл _ConfigSettings.xml в каталоге среды разработки просто добавить:
<repository folderName="PdfStamps" solutionType="Base" url="https://github.com/STARKOV-Group/PdfStampExample.git"/> в раздел <block name="REPOSITORIES"> и перезапустить среду разработки. Можно использовать solutionType="Work", чтобы разработка оказалась на рабочем слое, но могут возникнуть конфликты с существующей разработкой, поэтому тут уже попрошу действовать аккуратней.
Ну или внести аналогичную правку в config.yml в DirectumLauncher, а потом запустить в cmd: do.bat dds config_up , в этом случае скрипты платформы самостоятельно обновят _ConfigSettings.xml
А это мультик, где я укладываюсь с редактированием штампа в 30 секунд. как и обещал в заголовке. Просьба не судить по общему виду штампов о красоте решения - просто типичная ситуация, когда бэкендер занимается фронтендом, да еще и без ТЗ (зато работает).
Авторизуйтесь, чтобы написать комментарий