Skip to main content

Watermark

Two different verbs, depending on whether your watermark is an image or text:

  • watermark(overlay, options) — composites a second image onto your file. Multi-input: the overlay is its own file. Image or video base.
  • textWatermark(text, options) — stamps a text label. Single-input: the text is an option, not a file. Image base only.

There is no document watermarking — see What cannot be watermarked.

Image overlay — watermark()​

The overlay is a file-node, exactly like your base file. Pass the node itself — there are no string input refs. You can process the overlay first if you want (it is a recipe like any other).

const logo = client.file('logo.png');

const result = await client
.file('photo.jpg')
.watermark(logo, { anchor: 'bottom_right', opacity: 0.6 })
.run({ maxWait: '2m' });

await result.toFile('photo-wm.jpg');

Which media route where​

watermark() routes on the base file's media — you use the same verb either way:

Base fileWire operation
image/jpeg, image/png, image/webpimage_watermark
image/tiff, image/bmpimage_watermark
video/mp4, video/webmvideo_watermark — ⛔ withdrawn, see below
image/gifimage_watermark — not currently accepted
Availability is not mirrored on this page

This table used to carry a per-MIME availability column. It was wrong — it reported video_watermark as Beta after the contract had promoted it — and a hand-copied availability value is wrong the moment the contract moves.

Availability travels with the contract, and getSchema() returns the authoritative per-operation, per-media-type answer at runtime. Ask it rather than a documentation page if you are gating a feature programmatically.

Those are the only accepted types. Other video containers (MOV and friends) are rejected locally, before upload.

Video watermarking is WITHDRAWN as of contracts v2.203.0

video_watermark was stable and is now planned. The SDK refuses a video base before any upload — file('clip.mp4').watermark(...) throws GislConfigError, it does not reach the server.

⚠️ This is not the usual "coming soon". The worker exists; it cannot serve the ceiling the contract advertised, which is why the capability was pulled rather than never shipped. Re-listing at honest ceilings is expected — so treat this as a pause on a route that worked, not as a feature nobody has built.

The example below is kept because the call shape does not change when it returns. It does not work today.

A video base needs no different call — and, until the withdrawal is reversed, no call at all:

const result = await client
.file('clip.mp4')
.watermark(logo, { anchor: 'top_right', overlay_width: '20%' })
.run({ maxWait: '5m' });

What cannot be watermarked​

Video, documents, audio and animated GIF have no watermark you can use today. Video is the newest and the odd one out: it is withdrawn rather than unbuilt (see the note above). Some routes are planned in the contract (a dedicated audio-watermark operation, and image/gif on image_watermark) but are not yet exposed here; document watermarking has no path at all.

textWatermark() is not an alternative for these — it is image-only (unlike watermark(), which additionally takes video, textWatermark() does not take document, audio or video). If you have a PDF, there is currently no watermark verb for it. When you hand watermark() a document base, the SDK rejects it before upload and the error says exactly that.

The pre-upload gate​

An unsupported base throws locally, before anything uploads — you do not pay a round trip to discover that a PDF cannot take an image overlay. The SDK checks as soon as it knows the base media: usually at the .watermark() call itself, and otherwise just before upload on run() / submit() (when the base media cannot be detected up front, such as a stream).

The error is a GislConfigError; its reason says which check failed:

reasonCause
feature_not_availableThe base type is declared but not available — a video or GIF base today.
unsupported_mediaThe base type has no watermark route — audio, documents, and image or video types not listed above.
invalid_overlay_mediaThe overlay is not an image.
media_unknownThe base's type cannot be detected — use a path with an extension, or a typed input.
invalid_overlay_countFewer than 1 or more than 8 overlays were passed.
overlays_count_mismatchoverlays does not have exactly one entry per overlay — see Several overlays.
overlays_unsupported_baseoverlays, or more than one overlay, on a base other than JPEG, PNG or WebP.

