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

@watermarker/browser

v1.0.2

Published

Fast image watermarking in the browser: logos, text and timestamps, tiled or positioned, powered by Rust + WebAssembly.

Readme

@watermarker/browser

Fast image watermarking in the browser: logos, text and timestamps, positioned or tiled, with effects, powered by Rust compiled to WebAssembly. Nothing is uploaded, and every photo stays in the page.

npm install @watermarker/browser

Use

import { watermark } from '@watermarker/browser';

const out = await watermark(file, {
  layers: [{ type: 'text', value: '© Alex Do', layout: 'mesh', style: 'subtle' }],
});

For many photos, build a Watermarker once. The mark is prepared on construction, so each apply is only a blend:

import { Watermarker } from '@watermarker/browser';

const wm = await Watermarker.create({
  layers: [
    { type: 'image', source: logoBytes, layout: 'corner' }, // bytes, a Blob/File, or a data URL
    { type: 'text', value: '© Alex Do', layout: 'corner' },
  ],
});
for (const file of files) results.push(await wm.apply(file));
wm.free();                          // wasm memory is not garbage collected

Options

layers holds the marks, drawn bottom to top in the order given. Each carries its own layout and style:

{
  layers: [
    { type: 'image', source: logoBytes, layout: { position: 'bottomRight' } },
    { type: 'text', value: '© Alex Do', layout: 'mesh', style: 'subtle' },
    { type: 'timestamp', from: 'exif', format: '%Y-%m-%d %H:%M', layout: 'corner' },
  ],

  output: { format: 'same', quality: 90, metadata: 'preserve' },
}

A logo, a line of text and a timestamp are the same thing with different content, so they are one list rather than three named slots. That also means two of a kind is allowed, and z-order is explicit rather than fixed.

repeat

layout.repeat is CSS background-repeat: false places one mark, 'x' and 'y' repeat along one axis, 'both' tiles. There is no separate tile type to pick, which is what lets a preset's layout and your own always combine.

It also decides which of the other layout fields the engine reads:

| field | applies when | |---|---| | size, rotation, area | always | | fit | image marks only, text is always contained | | position, padding, offset | repeat is off | | gap, stagger, spacing | repeat is on |

Inapplicable fields are ignored, not rejected, so flipping repeat on a layout you already tuned cannot fail.

area

The region a layout works inside, which is the whole canvas unless you say so. Described the same way a mark is placed, one level up, and marks are clipped to its edges:

// A dense mesh over the top-left quarter, and nothing anywhere else.
{
  layers: [
    {
      type: 'text',
      value: 'PROOF',
      layout: {
        repeat: 'both',
        rotation: -45,
        size: '12%',
        gap: '4%',
        area: { position: 'topLeft', size: ['50%', '50%'] },
      },
    },
  ],
}

Units

24 or '24px' for pixels, '18%' of the canvas, '12pt' scaled by the photo's DPI, 'auto' for an asset's own size. Prefer % across mixed-size photos: a px mark tuned for a 6000px original is invisible on a thumbnail.

Positions

'bottomRight', 'southEast' and ['100%','100%'] are the same position. CSS keywords, ImageMagick and sharp gravity names, and explicit pairs all work.

color: 'auto'

The default for text. Picks white or near-black from the luminance under each placement, so a tiled mark stays readable as it crosses a gradient. It costs one cheap sample per tile, and the mark itself is still rasterized once. Set an explicit color to opt out.

Presets

Two registries, one per field, so they multiply rather than enumerate:

layout: corner, center, bar, mesh, meshDense, bands, grid, edges

style: subtle, proof, badge, plate, wash, embossed, stamp, fade

{ layers: [{ type: 'text', value: 'PROOF', layout: 'meshDense', style: 'proof' }] }

A preset is a complete layout or style, not defaults merged under one, so there are two ways to write either field and no third:

layout: 'mesh'                                      // the preset
layout: { repeat: 'both', size: '22%', gap: '8%' }  // written out, used as-is

Nothing is inherited from the registry into an object you wrote, so what is on the page is what runs. Writing { preset: 'mesh', gap: '8%' } is an error that says so.

To start from a preset and change one thing, spread it:

import { presetOptions } from '@watermarker/browser';

const layout = { ...presetOptions('layout', 'mesh'), gap: '8%' };

presetOptions reads the engine, so the values cannot drift from what it runs. layoutPresets() and stylePresets() list the names the same way.

fontSize vs layout.size

Two different things, which is why they have different names:

  • layout.size is the box the mark occupies: the tile cell when repeating, the placed rectangle otherwise. Every layer has one.
  • fontSize is the em size of the type, on text and timestamp layers only.

Leave fontSize unset and the text is rasterized at whatever size fills the box. That is why a layout preset works on its own: center gets large text and mesh gets tile-sized text, with no font size to keep in sync. Set fontSize when the type size is what matters, such as a caption at '12pt'.

Timestamps have no literal value

A timestamp comes from the photo's EXIF date or the clock. For a fixed string, use a text layer.

Formats

JPEG, PNG and WebP in and out. EXIF and ICC are carried across by default, and EXIF orientation is baked into the pixels so a mark positioned "bottom right" lands on the edge you actually see. HEIC is not supported here because browsers cannot decode it. The iOS and Android bindings use the OS decoder for it.

Size

The wasm bundle is about 4 MB, or 1.5 MB gzipped, including a bundled font and an SVG renderer.

License

MIT