npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.