@watermarker/browser
v1.0.2
Published
Fast image watermarking in the browser: logos, text and timestamps, tiled or positioned, powered by Rust + WebAssembly.
Maintainers
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/browserUse
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 collectedOptions
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-isNothing 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.sizeis the box the mark occupies: the tile cell when repeating, the placed rectangle otherwise. Every layer has one.fontSizeis 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
