open-wa
DocsAPI reference

@open-wa/stickers

Local sticker rendering with automatic browser or standalone WASM execution

@open-wa/stickers

Create WebP stickers locally, using an available browser for jobs it can execute completely, or a portable standalone worker for the whole job. No Sharp, native addon build, browser download or default model weights.

import { createSticker } from '@open-wa/stickers';

const sticker = await createSticker('./photo.jpg', {
  effects: ['comic', { name: 'tint', options: { color: '#e056ae' } }, 'sparkle'],
});
await writeFile('./sticker.webp', sticker.bytes);

// The SDK supplies its existing browser context automatically.
await client.sendSticker(to, './photo.jpg', {
  effects: ['comic', 'sparkle'],
  author: 'My label',
  pack: 'My pack',
  quotedMsgId: messageId, // optional sticker reply
});

The SDK imports this package on the first sticker request. Rendering produces pixels and optional author/pack labels. Sending requires the private sticker patch: it owns donor app links and finalizes EXIF before native hashing, encryption and upload. Rendering alone doesn't produce donor-branded output. Sticker payloads are rejected by sendRawMessage; use sendSticker to reach the finalizer. Native sticker forwarding is blocked until its local re-finalization path is ported.

Routing and lifecycle

createSticker(input, job) defaults to backend: 'auto'. Before decoding, the planner compares the input codec and every ordered effect with the available browser capabilities. If all requirements fit, the job runs in that browser. Otherwise the complete job runs standalone. An unsupported complete job fails with CAPABILITY_UNAVAILABLE; it never transfers intermediate frames between backends or calls a rendering server. Explicit browser and standalone overrides are strict.

Outside a browser, standalone is the default. For an existing page/driver:

import { createStickerRuntime } from '@open-wa/stickers';
import { createBrowserBackend } from '@open-wa/stickers/browser';

const stickers = createStickerRuntime({ browser: createBrowserBackend(page) });
try {
  const result = await stickers.createSticker(input, { effects: ['invert'] });
} finally {
  await stickers.dispose();
}

The page must provide evaluate(fn, argument). Inside a browser page the default runtime uses that page directly. Each backend serializes work, caps its queued inputs at 64 MiB and four requests, and drops owned resources after 60 seconds without work. Standalone cancellation terminates the active worker; queued jobs continue in a new worker. Browser cancellation prevents submission and rejects the result, with cancellation observed between frames; an active native codec operation can finish before cleanup. The CLI disposes its worker when the command ends.

CLI

openwa-sticker photo.jpg --effects comic,tint,sparkle --output sticker.webp
openwa-sticker photo.jpg --job sticker-job.json --output sticker.webp
openwa-sticker effects
openwa-sticker effects --all

--job supplies the same JSON job fields as the library. --offline requires previously cached resources; --cache-dir selects the resource cache. By default, resources live under $XDG_CACHE_HOME/openwa/stickers/v1, or ~/.cache/openwa/stickers/v1. Resources are pinned by version, length and SHA-256. They download on demand and are verified before execution. Proxies and custom certificate authorities must be configured for Node's network client.

Current coverage

This first implementation contains 62 shared recipes: 57 versioned OpenWA-authored recipes and five additional numerical effects. Both backends use the same recipe definitions; Skia supplies standalone Canvas operations, and libwebp supplies standalone encoding. Invert, grayscale, stereo, dilation, erosion and Bayer expansion use shared Rust SIMD WASM. The other effects currently use JavaScript and their context's compositor. This is not a claim of historical Jeyy parity.

PNG, JPEG, GIF and WebP are implemented in both contexts, including animated inputs. AVIF is available in browsers with an appropriate ImageDecoder. Standalone AVIF and MP4 are not yet implemented. The historical catalogue is included as an inventory; its unported entries fail explicitly. Use listEffects({ availableOnly: true }) to enumerate executable recipes. Scene/simulation recipes, remaining historical ports, background removal and statistics reporting remain separate epic work.

Custom colours use the portable sRGB subset: hex, named colours and comma-separated RGB/HSL. Jobs accept up to eight ordered effects. freeze keeps the first frame at its position in the stack; subsequent animated effects can introduce new motion. fps samples the selected timeline without speeding it up. slow changes frame durations. trim uses source milliseconds. quality is a maximum: the encoder can lower it to meet the byte budget and fails if the output still cannot fit. Text recipes lazily load pinned portable fonts in either context.

Resource costs and limits

ComponentExtra downloadLifetime
Public source package and shared kernelsAbout 247 KB unpackedInstalled once per package copy
Shared Rust kernel binary66,478 bytes, included aboveInstantiated in the selected renderer
Standalone CanvasKit loader + WASM7,438,222 bytesDisk cache; worker lives while hot
Standalone libwebp loader + SIMD WASM384,230 bytesDisk cache; worker lives while hot
Portable fonts46,016–212,196 bytes eachDownload only for selected text recipes

Browsers use their existing Canvas and codecs and download neither standalone compositor nor encoder. A 512×512 RGBA surface consumes 1 MiB; recipes can own several temporary surfaces during a frame. Frames are encoded sequentially. WASM heaps, Skia allocations and browser/worker overhead add memory beyond those surfaces. Peak RAM hasn't been measured for this implementation; the worker's 128 MiB JavaScript heap limit doesn't cap its native/WASM allocations.

The admission profile is 512×512, at most 120 frames / 10 seconds, 20 MiB input, 16 million source pixels, and 100,000 / 512,000 output bytes for static / animated stickers. Rendering reserves 4,096 bytes for final metadata. The private patch checks final bytes again. An unknown send outcome is never retried automatically.

WASM and client patches can be inspected and modified by a machine's owner. Donor protection applies to the supported OpenWA send path, not to arbitrary modified clients or externally generated files.

Was this helpful?

Your answer includes the page path and docs version.

On this page