Skip to content

Render on your server ​

@sealpixel/render renders an edit log on the original. It uses the same WebAssembly core as the browser, so the bytes match a browser export. The contract is small: the original's bytes and the edit log go in, and the rendered file comes out.

The package does no routing, no CORS, no storage and no retries. It maps no HTTP status codes. Put it inside your own route, function or job worker.

ts
import { createRenderer } from '@sealpixel/render';
import { defaultPlugins } from '@sealpixel/plugins';

// Once per process: builds the plugin registry. The first render loads the core.
const renderer = createRenderer({
  plugins: defaultPlugins, // the same list the editor registers
  limits: { maxMegapixels: 80, maxBytes: 100e6, maxLogBytes: 2e6 },
});

const { log, index } = saved; // what the editor's getEditLog() returned
const result = await renderer.render({
  original, // Uint8Array: the original, byte for byte
  editLog: log,
  upTo: index,
  output: { format: 'image/jpeg', quality: 0.9 },
});
// result: { bytes, stream(), hash, width, height, mimeType, timings }
What goes into @sealpixel/render and what comes outYour infrastructure passes the original bytes, the edit log with its index, and the fonts and images the log uses. The renderer checks the inputs, decodes the original once, renders the plan and encodes the file. It returns the bytes with their hash and size, and your infrastructure returns or stores them.YOUR INFRASTRUCTURE@SEALPIXEL/RENDERYOUR INFRASTRUCTUREOriginalfrom your storageEdit log{ log, index }AaAssetsfonts and images1Checklimits, log, hashes2Decodeonce, in memory3Renderthe plan of the log4EncodeJPEG, PNG or WebPRendered filebytes, hash, sizeYour responseor your storage
The tinted zone is @sealpixel/render. The dashed zones are your infrastructure: where the inputs come from and where the file goes. The numbers are the renderer’s steps, in order.

Edit in the browser, render on your server shows where the log and the original come from. Example: your own infrastructure sketches one complete setup around it.

createRenderer(options) ​

OptionDefaultWhat it does
pluginsrequiredThe plugins that interpret the log. Register the same list as the editor.
pluginOptionsnone{ pluginId: partialOptions }, applied with configurePlugins. Use the same map as the editor, so that the renderer enforces the same constraints. An id that is not in plugins throws INVALID_REQUEST here.
assetsnone[{ id, bytes }]: fonts and images that every render can use, for example the plugins' fonts. See Assets.
limits.maxBytes100e6The largest original, in bytes.
limits.maxMegapixels80The largest original, in megapixels, read from the file header.
limits.maxLogBytes2e6The largest edit log, in UTF-8 bytes of its canonical JSON.
verifySourceHashtrueCompare the sha256 of the original with log.source.hash, when the log has one.
wasmfrom node_modules{ module }: a path, the bytes or a compiled WebAssembly.Module of sealpixel_core_bg.wasm. A bundle needs it, see Runtimes.

Create the renderer once and keep it. A warm process then reuses the registry, the loaded core and the sha256 of the assets you passed here.

renderer.render(input) ​

FieldDefaultWhat it is
originalrequiredA Uint8Array with the file the editor loaded, byte for byte. A scaled or re-encoded copy has another hash and fails.
editLogrequiredlog from getEditLog(). The renderer treats it as untrusted and parses it.
upToevery opindex from getEditLog(). The render uses ops[0..upTo). Without it, a step that the user took back reaches the render.
output{ format: 'image/png' }The format and the encoder options, the same as for a browser export.
assetsnone[{ id, bytes }]: fonts and images of this log only, for example a sticker that the user uploaded. An id here wins over the same id in createRenderer.
embedEditLogfalseWrite { log, index } into the file's metadata, as download({ embedEditLog: true }) does.
manifestfalseAlso return the render manifest, for Verify a render.
signalnoneAn AbortSignal. The render stops between steps with ABORTED.

The result:

FieldWhat it is
bytesThe encoded file, a Uint8Array.
stream()A ReadableStream<Uint8Array> over bytes, for a Response or a stream API. The file is complete before the stream starts, so the stream saves no memory.
hashThe sha256 of bytes, computed in the core. A browser export of the same log at the same index has the same hash.
width, heightThe size of the render.
mimeTypeoutput.format.
timingsMilliseconds for check, decode, render, encode and total.
manifestThe render manifest, when manifest: true was passed. See Verify a render.

The checks ​

render() runs these steps in this order. The first step that fails throws, and nothing after it runs.

  1. The size of the original against limits.maxBytes.
  2. The width and the height from the file header against limits.maxMegapixels. No pixel is decoded yet.
  3. The size of the edit log against limits.maxLogBytes.
  4. The parse of the log: its schema, its plugins, and every op against the plugin options.
  5. The sha256 of the original against log.source.hash, when the log has one and verifySourceHash is on.
  6. The decode, once. The decoded size must equal log.source. The decoded size includes the EXIF orientation, so a portrait photo stored as landscape passes. The render reuses this decode.
  7. The final state against the plugin options, for example a locked watermark that must be present.
  8. The render and the encode. Each font or image that the render uses must be in assets, with the sha256 of its declaration. The output carries the ICC profile of the original and nothing else of its metadata. Metadata has the table.

Errors ​

Every failure is a SealpixelError from @sealpixel/core. Its name is the code. Map the codes to your own responses.

CodeCause
TOO_LARGEThe original, its pixel count or the log is above a limit. The core also throws it for a plan whose work or output size is above its own limits (Security).
UNSUPPORTED_FORMATThe original is not a PNG, a JPEG or a WebP.
DECODE_FAILEDThe original has a valid header but does not decode.
INVALID_REQUESTeditLog is missing, upTo is not an integer from 0 to the number of ops, or output.format is unknown.
INVALID_EDIT_LOGThe log does not match the schema.
UNKNOWN_PLUGINThe log uses an op of a plugin that is not in plugins.
OPTION_VIOLATIONAn op, or the final state, breaks a plugin option. details lists the violations.
SOURCE_MISMATCHThe hash or the size of the original is not the one in the log. Or the bytes of an asset do not have the sha256 of its declaration: then details.assetId names the asset.
MISSING_ASSETThe render needs a font or an image that is not in assets. details.assetId names it.
ABORTEDThe signal was aborted.

Assets ​

Text, stickers and watermarks need fonts and images. The ops name them by id. The renderer loads no asset itself: it fetches no URL and reads no file. Pass the bytes of every asset that a log can use:

  • to createRenderer, for the assets that every render uses, such as the fonts of the plugins,
  • to render(), for the assets of one log, such as an uploaded sticker.

assetDeclarations(plugins, editLog?) lists the assets that a render can need: [{ id, sha256, url? }]. It reads the declarations of the plugins and the entries of log.assets, and it does no I/O. url is where the declaration says the bytes are. Use it to find the bytes, or ignore it.

ts
import { assetDeclarations, createRenderer } from '@sealpixel/render';
import { defaultPlugins } from '@sealpixel/plugins';

assetDeclarations(defaultPlugins);
// [{ id: 'font.roboto', sha256: '56a4…', url: 'file:///…/Roboto-Regular.ttf' }]

const renderer = createRenderer({
  plugins: defaultPlugins,
  assets: [{ id: 'font.roboto', bytes: fontBytes }],
});

await renderer.render({
  original,
  editLog: log,
  upTo: index,
  assets: [{ id: 'asset.3f2a9c1d0b7e4a51', bytes: uploadedStickerBytes }],
});

When a declaration carries a sha256, the renderer compares it with the sha256 of the bytes. A plugin declaration wins over an entry in log.assets with the same id. An asset that the render needs and that is not in assets is MISSING_ASSET. The renderer does not look for it anywhere else.

Every render starts with an empty asset table in the core. A render therefore uses only the bytes passed for it, even when an earlier render used the same id with other bytes. The renders of one process share one core instance, so they take turns in it: one render at a time decodes, renders and encodes (Memory).