@open-wa/stickers
v5.6.6
Published
Local sticker rendering with automatic browser or standalone WASM execution
Downloads
2,225
Readme
@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';
import { writeFile } from 'node:fs/promises';
const sticker = await createSticker('./photo.jpg', {
author: 'My label',
pack: 'My pack',
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() manages its own runtime. Call it directly, including for bursts;
there is no runtime to initialize or dispose:
const stickers = await Promise.all(files.map(file => createSticker(file, {
author: 'My label',
pack: 'My pack',
effects: ['comic'],
})));For a bundled browser UI, use the browser entry. Its browser export excludes the standalone Node worker and Node resource cache:
import { createSticker, validateStickerJob } from '@open-wa/stickers/browser';
const job = { effects: ['comic'] };
validateStickerJob(job);
const result = await createSticker(file, job);
// Download or preview result.bytes. Cleanup is automatic.Job validation does not read media or start rendering. The browser runtime uses
the same job planner, renderer and output limits as the other execution hosts.
listEffects({ backend: 'browser', availableOnly: true }) includes option
descriptors alongside defaults: numeric bounds, integer requirements and slider
steps, string lengths, and point-coordinate bounds. The planner uses those same
numeric bounds, so authoring controls can follow the public catalogue.
Browser font verification needs a secure context (HTTPS or localhost). Animated input decoding also depends on the browser's codec support. A browser capability failure leaves server-side code export available; it does not create a successful local render.
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) });
const result = await stickers.createSticker(input, { effects: ['invert'] });The page must provide evaluate(fn, argument). Inside a browser page the default
runtime uses that page directly. Explicit runtimes also clean up automatically;
their optional dispose() method is for an immediate, permanent shutdown.
Bursts wait in arrival order before inputs are read, downloaded or copied, rather than failing when four requests are queued. Node prepares at most two jobs at once and lazily starts up to two standalone workers, leaving a CPU core available where possible. Browser jobs render one at a time and yield between CPU-heavy pixel batches, effects and frames so the page can process input and cancellation. Waiting jobs retain their original input references; callers still own the memory used by those inputs and any completed results they retain.
Warm renderers are reused across the burst. After 60 seconds without work, owned workers, WASM instances, canvases and fonts are released; the next call recreates them automatically. Idle Node workers and cleanup timers do not keep the process alive. Cancelled or timed-out standalone jobs terminate their worker, and later jobs continue with a replacement. Each running job has a two-minute execution budget, including input loading; time spent waiting in the outer queue does not consume that budget. Browser cancellation is observed between pixel batches, effects and frames; an active native codec operation can finish before cleanup. The CLI releases its resources 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
| Component | Extra download | Lifetime | | --- | ---: | --- | | Public source package and shared kernels | About 247 KB unpacked | Installed once per package copy | | Shared Rust kernel binary | 66,478 bytes, included above | Instantiated in the selected renderer | | Standalone CanvasKit loader + WASM | 7,438,222 bytes | Disk cache; worker lives while hot | | Standalone libwebp loader + SIMD WASM | 384,230 bytes | Disk cache; worker lives while hot | | Portable fonts | 46,016–212,196 bytes each | Download 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.
