FaintMark

io.github.matchalatteguy由 GitHub 账号 matchalatteguy 发布v0.1.0更新于 Oct 24, 2026

Invisible per-recipient image watermarks. Mark one copy per recipient, then identify a leaked copy.

已验证STDIO仅桌面安全与监控媒体与设计

概览

AI 生成的概览

本地 MCP 服务器,为图片嵌入每位收件人专属的隐形水印,之后可识别泄露图片来自哪一份副本。

可以做什么
  • 标记图片:为每位收件人生成一份带随机 id 的水印副本(每次 1 至 100 人),支持 JPEG、PNG、WebP,HEIC 转为 JPEG
  • 检查图片:解码转发或外流图片中的水印,返回 matched、unregistered 或 not_detected,深度模式还会尝试翻转和小角度旋转
  • 列出副本:按时间倒序列出已登记的副本及其 id、收件人和时间
  • 比对图片:报告匹配副本的感知哈希是否仍与发出的文件一致
适用场景
适合把同一张照片发给多人、之后想判断是哪位收件人的副本被转发或外流的场景,包括经过聊天软件压缩和缩放之后。也便于在本地记录谁收到了哪份带标记的文件。
运行要求
以 stdio 本地进程运行,通过 uvx(或 pipx、uv tool install)启动;需要 Python 3.11 或更高版本,且客户端 PATH 中能找到 uvx。首次 mark 或 check 会把 Adobe TrustMark Q 模型(65 MB)下载到 ~/.faintmark。处理 HEIC 需安装 heic 附加组件。无需账号、API 密钥或上传;可用 FAINTMARK_HOME 指定其他登记目录。
安装前请注意
匹配结果只说明副本是以哪个标签发出的,不能证明是谁分享的,且只是登记表查询而非签名。任何持有 TrustMark 的人都能读出 id,重新编码、重新打标、合并多份副本或大幅裁剪旋转都可能抹除或破坏水印。本地 SQLite 登记表把 id 对应到姓名,应保持私密并备份;能修改它的人就能改变归属。标记会在本地写入新的图片文件并记录条目。

安装

在 SourceWeft 中

  1. 打开 控制台中的 FaintMark,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

FaintMark

[CI] [PyPI]

Invisible per-recipient image watermarks. Create one marked copy per recipient, then identify which copy a shared or leaked image is. Designed to tolerate the recompression and resizing chat apps apply; see the app tests and limits below. Runs locally, no account, no upload. Python CLI and MCP server.

$ faintmark mark photo.jpg alice bobalice  991e318a21  /Users/me/Pictures/marked/photo-991e318a21.jpgbob  746ac9fa88  /Users/me/Pictures/marked/photo-746ac9fa88.jpg2 copies in /Users/me/Pictures/marked
$ faintmark check ~/Downloads/forwarded.jpgmatched: alice  991e318a21  marked 2026-10-06T23:11:54Z

[Original scene, a WhatsApp chat screenshot of the marked copy, and CLI checks: the received copy matches, the original has no mark, a transplanted id is flagged]

Send each person only their own file; file names carry the id, not the name. A match identifies the label the copy was issued under, not who shared it. The mark is intended to be visually unobtrusive on photographs. JPEG, PNG and WebP keep their format; HEIC becomes JPEG.

Install

sh
uvx faintmark mark photo.jpg alice bob   # runs without installing; needs uvuv tool install faintmark                # or: pipx install faintmark

Python 3.11 or newer on macOS, Linux or Windows (Intel Macs: 3.11 to 3.13). uv can install Python and run FaintMark. The first mark or check downloads Adobe's TrustMark Q models (65 MB, SHA-256 verified) into ~/.faintmark. For HEIC input install faintmark[heic]: uv tool install 'faintmark[heic]', or without a persistent install uvx --from 'faintmark[heic]' faintmark mark photo.heic alice. uv tool uninstall faintmark removes the command and leaves ~/.faintmark in place.

faintmark mark IMAGE RECIPIENT... [-o DIR] [--format png|jpeg|webp] [--json]faintmark check IMAGE [--deep] [--files] [--json]faintmark list [-n N] [--files] [--json]

Copies go into marked/ beside the image unless -o says otherwise. Quote labels with spaces: faintmark mark photo.jpg 'Alice Smith'. One run marks 1 to 100 recipients. --json prints the result the matching MCP tool returns; mark includes the source file name and output paths, check and list omit file names and paths unless --files is set. check --deep also tries a flip and small rotations, and when nothing is detected lists up to three registered ids within 26 of 96 codeword bits of the raw decoder output as candidates, with their bit distance. Those are uncalibrated and can be coincidental or forged; they are never a match. With --json, errors print {"error": "..."}. check exits 0 for matched, 1 for unregistered or not_detected, 2 for an error.