Options​

anchor takes a 9-grid position: top_left, top_center, top_right, center_left, center, center_right, bottom_left, bottom_center, bottom_right.

OptionTypeDefaultNotes
anchoranchorbottom_rightWhere the overlay sits.
margin_x / margin_ystring'0px'Offset from the anchor — '40px' or '5%'.
opacitynumber0.50–1.
overlay_widthstringoverlay's own size'120px' or '20%'; the aspect ratio is kept.

Defaults are applied by the API when you omit an option. With anchor: 'center' a non-zero margin is rejected at workflow creation (422 invalid_options) — the centre has no edge to offset from; pick an edge or corner anchor to nudge it.

Unknown keys are rejected before upload.

Several overlays​

On a JPEG, PNG or WebP base you can stack up to 8 overlays in one call: pass a list of overlay nodes, and place each one with an overlays entry. overlays[i] places the i-th overlay, so overlays needs exactly one entry per overlay. Later entries are drawn on top of earlier ones.

const result = await client
.file('photo.jpg')
.watermark([client.file('logo.png'), client.file('badge.png')], {
overlays: [
{ anchor: 'top_left', margin_x: '20px', margin_y: '20px' },
{ anchor: 'bottom_right', overlay_width: '15%', opacity: 0.8 },
],
})
.run({ maxWait: '2m' });

Use overlays or the top-level placement options, not both. Both SDKs refuse the combination before uploading anything, with a GislConfigError whose reason is invalid_combination (the API would answer 422 invalid_options). Likewise, more than one overlay needs overlays, with one entry per overlay: without it the SDK refuses with overlays_count_mismatch.

Reusing an overlay​

To avoid re-uploading the same overlay on every run, upload it once and reference it by id. An uploadId input is passed through without being uploaded again:

import { fileInput } from '@giveitsmaller/sdk';

const logo = client.file(fileInput.uploadId('file_...'));

Within a single run there is no upload de-duplication for watermark inputs — the base and the overlay are always uploaded separately, even if they are the same file.

Text label — textWatermark()​

No second file: the text is the first argument.

const result = await client
.file('flyer.png')
.textWatermark('CONFIDENTIAL', { anchor: 'center', opacity: 0.5 })
.run({ maxWait: '2m' });

await result.toFile('flyer-wm.png');

Accepts image/jpeg, image/png, image/webp, image/tiff and image/bmp — all stable. No video, audio or document support.

Tiling​

watermark_mode: 'tiled' repeats the label across the image; tile_spacing sets the gap in pixels.

await client
.file('flyer.png')
.textWatermark('DRAFT', {
watermark_mode: 'tiled',
tile_spacing: 120,
rotation: -30,
opacity: 0.3,
})
.run({ maxWait: '2m' });

Options​

OptionTypeDefaultNotes
font_sizenumber488–512 pixels.
colorstring'#FFFFFF80'Hex RGB/RGBA.
font_family'liberation_sans''liberation_sans'Bundled font.
rotationnumber-45Degrees, -360–360.
watermark_mode'single' | 'tiled''single'Rendering mode.
tile_spacingnumberfont_size0–1000 pixels between tiled labels.
anchoranchorbottom_rightSame 9-grid as above. Ignored when tiled.
margin_x / margin_ystring'0px'Offset from the anchor. Ignored when tiled.
opacitynumber10–1.

The same anchor: 'center' margin rule applies in single mode.

Passing text in the options bag is rejected — the first argument owns it.

One operation per job​

Both watermark operations must be the only operation in their job — and the SDK handles that for you. Chaining .compress() / .convert() / .thumbnail() after .watermark() is supported: the SDK lowers the post-watermark steps into a separate downstream job that consumes the watermark's output, rather than packing both operations into one job. You do not need two runs.

See also​

  • Image output — quality, target size and resize via output().
  • Thumbnail — a separate preview artifact, rather than a stamp.