跳转到内容
菜单
自动
简体中文

隐形追踪水印

隐形追踪水印不会添加肉眼可见的图层,而是把一个经过认证的短 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 假设。它是检测器采用的比例,不是精密测量结果。

每一项都可以使用不同的 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 或原生解码器的任务会正常完成并报告结果。

robust 检测会占用较多 CPU。先把随包发布的独立 Worker 文件复制到可信的同源静态目录:

Terminal window
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 能读取图片像素和检测密钥。在线体验使用本站同源文件,可以直接切换两种执行方式。

服务端运行时通过独立子路径按需引入。普通 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 必须是 1–12 个 UTF-8 字节。推荐使用随机 locator,例如压缩后的素材 ID 或分发 ID。
  • key 至少包含 16 个 UTF-8 字节。不要提交密钥、在应用中打包主密钥,也不要复用 Playground 的公开密钥。
  • 不要把姓名、邮箱、订单明细或其他个人数据直接写入 payload。应把这些数据放在有访问控制和留存策略的服务端记录中。
  • 检测器用截断 HMAC 标签验证水印帧,但它不是公开数字签名。

浏览器页面脚本和扩展可以读取交给 JavaScript 的密钥。生产 Web 分发应在可信服务端嵌入,或只向受控客户端下发范围有限、有效期很短的密钥。

设置 适合场景 代价
subtle 最重视像素干净程度 最不耐后续编码
balanced 普通照片与直接输出 默认的视觉与抗性平衡
robust 图片还会被压缩 像素改动最强
search: 'fast' 尺寸和 8×8 网格没有变化 检测开销低
search: 'robust' 可能有有限裁剪或 0.9×–1.1× 缩放 CPU 与临时内存开销更高

嵌入和检测必须使用相同 strength。Web 检测会在采集像素块和搜索候选时主动让出事件循环;总 CPU 工作量仍然存在,但页面不会被一个持续很久的同步任务完全卡住。

项目测试矩阵覆盖直接 PNG、JPEG 质量 90/75/60 重新编码、轻微亮度与对比度变化、错误密钥拒绝、有限裁剪,以及六张照片在 Chromium、Firefox、WebKit 中的 0.9/0.95/1.05/1.1 缩放恢复;先经 JPEG 75 重编码再轻度缩放的链路也在门禁内。图片内容与编码器会影响结果,因此正式使用前仍要用自己的分发链路做验证。

强模糊、超出测试范围的缩放、大面积裁剪、涂抹、生成式修改、截图以及打印后拍照,不属于稳定承诺。没有找到可验证水印时,检测器返回 detected: false,不会把它当作异常。

基础包只定义轻量的 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。

  1. 为每一份导出副本生成随机 locator。
  2. 在服务端保存 locator、素材 ID、接收方或渠道、时间以及签名审计记录。
  3. 完成所有可见图层合成后、最终编码前,只把 locator 写入像素。
  4. 保留原图和水印输出,以便调查漏检。
  5. 检测时应用自己的置信度策略,并到服务端确认 locator 记录。

帧格式与威胁模型见 RFC 0003。批量处理、缩放恢复、Web 响应性与 Content Credentials 集成见 RFC 0004。 按需服务端运行时、Trace Service、Worker 协议与可靠性矩阵见 RFC 0005