隐形追踪水印
隐形追踪水印不会添加肉眼可见的图层,而是把一个经过认证的短 locator 写进图片像素。你可以用它查询素材、接收方、导出任务或服务端签名记录。
import Marker, { ImageFormat } from 'react-native-image-marker';
const key = await loadWatermarkKeyFromTrustedStorage();const output = await Marker.embedInvisible({ image: { src: require('./photo.jpg') }, payload: 'asset-42', key, strength: 'robust', saveFormat: ImageFormat.png, filename: 'traced-photo',});
const result = await Marker.detectInvisible({ image: { src: { uri: output } }, key, strength: 'robust', search: 'robust',});
if (result.detected) { console.log(result.payload, result.confidence, result.bitErrorRate);}原生端嵌入后返回缓存文件路径,Web 返回 data URL;检测不会创建新文件。
当 robust 从缩放图片中恢复水印时,result.scale 会返回命中的
0.9、0.95、1.05 或 1.1 假设。它是检测器采用的比例,不是精密测量结果。
批量生成不同接收方的副本
Section titled “批量生成不同接收方的副本”每一项都可以使用不同的 locator、密钥、图片和输出格式。结果始终与输入同序; 单项失败不会让整个批次 reject。Web 最多并发 4 项,原生端保持串行以控制峰值内存。
const controller = new AbortController();const outputs = await Marker.embedInvisibleMany( recipients.map((recipient) => ({ image: { src: source }, payload: recipient.locator, key, strength: 'robust', saveFormat: ImageFormat.png, })), { concurrency: 4, signal: controller.signal, onProgress: ({ settled, total }) => console.log(`${settled}/${total}`), });
const verified = await Marker.detectInvisibleMany( outputs.flatMap((output) => output.status === 'fulfilled' ? [{ image: { src: output.value }, key, search: 'robust' }] : [] ));调用 abort() 只会阻止尚未开始的项目。已经交给 Canvas 或原生解码器的任务会正常完成并报告结果。
在 Web Worker 中检测
Section titled “在 Web Worker 中检测”robust 检测会占用较多 CPU。先把随包发布的独立 Worker 文件复制到可信的同源静态目录:
cp node_modules/react-native-image-marker/lib/worker/invisible-watermark.js \ public/worker/invisible-watermark.js然后在需要时显式启用:
const controller = new AbortController();
const result = await Marker.detectInvisible({ image: { src: suspectImage }, key, strength: 'robust', search: 'robust', worker: { scriptUrl: '/worker/invisible-watermark.js', signal: controller.signal, onProgress: ({ phase }) => console.log(phase), },});
controller.abort(); // 终止正在执行的 Worker,并以 AbortError reject不传 worker 时继续使用 v1.11 的主线程协作式检测。显式配置 Worker 后,脚本、协议或运行错误会直接报告,不会悄悄回退。不要使用不可信的第三方 Worker URL,因为 Worker 能读取图片像素和检测密钥。在线体验使用本站同源文件,可以直接切换两种执行方式。
在可信服务端嵌入与检测
Section titled “在可信服务端嵌入与检测”服务端运行时通过独立子路径按需引入。普通 React Native 入口不会加载它,SDK 也不会安装图片 codec:
const { createInvisibleWatermarkRuntime,} = require('react-native-image-marker/trace-runtime.js');
const runtime = createInvisibleWatermarkRuntime({ codec: yourRgbaCodec, maxConcurrency: 4,});
const output = await runtime.embedInvisible({ image: { src: inputBuffer }, payload: 'asset-42', key: await secretManager.get('trace-watermark-key'), strength: 'robust', saveFormat: 'png',});codec 负责解码、EXIF 方向归一化、等比例 maxSize 限制、RGBA 转换与 JPEG/PNG 编码。运行时会先复制解码后的像素再嵌入,并提供与 Marker 相同的单项和批量方法,包括同序结果、进度和取消语义。
独立的 Node.js Trace Service 示例
使用 Node.js 22、sharp、服务端自有密钥、有限制的 JSON/base64 请求、可注入记录存储与可选 Content Credentials。内存存储和无鉴权只适合本地演示,不能直接当作生产安全边界。
Payload 与密钥规则
Section titled “Payload 与密钥规则”payload必须是 1–12 个 UTF-8 字节。推荐使用随机 locator,例如压缩后的素材 ID 或分发 ID。key至少包含 16 个 UTF-8 字节。不要提交密钥、在应用中打包主密钥,也不要复用 Playground 的公开密钥。- 不要把姓名、邮箱、订单明细或其他个人数据直接写入 payload。应把这些数据放在有访问控制和留存策略的服务端记录中。
- 检测器用截断 HMAC 标签验证水印帧,但它不是公开数字签名。
浏览器页面脚本和扩展可以读取交给 JavaScript 的密钥。生产 Web 分发应在可信服务端嵌入,或只向受控客户端下发范围有限、有效期很短的密钥。
强度与搜索方式
Section titled “强度与搜索方式”| 设置 | 适合场景 | 代价 |
|---|---|---|
subtle |
最重视像素干净程度 | 最不耐后续编码 |
balanced |
普通照片与直接输出 | 默认的视觉与抗性平衡 |
robust |
图片还会被压缩 | 像素改动最强 |
search: 'fast' |
尺寸和 8×8 网格没有变化 | 检测开销低 |
search: 'robust' |
可能有有限裁剪或 0.9×–1.1× 缩放 | CPU 与临时内存开销更高 |
嵌入和检测必须使用相同 strength。Web 检测会在采集像素块和搜索候选时主动让出事件循环;总 CPU 工作量仍然存在,但页面不会被一个持续很久的同步任务完全卡住。
能承受哪些变化
Section titled “能承受哪些变化”项目测试矩阵覆盖直接 PNG、JPEG 质量 90/75/60 重新编码、轻微亮度与对比度变化、错误密钥拒绝、有限裁剪,以及六张照片在 Chromium、Firefox、WebKit 中的 0.9/0.95/1.05/1.1 缩放恢复;先经 JPEG 75 重编码再轻度缩放的链路也在门禁内。图片内容与编码器会影响结果,因此正式使用前仍要用自己的分发链路做验证。
强模糊、超出测试范围的缩放、大面积裁剪、涂抹、生成式修改、截图以及打印后拍照,不属于稳定承诺。没有找到可验证水印时,检测器返回 detected: false,不会把它当作异常。
再加一份签名 Content Credential
Section titled “再加一份签名 Content Credential”基础包只定义轻量的 ContentCredentialsAdapter,不会把 C2PA 运行时或签名私钥塞进 React Native 包。组合顺序固定为:先写入 locator,再对最终像素签名。
const signed = await Marker.embedInvisibleWithCredentials({ watermark: { image: { src: source }, payload: 'asset-42', key, strength: 'robust', saveFormat: ImageFormat.png, }, claim: { title: 'asset-42.png', format: 'image/png' }, adapter,});
const credentials = await Marker.verifyContentCredentials({ image: signed.signedImage, adapter,});独立的 Node.js C2PA 服务示例
使用官方 @contentauth/c2pa-node,证书与私钥只能从运行环境注入。dct-qim-v1
尚未进入 C2PA Soft Binding Algorithm List,因此示例使用普通 hard binding,并在私有
assertion 中只保存算法名与 SHA-256(locator);不会声明 c2pa.watermarked,也不会冒充标准 soft binding。
推荐的服务端流程
Section titled “推荐的服务端流程”- 为每一份导出副本生成随机 locator。
- 在服务端保存 locator、素材 ID、接收方或渠道、时间以及签名审计记录。
- 完成所有可见图层合成后、最终编码前,只把 locator 写入像素。
- 保留原图和水印输出,以便调查漏检。
- 检测时应用自己的置信度策略,并到服务端确认 locator 记录。
帧格式与威胁模型见 RFC 0003。批量处理、缩放恢复、Web 响应性与 Content Credentials 集成见 RFC 0004。 按需服务端运行时、Trace Service、Worker 协议与可靠性矩阵见 RFC 0005。