mark accepts non-animated JPEG, PNG, WebP and HEIC up to 25 megapixels without transparency (16-bit RGB is read at 8 bits; 16-bit grayscale and CMYK are rejected) and needs about 1.5 GB of memory at that size. check reads the first frame of anything Pillow opens, up to 100 megapixels.

Agents

sh
claude mcp add --scope user faintmark -- uvx faintmark mcpcodex mcp add faintmark -- uvx faintmark mcpgemini mcp add -s user faintmark uvx faintmark mcp

Any other client that reads an mcpServers config (Claude Desktop, Cursor, Windsurf): add this entry to the client's MCP configuration. The client must find uvx on its PATH; set FAINTMARK_HOME in the server's environment to use another registry.

json
{ "mcpServers": { "faintmark": { "type": "stdio", "command": "uvx", "args": ["faintmark", "mcp"] } } }

Choose one route per client; plugins and extensions bundle the skill too. For HEIC input start the server as uvx --from 'faintmark[heic]' faintmark mcp; the bundled launchers use the base package.

  • Add to Cursor
  • Add to VS Code
  • Claude Code plugin: claude plugin marketplace add matchalatteguy/FaintMark then claude plugin install faintmark@faintmark
  • Codex plugin: codex plugin marketplace add matchalatteguy/FaintMark then codex plugin add faintmark@faintmark
  • Gemini extension: gemini extensions install https://github.com/matchalatteguy/FaintMark
  • Skill for any agent with a shell: npx skills add matchalatteguy/FaintMark
ToolArgumentsReturns
mark_imageimage_path, recipients, optional output_dir, formatoutput folder and each copy's id, path, warnings
check_imageimage_path, optional files, deepstatus of matched (with recipient, id, marked_at, and same_image when a stored hash exists), unregistered (with id) or not_detected; with deep, transform naming the flip or rotation that recovered the result, and suspects (id, recipient, bit_errors, same_image) plus registry_size when nothing was detected
list_copiesoptional limit, filesregistered copies, newest first

Give tools absolute paths. unregistered means an id was decoded but it is not in this machine's registry; not_detected means no readable mark, which says nothing about where the image came from. same_image compares perceptual hashes with the issued copy: false means they differ beyond the threshold, which cropping or an id copied into another picture can cause; the comparison can also miss differences.

check_image and faintmark check --json return one of these: the three plain statuses (a match with same_image: false means the picture differs from the issued copy); with deep, a result recovered by a transform, or a miss with candidates (suspects is empty when no registered id is within the cutoff). The CLI prints errors as the last line; MCP tools raise them as tool errors (isError): messages from the service match, argument validation messages differ.

{"status": "matched", "id": "991e318a21", "recipient": "alice", "marked_at": "2026-10-06T23:11:54Z", "same_image": true}{"status": "matched", "id": "991e318a21", "recipient": "alice", "marked_at": "2026-10-06T23:11:54Z", "same_image": false}{"status": "unregistered", "id": "3123f6f877"}{"status": "not_detected"}{"status": "matched", "id": "991e318a21", "recipient": "alice", "marked_at": "2026-10-06T23:11:54Z", "same_image": true, "transform": "rotated 4"}{"status": "not_detected", "suspects": [{"id": "991e318a21", "recipient": "alice", "bit_errors": 21, "same_image": true}], "registry_size": 100}{"error": "forwarded.jpg: no such file"}

mark_image and faintmark mark --json return the first line; list_copies and faintmark list --json return the second, newest first:

{"original": "photo.jpg", "output_dir": "/Users/me/Pictures/marked", "copies": [{"id": "991e318a21", "recipient": "alice", "path": "/Users/me/Pictures/marked/photo-991e318a21.jpg", "warnings": []}]}{"copies": [{"id": "991e318a21", "recipient": "alice", "marked_at": "2026-10-06T23:11:54Z", "warnings": []}]}

Tested on real apps

7 October 2026, FaintMark 0.1.0, three source photos (an iPhone scene, a CC0 portrait, a CC0 sunset). Each row reports the samples actually sent on that route at default photo quality. Screenshot rows are screenshots of the receiving phone, cropped to the photo before checking. These are individual round-trip observations, not recovery rates or guarantees for an app.

RouteReceivedResult
WhatsApp photo, all threelong edge 1600 px, 155 to 317 KBmatched 3/3
Discord attachment, sceneoriginal size, 1.7 MBmatched 1/1
Discord sticker made from the scene217x285 PNGmatched 1/1
Telegram photo, scene960x1280, 102 KBmatched 1/1
WeChat photo, portrait and sunsetlong edge 1644 to 1920 px, 67 to 302 KBmatched 2/2
Instagram DM, portrait1206x804, 176 KBmatched 1/1
Chat screenshots on WhatsApp, Discord, Telegram, WeChat, Instagramphoto long edge 286 to 1022 pxmatched 9/9
Tinder profile card, portrait (screenshot)heavy cropnot detected 0/1

