От идеи до воплощения: опыт создания библиотеки водяных знаков
Олег Чебан, rлавный инженер-программист отделения разработки информационных систем компании «Форс – Центр разработки» (ГК Форс)
В любой компании есть документы, которые должны оставаться внутри — регламенты, отчёты, внутренние исследования. Доступ к ним можно ограничить через роли и права, но есть один момент: как только файл скачан, контроль теряется. Его можно переслать по почте, отправить в мессенджер или просто скопировать на флешку.
Мы столкнулись с этим на практике. Формально всё было защищено, но утечка могла произойти уже после того, как сотрудник получил доступ. Стало ясно, что нужен инструмент, который помечает документ специальным образом и позволяет легко определить его источник.
Так появилась идея многоуровневых водяных знаков и отдельной библиотеки, которую можно подключить к любому сервису и настроить под разные форматы файлов.
Многоуровневая система водяных знаков
Чтобы решить проблему потери контроля над файлами после скачивания, мы по инициативе нашего заказчика внедрили систему цифровых водяных знаков, интегрированную с корпоративными сервисами напрямую.
Было сделано три уровня маркировки, которые работают в связке и решают разные задачи:
Основной водяной знак показывает, что документ принадлежит компании и имеет конфиденциальный статус.
Метаданные скачиваний представляет собой особый тип водяного знака, фиксирующий временные метки и идентификационные данные пользователей, выгружавших документ из системы. Это даёт возможность быстро установить источник в случае утечки.
Метаданные пользовательской активности несут дополнительную информацию в зависимости от среды, где скачивали документ: IP-адрес, тип клиента, операционная система и другие параметры для расширенной аналитики.
Технические вызовы реализации
В данном проекте файловое хранилище содержало около миллиона файлов различных форматов, где доминировали изображения и PDF-документы.
Многообразие PDF-форматов стало первым серьезным испытанием. В архиве присутствовали документы различных версий стандарта — от устаревшего PDF 1.0 до современного PDF 2.0, включая промежуточные версии 1.4, 1.7 и другие. Особую сложность представляли многостраничные PDF-документы большого объема — некоторые из них содержали десятки страниц. Обработка таких файлов требовала оптимизации алгоритмов и применения решений с поддержкой многопоточности.
Проблема защищенных документов добавила еще один уровень сложности. Часть PDF-файлов имела настройки безопасности (флаг isEncrypted), что делало невозможным прямое внедрение водяных знаков без предварительной обработки.
Адаптивное масштабирование для изображений потребовало разработки функции определения оптимального размера водяного знака. Файлы имели различные размеры — от небольших изображений до файлов большого объема, что требовало динамической адаптации размера маркировки.
Наконец, требование заказчика о стойкости водяных знаков к удалению (большинство PDF редакторов позволяют удалить слой за считанные секунды) исключило использование стандартного overlay-режима наложения. Потребовалась разработка нового метода наложения, что значительно усложнило техническую реализацию.
Принципы и подходы к разработке
Анализ технических требований и необходимость интеграции с существующей корпоративной инфраструктурой привели нас к выводу о необходимости создания специализированной Java-библиотеки.
Принцип легковесности стал ключевым при проектировании такой библиотеки. Разработанная библиотека представляет собой самодостаточный модуль с минимальными зависимостями. Это позволило интегрировать функциональность водяных знаков в любой корпоративный сервис без существенных изменений архитектуры.
Особое внимание было уделено удобству использования для разработчиков. Конфигурация водяных знаков включает множество параметров: размер, цвет, прозрачность, позиционирование, тип знака (текст или изображение), способ наложения, количество меток на странице, специальные условия применения и другие настройки. Управление таким количеством параметров через стандартные API привело бы к созданию громоздкого и сложного в использовании интерфейса.
Для решения этого вопроса команда Форс создала Domain Specific Language (DSL) — специализированный язык, который позволяет описывать сложные сценарии нанесения водяных знаков в декларативном стиле.
Вместо написания многочисленных строк процедурного кода пользователи могут использовать интуитивно понятные конструкции, описывающие желаемый результат.
При выборе подхода к реализации DSL рассматривались два основных варианта: внутренний DSL, использующий возможности основного языка программирования для создания предметно-ориентированного синтаксиса, и внешний DSL с собственным синтаксисом и парсером. Решение было принято в пользу внутреннего DSL на языке Java, что обеспечило лучшую интеграцию с существующей кодовой базой и упростило процесс внедрения.
Подробнее о DSL
Языки программирования можно разделить на 2 типа: универсальные языки (general-purpose programming language) и предметно-ориентированные (domain-specific language). Популярные примеры DSL это SQL или регулярные выражения. Язык уменьшает объем функциональности, который он дает, при этом он способен эффективно решать определенную задачу. Вместо детального описания алгоритма ("как делать") разработчик фокусируется на конечной цели ("что получить"). Это принципиально упрощает процесс разработки и делает код более читаемым для коллег, не знакомых с техническими деталями реализации.
Внутренний vs внешний DSL: ключевые различия
Внутренний DSL представляет собой специализированный API или набор конструкций, построенных на базе существующего языка программирования. Он использует синтаксические возможности host-языка для создания более выразительного и предметно-ориентированного интерфейса:
- Преимущества: не требует создания отдельного парсера, полная интеграция с IDE (подсветка синтаксиса, автодополнение, отладка), возможность использования всех возможностей базового языка;
- Недостатки: ограничен синтаксисом host-языка, может выглядеть менее естественно;
- Примеры: Gradle (на основе Groovy), JUnit assertions, библиотеки для работы с JSON.
Внешний DSL — это полноценный язык с собственным синтаксисом, грамматикой и парсером:
- Преимущества: полная свобода в проектировании синтаксиса, максимальная выразительность для конкретной предметной области;
- Недостатки: необходимость разработки парсера и инструментария, сложность интеграции с существующими системами, дополнительные затраты на поддержку;
- Примеры: SQL, CSS, конфигурационные языки типа YAML.
Простое и элегантное добавление водяных знаков
Разработанный нами DSL позволяет создавать сложные конфигурации водяных знаков с помощью интуитивно понятных цепочек методов. В примере
происходит добавление текстового и графического водяных знаков с указанием их размеров, прозрачности, позиционирования, цвета и поворотов. Декларативный стиль скрывает сложность внутренних процессов, позволяя разработчику сосредоточиться на бизнес-логике, а не на технических деталях.
Низкий порог входа — одна из важных особенностей. Библиотекой может успешно пользоваться любой разработчик с базовыми знаниями программирования, поскольку глубокого понимания специфики обработки графических форматов или тонкостей PDF-структур не требуется.
Система условного применения водяных знаков
Одной из особенностей библиотеки стала система условного применения водяных знаков, позволяющая создавать бизнес-правила для их размещения. Эта функциональность особенно важна в корпоративной среде, где различные документы требуют дифференцированного подхода к защите
Селективная обработка страниц позволяет применять водяные знаки только к определенным частям документа. Система поддерживает гибкие фильтры на основе номеров страниц — от простого пропуска первой страницы, часто содержащей только титульную информацию, до сложных правил обработки четных или нечетных страниц. Все эти условия могут комбинироваться в рамках единой конфигурации, создавая сложные сценарии применения и сохраняя при этом простоту и читаемость кода.
Основные концепции дизайна библиотеки
Технологический стек был сформирован с учетом требований корпоративной экосистемы и необходимости обеспечения широкой совместимости. Выбор версии Java определялся стремлением к максимальной совместимости с существующими корпоративными сервисами, работающими на различных версиях Java. Это решение гарантирует беспроблемную интеграцию библиотеки, в том числе с legacy-системами.
Для расширения возможностей разработки и компенсации ограничений выбранной версии Java команда внедрила гибридный подход с использованием Kotlin. Этот современный JVM-совместимый язык был задействован для реализации data-классов, написания тестов и создания вспомогательных компонентов. Лаконичный синтаксис Kotlin, продвинутые возможности системы типов и встроенная поддержка null-safety позволили создать более чистый и maintainable код, сократив при этом время разработки и повысив читаемость решения.
Принцип Open-Closed (OCP) стал основополагающим при проектировании архитектуры расширяемости. Для его реализации была применена комбинация встроенного механизма Java ServiceLoader и паттерна Service Locator. Такой подход позволяет легко расширять или переопределять поведение библиотеки без модификации основного кода, что особенно важно для адаптации корпоративных сервисов под различные специфические требования. Система приоритезации через интерфейс Prioritizable
обеспечивает автоматический выбор наиболее подходящей из доступных альтернатив реализации сервиса, позволяя разработчикам создавать специализированные версии компонентов без нарушения существующей функциональности.
Реализация DSL базируется на паттерне Fluent Builder. Этот паттерн позволил создать цепочки методов, максимально приближенных к естественному языку описания конфигурации водяных знаков, что значительно упрощает процесс разработки и сопровождения кода.
Паттерн Fluent Builder представляет собой гибкое решение для создания сложных объектов через интуитивно понятный интерфейс. Основная идея заключается в построении цепочки методов (однако это нечто большее, чем простой method chaining), которая читается практически как обычное предложение на естественном языке.
Ключевые преимущества реализации:
- каждый метод возвращает объект с соответствующими доступными операциями, обеспечивая типобезопасность на этапе компиляции;
- благодаря контролю последовательности вызовов исключается возможность пропуска обязательных параметров;
- автодополнение IDE помогает разработчику выбрать следующий логический шаг в построении конфигурации.
Для работы с PDF-документами был выбран Apache PDFBox — зрелое open-source решение с богатой функциональностью и активной поддержкой сообщества. Библиотека обеспечивает широкие возможности по манипулированию PDF-структурами и демонстрирует высокую надежность в задачах промышленного масштаба. Среди недостатков следует отметить частичную поддержку многопоточности. При реализации многопоточной обработки потребовались дополнительные архитектурные решения для обеспечения полной потокобезопасности, что было успешно реализовано посредством изоляции операций на уровне страниц документа.
Техническая реализация обработки изображений
Для работы с графическими файлами была выбрана библиотека JAI ImageIO — расширение стандартного Java API для работы с изображениями, обеспечивающее поддержку широкого спектра графических форматов и продвинутые возможности обработки.
Алгоритм нанесения водяных знаков построен по принципу многоэтапной композиции с использованием Graphics2D API — мощного инструмента для двумерной графики в Java. Библиотека поддерживает создание двух типов водяных знаков: текстовые и графические, каждый из которых обрабатывается специализированными компонентами.
Архитектура обработки включает несколько ключевых модулей:
Модуль конвертации отвечает за преобразование входящих данных в стандартный формат BufferedImage, обеспечивая единообразную работу с различными типами изображений — от байтовых массивов до файлов на диске.
Система позиционирования автоматически рассчитывает координаты размещения водяного знака в зависимости от выбранной стратегии
: единичное размещение в определенной позиции (угол, центр, край) или мозаичное заполнение (tiling) — равномерное распределение водяного знака по всей площади изображения с заданным интервалом. Алгоритм учитывает размеры исходного изображения и водяного знака для обеспечения оптимального визуального баланса.
Модуль рендеринга применяет продвинутые техники графической обработки: настройка альфа-канала для контроля прозрачности, динамическое масштабирование шрифтов на основе размеров изображения, поддержка поворота элементов с сохранением качества изображения.
Особенностью реализации является управление трансформациями: система сохраняет исходное состояние графического контекста перед каждым изменением и восстанавливает его после завершения операции, что критически важно при нанесении множественных водяных знаков. Это предотвращает накопление нежелательных эффектов.
Дополнительные возможности включают автоматическое добавление символов товарного знака (для текстовых водяных знаков) и тонкую настройку композитных режимов наложения для достижения требуемого визуального эффекта при различных типах подложки.
Обеспечение стойкости водяных знаков в PDF
Одним из ключевых требований заказчика было обеспечение стойкости водяных знаков и недопущение возможности их простого удаления из PDF-документов. Стандартные методы наложения легко обходятся базовыми инструментами редактирования.
Библиотека предоставляет гибкий выбор режимов защиты в зависимости от требований к конкретному документу. Помимо традиционного метода overlay-наложения, доступного для случаев, где важна сохранность текстового слоя и компактность файла, была разработана технология глубокой интеграции водяных знаков на уровне растрового представления для максимальной защиты.
Принцип работы усиленного режима кардинально отличается от традиционных подходов: вместо добавления водяного знака как отдельного слоя система выполняет деструктивное преобразование содержимого страницы.
Процесс включает несколько критических этапов. Сначала каждая страница PDF-документа преобразуется в высококачественное растровое изображение с настраиваемым разрешением (по умолчанию 300 DPI). Затем к полученному изображению применяется алгоритм нанесения водяных знаков, использующий те же принципы, что и для обычных графических файлов.
Ключевая особенность метода заключается в полной замене исходного содержимого страницы. Система удаляет все исходные векторные объекты, текстовые слои и графические элементы, заменяя их единым растровым изображением с уже интегрированными водяными знаками. Такой подход делает извлечение или удаление защитных меток технически сложным без серьезной деградации качества документа.
Несмотря на то, что данный метод приводит к увеличению размера файлов и потере возможности поиска по тексту, он обеспечивает хороший уровень защиты для критически важных корпоративных документов.
Выбор оптимального режима зависит от баланса между уровнем защиты и функциональными требованиями: overlay-режим сохраняет возможность поиска по тексту и минимальный размер файла, в то время как растровый режим обеспечивает максимальную стойкость водяных знаков для критически важных корпоративных документов.
Оптимизация производительности для больших документов
Деструктивное преобразование PDF-страниц оказалось значительно более ресурсоемким процессом по сравнению с традиционными методами наложения. Особенно остро эта проблема проявилась при обработке многостраничных документов, содержащих десятки страниц.
Для решения проблемы производительности была реализована система асинхронной многопоточной обработки. Алгоритм разбивает документ на отдельные страницы и обрабатывает их параллельно, максимально используя доступные вычислительные ресурсы.
Архитектура параллельной обработки построена на принципе независимых задач: каждая страница обрабатывается в отдельном потоке без блокировки других операций. Система создает пул асинхронных задач, количество которых соответствует числу страниц в документе, и координирует их выполнение через Java механизм CompletableFuture.
Система включает механизм изоляции сбоев: ошибка обработки одной страницы не влияет на остальные операции, все исключения логируются с указанием конкретного номера страницы для упрощения диагностики.
Такой подход позволил сократить время обработки больших документов в разы, делая технологию применимой для реальных корпоративных сценариев с высокими требованиями к производительности.
Ниже, на диаграмме
продемонстрирован алгоритм выбора стратегии обработки PDF-документа в зависимости от наличия настроенного Java пула потоков (Executor). Схема наглядно показывает два сценария работы системы: асинхронную и синхронную последовательную обработку.
Результаты внедрения и производительность
Практическое тестирование библиотеки в реальных корпоративных условиях продемонстрировало хорошие показатели производительности. Время обработки многостраничных PDF-документов удалось свести к 2-3 секундам даже для файлов, объемом в десятки страниц, что делает технологию применимой для интерактивных сценариев использования.
Разработанная архитектура успешно скрывает техническую сложность за интуитивно понятным DSL, позволяя разработчикам сосредоточиться на бизнес-логике. При этом библиотека сохраняет высокую гибкость с возможностью настройки различных аспектов процесса нанесения водяных знаков.
Заключение
В ходе реализации данного проекта нам удалось не просто «сделать водяной знак», а создать понятную, удобную и многократно используемую библиотеку. Вместо того чтобы распылять сложную логику по разным сервисам, мы собрали её в одном месте, добавили удобный DSL и сделали так, чтобы любая команда могла подключить решение за пару строк кода без погружения в «дебри» PDF графики и потокобезопасности.
Такой подход экономит время, снижает риск ошибок и даёт пространство для развития решения. Теперь, если появится задача обеспечить защиту не только PDF и картинки, но и, скажем, видео или аудио, достаточно будет просто разработать новый модуль, а не переписывать всё с нуля.
Главная мысль проста: если возникает сложная задача, которую придётся решать снова и снова в рамках этого проекта или других, лучше сразу вынести её в отдельную библиотеку и продумать механизм использования. Тогда решение проживёт дольше, будет развиваться другими командами и в итоге принесёт больше пользы, чем разрозненные «одноразовые» реализации.