Skip to content

Verify a render ​

A render manifest states what a render was made from and the hash of what it produced. Anyone with the original can check the result later: on your server with @sealpixel/render, or with the native CLI. The check re-renders and compares pixels, so a file that was tagged, re-wrapped or signed after the render still verifies.

Write the manifest ​

ts
const result = await renderer.render({ original, editLog, manifest: true });
await save(`${result.hash}.jpg`, result.bytes);
await save(`${result.hash}.render.json`, JSON.stringify(result.manifest));

manifest: true is off by default, because the pixel hash reads the whole rendered buffer once. It describes a truth render; a preview render with manifest: true is refused. The native CLI writes the same format with --manifest:

bash
sealpixel render original.jpg plan.json result.jpg --manifest render.json

Check it ​

ts
const report = await renderer.verify({ manifest, original, output });
if (!report.verified) {
  for (const [name, check] of Object.entries(report.checks)) {
    if (!check.ok) console.log(name, check.detail);
  }
}
bash
sealpixel verify render.json --source original.jpg --output result.jpg
text
✓ source    sha256 9f3e…, 6126 x 4136
✓ renderer  native 1.0.0-rc.15, same build
✓ pixels    sha256 e4d9…
✓ output    pixels identical, bytes differ (metadata or a signature added after the render)
VERIFIED

verify() takes the same assets as render() when the render was given fonts or images per request. The CLI exits 1 when the render is not verified. A failed check is a false in the report. verify() throws only for what makes the attempt impossible: a manifest it cannot read, an original it cannot decode or that is over a limit, or an abort. A manifest the CLI wrote has no edit log, so verify() reports editLog as failed and names sealpixel verify; the CLI, in turn, needs the plan and refuses a manifest whose render ran a kernel outside the core.

The checks ​

CheckWhat passesWhat it means when it fails
sourceThe sha256 of the original equals the manifest's, and the decoded size equals the log'sAnother file, or the original was changed
editLogThe log is present, hashes to editLogHash, parses with the registered plugins, and describes the source the manifest describesThe manifest was edited, a plugin is missing, or the CLI wrote the manifest
pluginsThe registered plugin versions equal the ones the log was compiled withAnother plugin version would compile another plan
pixelsRendering the log on the original again gives the same pixel hashThe renderer, a plugin or a kernel produces other pixels now
outputThe given file decodes to the same pixels as the same encoding of the new render. Given only when output is passed; the file is held to the renderer's limits like an originalThe file holds other pixels
rendererThis build equals the one in the manifest: same kind, version and binary hash, both hashes knownInformative only. A different build that reproduces the pixels is still a verified render, and the report says both builds, or that a binary hash is unknown

verified is true when every check except renderer passes. output also reports bytesIdentical: false after a signer or a tagger wrote into the file, which leaves the pixels, and the verdict, untouched.

A lossy output is never held against the raw pixels: the file is decoded and compared with the decoded form of the same JPEG encoding of the new render, so an identical file verifies, and a re-encoded one does not.

The manifest ​

Version 1. A field is added with a default, never renamed or removed; an incompatible change bumps version. The TypeScript type is RenderManifest in @sealpixel/core, the Rust struct sealpixel_core::manifest::RenderManifest.

FieldContent
version1
sourcewidth and height after EXIF orientation, the hash of the original's bytes, mimeType when known
editLog{ log, index } as the editor keeps it. Absent in a manifest the CLI wrote
editLogHashsha256 of editLog in canonical JSON, the text embedEditLog stores in a file
planThe core plan, when every stage ran in the core. The CLI verifies from it
kernelsThe stages that ran outside the core, when any did. Then there is no plan, and only verify() can check it
pluginsThe plugin versions the log was compiled with
rendererkind (wasm or native), version, and binarySha256 of the WebAssembly binary or the native executable
outputoptions as given to the core, never with embed; embedEditLog when the file holds the log; width, height; pixelHash over the rendered pixels; byteHash of the written bytes
renderHashOne id for the render

The pixel hash is sha256 over the width and the height as big-endian 32-bit integers, then the RGBA bytes row by row. It is the hash of the render; hash on the result stays the hash of the file.

renderHash is sha256 over a fixed text, so every runtime agrees on it without canonical-JSON rules:

text
sealpixel-render/1
<source hash>
<edit log hash, or the plan hash in a manifest without a log>
<plugins as id@version, sorted by id, joined by ','>
<renderer kind> <renderer version> <binary sha256 or '-'>
<pixel hash>
<byte hash>

verify does not recompute it. It checks the facts.

From here to Content Credentials ​

manifestDefinitionFor(manifest, options) in @sealpixel/c2pa turns a render manifest into a C2PA manifest definition: the same log, the same index, the renderer's version as the software agent.

The renderer's identity ​

binarySha256 tells two builds of one version apart: a release that changed pixels in a patch is visible in the manifest. The WebAssembly loader hashes the binary it reads from disk; in the browser, where the binary streams from its URL, it reports the hash recorded at build time for the binary that ships next to the glue. A module passed as a URL, a Response or a compiled WebAssembly.Module gives null. The native CLI hashes its own executable.