Every match also reported same_image: true. The originals, every received file, the screenshots, the crops and a manifest of expected ids and results (docs/evidence-2026-10-07.json) are in the evidence release. On a machine without this registry, the received files return unregistered with the id from the manifest; the originals and the Tinder crop return not_detected. X, Signal, iMessage and Instagram stories were not tested.

How it works

TrustMark Q (Adobe, MIT) embeds a 40-bit id as an image-wide pattern designed to tolerate JPEG recompression, WebP conversion and moderate resizing. This is forensic watermarking, not steganography: anyone with TrustMark can read the id; only your registry maps it to a name. FaintMark draws a random id per recipient, writes the copy in the source format (JPEG at the source's own quantization tables, PNG, WebP; HEIC becomes JPEG), decodes the saved copy to verify its id is recoverable, and records id, recipient, file and a perceptual hash of the copy in a local SQLite registry. On a registered match, check compares perceptual hashes and reports same_image: false when their distance exceeds the threshold. A JPEG whose source settings lose the mark is saved at quality 95 instead, and says so in its warnings. Marking an image that already carries a mark replaces it and says so. A preflight re-encodes each copy at JPEG quality 70, at full size (skipped with a warning when a side exceeds 65,500 px, JPEG's limit) and resized to 1280 px (half size when the image is smaller), and warns when the id does not survive; send those copies as a file rather than a photo.

ICC profiles are kept so Display P3 photos stay the same colour. EXIF and other metadata are not copied.

Compatibility with Adobe's implementation is checked in: tests/fixtures holds BCH vectors and two images marked by upstream TrustMark 0.9.2; FaintMark decodes their ids and reproduces their marked pixels within one value per RGB channel. A chat-app style ladder in the real-model tests (resize to 1600, 1280 and 1024 px with JPEG 70 to 80, WebP 75, and a chained resize-plus-JPEG pass, on delivered JPEG bytes) recovers the id on two 12-megapixel synthetic covers and the 320 px fixture, except WebP 75 on the fixture.

Attacks we measured

Someone who has this repository and a copy can do the following; measured on three photos at 2000 px, two ids each, full tables in the attack report attached to the evidence release.

  • Erase the mark invisibly. Re-running the public encoder with a random payload removes it at 42 to 47 dB PSNR (6/6). check then says not_detected, exactly as for an unmarked photo.
  • Overwrite it. Re-marking with another id wins every time (last writer wins, 41 to 44 dB). In these trials the decoder read the replacement and never the previous id.
  • Combine copies. Averaging two or three recipients' copies defeats the plain decoder every time. Against a registry of 100 ids, the closest registered codeword belonged to a colluder in 264 of 264 trials (12 to 30 bits away; the closest innocent was 28 to 41 away), and with the 26-bit cutoff check --deep lists at least one colluder in 253 of 264 and never an innocent. For random decoder output each registered id has about a 4 in a million chance of falling within the cutoff, so coincidences grow with the registry.
  • Forge the picture check. A plain transplant of an id into another photo fails same_image (28 to 32 bits away). A tuned forgery that also nudges the luminance to flip the cheapest hash bits passes both checks at 27 dB PSNR, with a visible but ordinary-looking tone shift.
  • Ordinary edits. Blur, noise, JPEG down to quality 40, downscale to 100 px and back, flips, brightness and contrast changes, a print-and-photograph simulation: the mark survived all of them (6/6). Crops of 15% or more per side and rotations above 4 degrees destroy it for the plain decoder; --deep recovers rotations up to about 10 degrees.

Limits

  • matched means the decoded id is registered to that recipient. It does not prove who shared the image: copies can be forwarded.
  • matched is a registry lookup, not a signature. Anyone who knows an id can embed it in another image. The perceptual-hash comparison may flag a different picture, but cannot authenticate a copy or detect re-marking of the same picture. TrustMark also publishes a watermark remover; an informed recipient can strip the mark, and so can heavy editing.
  • Recipients who combine their copies defeat a plain check; --deep lists candidates by codeword distance, which can be coincidental or forged and is never proof.
  • Images with an aspect ratio above 2:1 are marked only in their centre square; edits that move an image across that boundary can prevent decoding. Heavy downscaling and cropping destroy the mark.
  • Results trust this machine's registry. Keep it private: anyone who can edit it can change attribution.

Registry

~/.faintmark/registry.sqlite3 maps ids to recipient names. Keep it private and back it up; without it, a recoverable id is reported as unregistered. FAINTMARK_HOME selects another directory.

MIT licensed. TrustMark and its models are Adobe's, also MIT. See CONTRIBUTING, SECURITY and releases.

来源:README.md,提交 26fb091

工具

0
SourceWeft 尚未在沙箱中启动过该软件包,因此其工具未知。

版本历史

1
  1. v0.1.0最新Oct 11, 2026

评分

已满足适用检查项的 40%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分

    请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。

  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分

    请在仓库或注册中心条目中声明许可证。