TL;DR
Microsoft Paint поддерживает и локальную, и облачную генерацию изображений
Paint и Photos также имеют в комплекте локальные модели ИИ
Эти приложения отправляют промпт удалённому серверу для модерации
Вместе с отмодерированным промптом сервер возвращает GUID
GUID встраивается в локально сгенерированное изображение в качестве невидимого водяного знака
Переключаемая в настройках функция видимого водяного знака не управляет этим невидимым водяным знаком
На PC с Copilot+ генерация изображений выполняется локально, но модерация промптов остаётся удалённой
Microsoft раскрывает, что Paint добавляет к сгенерированным ИИ изображениям метаданные C2PA
Сгенерированные ИИ изображения можно сохранять только в форматы, поддерживающие C2PA: PNG, JPEG, GIF и
.paint

Интерес к Microsoft Paint
Это исследование началось благодаря моей заинтересованности в Paint. Недавно я добился определённых успехов в изучении малоизученных функций Windows наподобие UCPD и WHESCVC; к тому же я давно знал, что Microsoft добавила в приложение Paint кучу ИИ‑функций. Лично я не знаю ни одного человека, использующего Paint + ИИ для генерации изображений, но мне любопытно было разобраться, как работает эта генерация.
До начала исследования я предполагал, что для генерации изображений просто вызывается удалённый API. Однако вооружившись Binary Ninja MCP с Codex и приступив к анализу, я вскоре осознал, что в рамках Copilot компания Microsoft выпустила в Windows локальные модели.
Приложение Paint находится по следующему пути (да, теперь все они стали Windows App):
C:\Program Files\WindowsApps\Microsoft.Paint_11.2605.71.0_x64__8wekyb3d8bbwe\PaintApp\
Существует четыре очевидных файла моделей с расширением .onnxe:
seg.onnxe 23,1 МБ inseg_enc.onnxe 28,0 МБ inseg_dec.onnxe 16,5 МБ mager.onnxe 302,4 МБ
Формат seg.onnxe уже был известен: при выполнении XOR со строкой Microsoft_2023 он превращается в обычный файл ONNX. Однако формат остальных трёх файлов .onnxe изначально выглядел иначе.
Оказалось, что Microsoft не поменяла алгоритм, сменив только ключ. В segapi.dll есть небольшая запись ключа:
ps_enc_key.1.0.80-main -> "Microsoft_2023" ps_enc_key.1.0.81-main -> 4096-байтная алфавитно-цифровая строка
После расшифровки для всех них работает onnx.checker.check_model():
Модель | Граф |
|---|---|
| 1094 узлов, ввод: |
| 1014 узлов, вывод: |
| 1133 узла, ввод: эмбеддинги, точки и маски, вывод: |
| 15 284 узла, ввод: изображения/маски, вывод: |
Видимый водяной знак
Изучая эти файлы, я обнаружил Watermarker.dll:

Это меня не сильно удивило, ведь при работе с приложением Paint я уже выяснил, что в нём есть настройка для добавления на создаваемое изображение видимого водяного знака:

