Программист с усами — сила.
А программист с 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 секунд. как и обещал в заголовке. Просьба не судить по общему виду штампов о красоте решения - просто типичная ситуация, когда бэкендер занимается фронтендом, да еще и без ТЗ (зато работает).
Если возникнут вопросы, можно задать их в чате разработчиков, так будет сильно проще
https://t.me/DRXDevChat/1
Переносится ли штамп (особенно если он из множественный на следующую страницу если он не влазит?
Сдвигается ли содержимое документа при наложении штампа чтобы штамп не накладывался на данные?
Станислав, пока нет, самым простым будет сделать оценку габаритов штампа и влезет ли на страницу, а дальше скорей всего проще будет добавить новую страницу только для штампа
Авторизуйтесь, чтобы написать комментарий