Skip to content
Menu
Auto
English

Invisible trace watermarks

Invisible trace watermarks keep the visible composition unchanged while carrying a short authenticated locator in the image pixels. Use the locator to look up an asset, recipient, export job, or signed server record.

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);
}

Native embedding returns a cache-file path; Web returns a data URL. Detection does not create a file.

When robust recovers a resized image, result.scale reports the successful 0.9, 0.95, 1.05, or 1.1 hypothesis. It is a detector estimate, not a precise measurement.

Distribute recipient-specific copies in a batch

Section titled “Distribute recipient-specific copies in a batch”

Each item can use a different locator, key, image, and output format. Results stay in input order; one failure does not reject the whole batch. Web runs at most four items concurrently, while native platforms stay serial to limit peak memory.

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' }]
: []
)
);

Calling abort() stops new items from starting. Work already handed to Canvas or a native decoder finishes normally and still reports its result.

Robust detection is CPU intensive. Copy the standalone Worker artifact into a trusted same-origin static directory:

Terminal window
cp node_modules/react-native-image-marker/lib/worker/invisible-watermark.js \
public/worker/invisible-watermark.js

Then opt in for each detection:

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(); // terminates an active Worker and rejects with AbortError

Without worker, detection keeps the v1.11 cooperative main-thread behavior. When a Worker is configured, script, protocol, and runtime failures are reported instead of silently falling back. Never load this Worker from an untrusted third-party origin: it receives both image pixels and the detection key. The live Playground uses the same-origin artifact and lets you compare both execution modes.

Import the runtime through its explicit subpath. The normal React Native entry does not load this module, and the SDK does not install an image 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',
});

The codec owns decoding, EXIF orientation, aspect-preserving maxSize, RGBA conversion, and JPEG/PNG encoding. The runtime copies decoded pixels before embedding and provides single and batch methods with the same result ordering, progress, and cancellation semantics as Marker.

The independent Node.js Trace Service example uses Node.js 22, sharp, a service-owned key, bounded JSON/base64 requests, an injectable record store, and optional Content Credentials. Its in-memory store and lack of authentication are local-demo defaults, not a production perimeter.

  • payload must contain 1–12 UTF-8 bytes. Prefer a random locator such as a compact asset or distribution ID.
  • key must contain at least 16 UTF-8 bytes. Do not commit it, bundle a master key, or reuse the public Playground key.
  • Do not put names, email addresses, order details, or other personal data in the payload. Store that data behind the locator with appropriate access controls and retention.
  • Detection authenticates the embedded frame with a truncated HMAC tag. It does not create a public digital signature.

Browser scripts and extensions can read any key available to page JavaScript. For production Web delivery, embed on a trusted server or issue narrowly scoped, short-lived keys to a controlled client.

Setting Use it when Trade-off
subtle The cleanest pixels matter most Least tolerant of later encoding
balanced General photos and direct output Default visual/robustness balance
robust The image will be recompressed Strongest pixel change
search: 'fast' Original dimensions and block grid are preserved Low detection cost
search: 'robust' Limited crop or 0.9×–1.1× resizing is expected More CPU and temporary memory

Use the same strength for embedding and detection. On Web, detection yields to the event loop while collecting blocks and searching candidates, so the page can keep responding even though the total CPU work remains substantial.

The project test matrix covers direct PNG output, JPEG re-encoding at quality 90/75/60, mild brightness and contrast changes, wrong-key rejection, limited crop recovery, and 0.9/0.95/1.05/1.1 resizing across six photographs in Chromium, Firefox, and WebKit. JPEG 75 re-encoding followed by light resizing is covered too. Real images and encoders vary, so validate your own delivery pipeline before relying on a threshold.

Heavy blur, resizing outside the tested range, large crops, painting over the image, generative edits, screenshots, and print-camera workflows are not stable guarantees. Detection returns detected: false instead of throwing when no authenticated frame is found.

The core package defines a small ContentCredentialsAdapter rather than shipping a C2PA runtime or signing key. The composition order is fixed: embed the locator first, then sign the resulting pixels.

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,
});

The independent Node.js C2PA service example uses the official @contentauth/c2pa-node package and injects its certificate and private key from the environment. dct-qim-v1 is not registered in the C2PA Soft Binding Algorithm List, so the example uses a normal hard binding and a private assertion containing only the algorithm and SHA-256(locator). It does not claim c2pa.watermarked or standard soft-binding compatibility.

  1. Generate a random locator for each exported copy.
  2. Store the locator, asset ID, recipient or channel, timestamp, and a signed audit record on the server.
  3. Embed only the locator after visible layers are composed and before final encoding.
  4. Keep the original and marked output long enough to investigate false negatives.
  5. When detecting, apply your own confidence policy and confirm the locator against the server record.

The frame format and threat model are specified in RFC 0003. Batching, resize recovery, responsive Web detection, and Content Credentials integration are specified in RFC 0004. The on-demand server runtime, Trace Service, Worker protocol, and reliability matrix are specified in RFC 0005.