Видимый водяной знак представляет собой небольшой логотип Copilot в правом нижнем углу изображения, что абсолютно нормально.
Затем я спонтанно решил попросить ИИ проанализировать DLL, чтобы проверить, может ли она встраивать невидимый водяной знак. Я исходил из своего опыта реверс‑инжиниринга: файл имел размер 1,67 МБ, что необычно много для такой тривиальной функциональности (к тому же, вероятно, видимый водяной знак не требует отдельной DLL). Очевидно, заставило меня задуматься о такой возможности и недавнее заявление о текстовых водяных знаках в Claude Code.
Невидимый водяной знак
Видимый водяной знак добавляется AddPerceptibleWatermark:
CPBDoc::Save(...) | `-- вспомогательная функция сохранения видимого водяного знака (bitmap, WatermarkSetting) | +-- WatermarkSetting::Never | `-- возвращает исходное растровое изображение | +-- WatermarkSetting::AskEveryTime | `-- отображает всплывающее окно с подтверждением Да / Нет | +-- Нет: возврат к исходному растровому изображению | `-- Да: продолжение исполнения | `-- Always или подтверждение выбором "Да" +-- Paint::AI::GetPerceptibleWatermarkSvg() `-- Paint::AI::AddPerceptibleWatermark(bitmap, SVG stream) `-- наложение видимого логотипа Copilot
Также там есть отдельная функция WmkWriteWatermark:
Watermarker.dll!WmkWriteWatermark( output_pixels, payload, payload_length, width, height, stride, input_pixels, pixel_format);
Выполнив трассировку дерева вызовов, можно увидеть, что WmkWriteWatermark вызывается после локальной генерации изображения Stable Diffusion. А в случае сбоя WmkWriteWatermark Paint преобразует всю генерацию в ошибку вместо возврата изображения без неё:
CocreatorViewModel::GenerateImageAsync(...) | `-- Paint::AI::StableDiffusionHelpers::GenerateAsync(..., watermarkId, ...) | `-- Microsoft.ImageCreation.ImageGenerator | `-- Результат-изображение, сгенерированный NPU | +-- вывод проверок безопасности/модерации | +-- Paint::AI::AddWatermark(bitmap, watermarkId) | | | `-- Watermarker.dll!WmkWriteWatermark(...) | | | +-- успех: возвращает растровое изображение с водяным знаком | `-- сбой: превращает генерацию в ошибку | `-- создание успешного StableDiffusionResult
Логично будет заинтересоваться, из чего состоит входящая payload. Сразу становится очевидно, что она должна иметь размер 16 байт:
if (payload_length < 16) return -6; if (payload_length > 16) return -5;
Мне кажется забавным, что код использует два разных кода ошибки, когда полезная нагрузка слишком мала или слишком велика. Функция игнорирует параметр длины и при копировании полезной нагрузки использует жёстко заданный цикл:
for (size_t i = 0; i < 16; i++) message.push_back(payload[i]);
Пока мы не знаем, из чего состоит эта 16-байтная полезная нагрузка, но, как выяснится позже, это GUID! WmkWriteWatermark не встраивает GUID напрямую. Её обёртка создаёт следующее 18-байтное (144-битное) сообщение:
0x4c || GUID[0..15] || (сумма 16 байт GUID по модулю 256)
Базовый кодировщик округляет размеры изображения вниз до чисел, кратных 8, и хранит 144 счётчиков, по одному на каждый бит. Для этого каждый бит должен быть размещён как минимум трижды.
Сам кодировщик вкратце можно описать так:
WmkWriteWatermark(output, guid, 16, width, height, stride, input, format) | +-- валидация указателей, формата, шага и длины полезной нагрузки +-- требование width >= 192 и height >= 192 +-- создание полезной нагрузки | `-- 0x4c || GUID || контрольная сумма в виде суммы байт +-- развёртывание 18 байт в 144 отдельных бит +-- округление размеров вниз до чисел, кратных 8 +-- сканирование/выбор подходящих блоков изображения +-- квантирование значений выбранных блоков/матриц согласно каждому биту +-- проверка наличия как минимум трёх успешных размещений на бит | | | `-- недостаточная ёмкость -> return -8 `-- воссоздание RGB-пикселей в буфер вывода
Цикл встраивания вносит небольшие квантированные изменения в выбранные блоки изображения. Он включает в себя операции с матрицами 3 х 5 и процедуру разложения матриц, использует константы, в том числе 24.0, 0.25, 0.5 и 0.2. Это похоже на адаптивный к содержимому блочно‑доменный водяной знак в стиле SVD.
Я не специалист в водяных знаках на изображениях, но очевидно, что это невидимый водяной знак! ИИ даже написал код для непосредственного вызова этой функции и протестировал его на синтетическом BGRA‑изображении размером 512 x 512 — после добавления водяного знака изменились 193 376 из 262 144 пикселей.
Это приводит нас к следующему вопросу: откуда берутся входные данные для водяного знака?
GUID из удалённой модерации промпта
На границе WmkWriteWatermark полезная нагрузка представляет собой просто указатель и длину. Подсказкой может служить размер 16 байт, но многие данные могут иметь такой размер, поэтому я начал двигаться назад по цепочке вызовов. Непосредственная оболочка в PaintAIManager.dll имеет следующую символьную сигнатуру:
Paint::AI::AddWatermark( Gdiplus::Bitmap& image, winrt::guid const& watermarkId);
winrt::guid, ого! Теперь мы точно знаем, что 16-байтная полезная нагрузка водяного знака — это GUID.
Отследив источник, мы выясняем, что GUID берётся из сетевого запроса. Перед тем, как Paint выполняет локальную модель генерации изображений, AIServices.dll отправляет промпт и стиль по следующему адресу:
https://apsaiservices-a0fqcjc6bzbhgdcd.b02.azurefd.net/ v1/paint-cocreator/moderate-prompt
Запрос представляет собой JSON, содержащий как минимум следующие поля:
{ "prompt": "...", "style": "...", "lastPromptGenerationId": "..." }
Ответ, ожидаемый парсером:
{ "revisedPrompt": "...", "promptGenerationId": "...", "watermarkId": "...", "containsHumanReference": false }
Статический анализ — это здорово, но мне захотелось увидеть реальный ответ сервера. Я воспользовался собственной аутентифицированной сессией Paint и отправил на конечную точку модерации следующий промпт:
a cobalt blue circle above a tiny orange square
Сервер вернул HTTP 200:
{ "revisedPrompt": "a cobalt blue circle above a tiny orange square", "promptGenerationId": "74d9e06b-adea-43ce-85fe-186a26e2e34a", "watermarkId": "83424621-03cb-40e3-9808-a9fae837156d", "containsHumanReference": false }
Ещё я попробовал промпт a portrait of a smiling person wearing a blue hat. На этот раз ответ содержат другую пару GUID, а значение containsHumanReference было равно true. Следовательно, это поле представляет собой классификацию того, связан ли промпт с человеком, выполняемую на стороне сервера. Paint парсит и хранит это значение вместе с ID, однако я не нашёл свидетельств того, что оно влияет на сам этап добавления водяных знаков.
ParseModerateResponse парсит обе строки ID как GUID и отклоняет нулевые значения с InvalidPromptGenerationId или InvalidWatermarkId. watermarkId сервера становится частью сгенерированного изображения:
PaintUI.dll `-- IPromptModerationService `-- PaintAIManager.dll `-- AIServices.dll!ModerateAsync(...) | +-- создание JSON | +-- промпт | +-- стиль | `-- lastPromptGenerationId | +-- HTTPS POST /v1/paint-cocreator/moderate-prompt | `-- AIServices.dll!ParseModerateResponse(response) +-- revisedPrompt +-- promptGenerationId -> парсится как GUID +-- watermarkId -> парсится как GUID `-- containsHumanReference | `-- PaintUI сохраняет WatermarkId `-- StableDiffusionHelpers::GenerateAsync(..., watermarkId, ...) `-- результат локальной Stable Diffusion `-- Paint::AI::AddWatermark(bitmap, winrt::guid const&) `-- WmkWriteWatermark(..., guid, 16, ...) `-- модифицированные RGB-пиксели
Иными словами, локальная генерация не означает, что вся операция выполняется локально. Microsoft получает промпт и модерирует его, а затем создаёт уникальный GUID, который Paint встраивает в локально сгенерированное изображение. Кроме того, со следующим запросом на модерацию Paint отправляет в виде lastPromptGenerationId предыдущий promptGenerationId, что позволяет связывать последующие запросы.
Тот же GUID водяного знака в метаданных C2PA
На этом история не заканчивается. Paint не только изменяет пиксели, но и прикрепляет к сохранённому файлу C2PA Content Credentials. Выполняющий эту операцию код находится ProvenanceHelper.dll на основе provenancesdk.dll.
Для локального пути Stable Diffusion поток выглядит так:
результат локальной Stable Diffusion | +-- Paint::AI::AddWatermark(bitmap, watermarkId) | `-- Watermarker.dll!WmkWriteWatermark(..., watermarkId, 16, ...) | `-- AIServices.dll!SignIngredientOnlineAsync(..., promptGenerationId, image, ...) | +-- POST /v1/paint-cocreator/image-sign | +-- imageMetadata | | +-- PromptGenerationId | | +-- GenerationSeed | | +-- CreativityLevel | | +-- AIFVersion | | `-- оценка модерации | `-- imageToSign.jpg | `-- ParseProvenanceResponse(...) `-- server-supplied C2PA manifest `-- ProvenanceHelper::InsertManifestIngredient(...) `-- AuthoringFinalizeOutputToBufferAsync(...) `-- окончательное изображение с метаданными C2PA
Обратите внимание, что запрос на подписывание отправляет PromptGenerationId, в то время как изображение уже содержит отдельно возвращаемый watermarkId. Сервер присваивает оба значения в процессе модерации, поэтому он может связать запрос на подписывание с водяным знаком, уже присутствующим в переданных пикселях.
Я сохранил реальное изображение из Paint Image Creator и изучил его блоки PNG. Сразу после IHDR шёл 18979-байтный блок caBX, содержащий подписанный манифест C2PA. Для нас интересна следующая часть:
{ "c2pa.soft-binding": { "alg": "com.microsoft.invismark.1", "blocks": [ { "scope": "the entire image", "value": "83424621-03cb-40e3-9808-a9fae837156d" } ] }, "c2pa.actions.v2": { "actions": [ { "action": "c2pa.watermarked", "description": "Content watermarked by Microsoft Responsible AI" } ] } }
В более удобочитаемом виде манифест сообщает следующее:
Генератор:
Microsoft Responsible AI ProvenanceИИ‑система:
Azure OpenAI ImageGenДействие:
c2pa.watermarkedАлгоритм:
com.microsoft.invismark.1Значение водяного знака:
83424621-03cb-40e3-9808-a9fae837156dОписание:
Content watermarked by Microsoft Responsible AI
watermarkId сервера, встроенный в пиксели идентификатор и c2pa.soft-binding.value — это одно и то же значение, присваиваемое при каждой генерации.
Эта связь важна: в C2PA это называется мягкой привязкой — значением, полученным из контента или встроенным в него, чтобы контент можно было соотнести с записью о его происхождении после удаления манифеста на уровне файла. В случае мягкой привязки водяного знака value — это идентификатор контента водяного знака. Microsoft криптографически подписывает эту проверку.
Почему Paint добавляет водяной знак даже при локальной генерации?
Теперь смысл существования Watermarker.dll становится понятнее: у Paint есть два довольно сильно различающихся пути генерации.
Протестированная выше функция Image Creator использует Azure OpenAI ImageGen. Генерация, добавление водяных знаков и указание происхождения происходят в облаке Microsoft, а Paint может просто получить готовое изображение, уже содержащее и невидимый водяной знак, и манифест C2PA:
Image Creator `-- Облако Microsoft +-- фильтрация контента +-- Azure OpenAI ImageGen +-- невидимый водяной знак +-- манифест C2PA `-- готовое изображение возвращается в Paint
С Cocreator ситуация иная. Microsoft заявляет, что на PC с поддержкой Copilot+ NPU генерирует изображение локально, но онлайн‑сервисы Azure при этом всё равно выполняют проверки безопасности. Поэтому эта функция требует и аккаунта Microsoft, и подключения к Интернету, несмотря на то, что инференс Stable Diffusion выполняется на устройстве:
Cocreator на PC с Copilot+ | +-- промпт -> сервис модерации Microsoft | +-- revisedPrompt | +-- promptGenerationId | `-- watermarkId | +-- revisedPrompt + черновик -> локальная генерация NPU | +-- Watermarker.dll -> локальное встраивание watermarkId | `-- онлайн-подписывание источника происхождения -> окончательный манифест C2PA
Вероятно, из‑за этого приложению Paint и требуется реализация локальных водяных знаков. Облачный генератор может перед возвратом результата добавить в него водяной знак. Локальный генератор не может полагаться на это, поэтому Paint вынуждено самостоятельно изменять сгенерированные локально пиксели. Это объясняет и то, почему Paint считает сбой WmkWriteWatermark сбоем всей генерации вместо того, чтобы просто вернуть изображение без водяных знаков.
Есть и ещё один на удивление очевидный признак того, что Microsoft спроектировала путь сохранения на основе источника происхождения. Когда я сохраняю сгенерированный результат непосредственно из панели Image Creator, приложение Paint предлагает только один формат: PNG.

После того, как ИИ‑результат применяется к холсту Paint, доступные форматы по‑прежнему ограничены PNG, JPEG, GIF и собственным форматом Paint .paint. Формат BMP классического Paint подозрительным образом отсутствует в списке.
Это согласуется со списком форматов, поддерживающих C2PA. PNG хранит свой манифест в блоке caBX, JPEG использует один или несколько сегментов маркеров APP11, а у GIF есть собственное представление расширения C2PA для приложения. Формат .paint контролируется Microsoft, он может сохранять любое состояние источника происхождения, требуемое Paint. В спецификации C2PA упоминается BMP в качестве классического формата, в который нельзя встраивать произвольные данные манифеста без использования внешнего манифеста. Если бы приложение Paint допускало экспорт изображения напрямую в BMP, то манифест C2PA на уровне файла потерялся бы.
Такое разделение поднимает любопытный вопрос о безопасности облачного пути. Если бы получилось заставить удалённую конечную точку генерации изображений перед добавлением водяных знаков и информации о происхождении (или если существует внутренняя опция для устранения этих этапов), то можно было бы получить сгенерированное облаком изображение без обоих сигналов.
Классификация такого пути зависела бы целиком от задач Microsoft. Он мог бы быть намеренным поведением, если бы сервису разрешалось возвращать сырые результаты генерации, а Paint отвечал бы только за добавление слоёв источника происхождения. Это был бы баг в продукте, если бы Microsoft не предусмотрела возможность непосредственного вызова API и обхода этапа добавления водяных знаков в Paint. Или это была бы уязвимость, если бы Microsoft считала добавление водяных знаков обязательным способом предотвращения злоупотреблений или контроля за источниками происхождения, а конечную точку можно было бы заставить обойти эту защиту. Так как мы не знаем проектных границ доверия, возможны все три варианта.
Приложение Photos делает то же самое
Когда я пытался найти на диске Watermarker.dll, то обнаружил, что Microsoft Photos содержит DLL с таким же именем:
C:\Program Files\WindowsApps\ Microsoft.Windows.Photos_2026.11060.2004.0_x64__8wekyb3d8bbwe\Watermarker.dll
В функциях Image Creator и Restyle Image приложения Photos тоже применяются локальные операции Stable Diffusion. Обе приводят к одной и той же обёртке водяного знака:
Photos Image Creator `-- PerformSDTextToImageAndWatermarkAsync(..., promptGenerationId, ...) +-- выполнение локальной модели text-to-image `-- ApplyWatermark(image, promptGenerationId) +-- парсинг promptGenerationId в качестве GUID +-- ConvertGUIDtoContiguousByteArray() +-- преобразование RGBA в ARGB +-- Watermarker.dll!WmkWriteWatermark(..., guid, 16, ...) `-- преобразование ARGB обратно в RGBA
Restyle Image идёт по параллельному пути:
Photos Restyle Image `-- PerformSDSketchToImageAndWatermarkAsync(..., promptGenerationId, ...) `-- ApplyWatermark(image, promptGenerationId) `-- Watermarker.dll!WmkWriteWatermark(..., guid, 16, ...)
Разница между Photos и Paint заключается в поведении при сбое. Если кодировщик водяных знаков возвращает ошибку, то его код записывает в лог следующее:
ApplyWatermark encountered error: ... - watermark will not be applied.
Затем он, похоже, продолжает возвращать сгенерированное изображение. Paint же считает сбой добавления водяных знаков сбоем генерации и не возвращает пользователю изображение.
О чём сообщает Microsoft
Проведя этот анализ, я выяснил, что Microsoft раскрывает часть соответствующей информации о системе на странице поддержки Image Creator. Про фильтрацию контента там говорится следующее:
«чтобы предотвратить генерацию изображений, мы применяем фильтрацию контента»
На той же самой странице о сгенерированных изображениях говорится следующее:
«они содержат манифест C2PA, помогающий пользователям определить, что изображение сгенерировано ИИ».
Это также объясняет использование приложением Image Creator онлайн‑сервисов Azure и уведомление о том, что для мониторинга и предотвращения злоупотреблений Microsoft собирает идентификаторы пользователя и устройства. Это раскрытие информации об удалённой фильтрации и метаданных C2PA.
Однако на странице не сообщается о том, что манифест C2PA содержит GUID, идентифицирующий невидимый пиксельный водяной знак, и что путь локальной генерации Paint получает GUID водяных знаков от системы удалённой модерации промптов. Название «Content Credentials» соответствует истине, но пользователю Windows неочевидно, что оно относится к идентификатору на основе промпта.
Заключение
Насколько я знаю, это первое исследование по документированию и анализу процесса добавления невидимых водяных знаков в Paint и Photos. В видимых водяных знаках на сгенерированных ИИ изображениях нет ничего нового — Microsoft задокументировала их для Microsoft 365 и Bing Image Creator, но информация о невидимых пиксельных водяных знаках наподобие Google SynthID и скрытого водяного знака Bing отсутствует.
Microsoft раскрывает, что в Paint используется удалённая фильтрация контента и добавляются C2PA Content Credentials. Новые доказательства демонстрируют, что эти метаданные — не просто пометка ИИ на файловом уровне: файлы подписываются именами c2pa.soft-binding Microsoft InvisMark и записывают свои идентификаторы, хранящиеся в невидимом пиксельном водяном знаке. Манифест файлового уровня и водяной знак на уровне пикселей — два слоя одной и той же системы отслеживания источников происхождения.
Кроме того, локальный и облачный пути объясняют необычное разделение обязанностей. Cloud Image Creator может возвращать уже подписанное изображение с водяными знаками, а Cocreator должен встроить генерируемый сервером идентификатор после локального инференса NPU. В обоих случаях под локальностью не подразумевается офлайновость: промпт всё равно отправляется на серверы Microsoft для модерации, а готовый локальный результат подвергается онлайн‑подписыванию для указания источника происхождения.
Возможно, это подпадает под Статью 50 Закона Евросоюза об ИИ, вступившего в силу 2 августа 2026 года; она требует, чтобы сгенерированный ИИ контент содержал распознаваемую машиночитаемую пометку, но не связанный с промптом GUID. Microsoft раскрывает существование метаданных C2PA, но я не смог найти заявления о генерируемом сервером GUID водяных знаков, его связи с модерацией промптов и его присутствии в пикселях. Эти тонкости определённо влекут за собой последствия, связанные с конфиденциальностью и правом на получение информации.
Кроме того, потенциально можно модифицировать Paint или Photos так, чтобы они миновали этапы модерации промптов и добавления водяных знаков. Но это не даёт никаких новых возможностей: любой пользователь уже может выполнять Stable Diffusion напрямую, без необходимости этих механизмов.

