Watermarks
A watermark is text, a shape or an image drawn over a document's pages: on screen, in thumbnails and slide shows, in print, and in exportToBytes and exportPageToBytes. It works on every document type the viewer and the editor open, and the same call is on UDocSession in @docmentis/udoc-node.
import { UDocViewer } from "@docmentis/udoc-viewer";
const viewer = await UDocViewer.create(document.getElementById("udoc")!);
// Set before opening, so the document's first frame already has it.
await viewer.setWatermarks([
{
content: {
text: {
fontFamily: "Arial",
bold: true,
italic: false,
size: 72,
color: { colorSpace: "srgb", components: [0.5, 0.5, 0.5] },
text: "CONFIDENTIAL",
},
},
opacity: 0.2,
target: "all",
placement: { at: { anchor: "center", offsetX: 0, offsetY: 0, rotation: -45 } },
},
]);
await viewer.open("https://example.com/report.pdf");setWatermarks replaces whatever was set before, and [] clears it. Later watermarks in the list draw on top of earlier ones, so two can be stacked, for instance text over a shape. It rejects if a watermark can't be drawn, and then leaves the ones before in place. It stays set across open, and setting it before the first open means no frame ever shows without it.
A watermark is not saved or enforced
saveToBytes and the Download button give the document as it is, in its own format, with no watermark. Set showDownloadButton: false if that file shouldn't be handed out, or offer exportToBytes("pdf") instead. In a browser a watermark deters; it doesn't protect, because the document is already on the viewer's machine.
What it draws
content | Fields | Its box |
|---|---|---|
text | fontFamily, bold, italic, size (points), color, text | As wide as the text and as tall as its font's ascent and descent. One line, no wrapping. |
shape | path, fillColor, strokeColor, strokeWidth (points) | What it paints, stroke included. |
image | pixels, width, height, scale | width / scale by height / scale points. |
A colour is { colorSpace: "srgb", components: [r, g, b] }, each from 0 to 1. opacity runs from 0, invisible, to 1, and applies to the whole watermark.
import { UDocViewer } from "@docmentis/udoc-viewer";
const viewer = await UDocViewer.create(document.getElementById("udoc")!);
await viewer.setWatermarks([
{
content: {
shape: {
// A 40 by 40 point diamond. Where these coordinates put it doesn't
// matter: the placement moves its outline.
path: {
segments: [
{ moveTo: { x: 20, y: 0 } },
{ lineTo: { x: 40, y: 20 } },
{ lineTo: { x: 20, y: 40 } },
{ lineTo: { x: 0, y: 20 } },
"close",
],
},
fillColor: { colorSpace: "srgb", components: [1, 0, 0] },
strokeColor: { colorSpace: "srgb", components: [0, 0, 0] },
strokeWidth: 1,
},
},
opacity: 0.15,
target: "all",
// Copies across every page, 60 points apart.
placement: { tile: { gapX: 60, gapY: 60, rotation: 0 } },
},
]);An image's pixels are its decoded samples, not a PNG or JPEG file: four bytes a pixel in R, G, B, A order, top row first, as getImageData gives them. In Node, decode the file with a library of your choice. scale is pixels per point, so 1 draws each pixel a point across, and 2 suits a logo made for high-density screens.
import { UDocViewer } from "@docmentis/udoc-viewer";
const viewer = await UDocViewer.create(document.getElementById("udoc")!);
// Decode the image with a canvas: `pixels` are its raw samples, not a PNG file.
const bitmap = await createImageBitmap(await (await fetch("/logo@2x.png")).blob());
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const context = canvas.getContext("2d")!;
context.drawImage(bitmap, 0, 0);
const { data } = context.getImageData(0, 0, bitmap.width, bitmap.height);
await viewer.setWatermarks([
{
// A logo made at twice its size: `scale: 2` draws it at half its pixels.
content: { image: { pixels: data, width: bitmap.width, height: bitmap.height, scale: 2 } },
opacity: 1,
// Only the first page.
target: { pages: { plane: 0, indices: [0] } },
// 24 points in from the top and from the right.
placement: { at: { anchor: "topRight", offsetX: -24, offsetY: 24, rotation: 0 } },
},
]);Which pages
target: "all"draws on every page.target: { pages: { plane: 0, indices: [0, 3] } }draws on those pages, counted from 0. A page the document doesn't have is passed over.
plane is 0, except in a workbook, where each sheet is a plane: plane: 2 is the third sheet.
Where it lands
Everything is measured on the page as it is shown, in points, with x to the right and y down. On a page turned a quarter turn, the top right is the top right on screen. Anything past the page's edge is cut off.
One copy: at
placement: {
at: {
(anchor, offsetX, offsetY, rotation);
}
}These four steps happen in this order:
- Rotate the watermark about its own centre by
rotationdegrees. Positive is clockwise, so-45is the diagonal rising to the right. - Take its bounding box: the upright rectangle just holding the rotated watermark. With no rotation, that is the watermark's own box.
- Align the bounding box to the page by
anchor, which names the same point on both."center"puts its centre on the page's centre, never its corner, and"topRight"puts its top-right corner on the page's top-right corner. - Move it by
offsetXto the right andoffsetYdown. Negative goes the other way.
anchor is one of "topLeft", "top", "topRight", "left", "center", "right", "bottomLeft", "bottom", "bottomRight".
center, (0, 0) topRight, (0, 0) topRight, (-24, 24) bottomLeft, (24, -24)
┌────────────┐ ┌────────┬───┐ ┌────────────┐ ┌────────────┐
│ │ │ │ W │ │ 24↓ │ │ │
│ ┌───┐ │ │ └───┤ │ ┌───┐ │ │ │
│ │ W │ │ │ │ │ │ W │←24 │24→┌───┐ │
│ └───┘ │ │ │ │ └───┘ │ │ │ W │ │
│ │ │ │ │ │ │ └───┘ │
└────────────┘ └────────────┘ └────────────┘ └─────24↑────┘To keep a watermark inside the page from a corner, the offset has to point inward, which is why it is (-24, 24) at the top right and (24, -24) at the bottom left. Because it is the rotated bounding box that aligns, a watermark at a corner stays whole on the page at any rotation, as long as it fits.
For a watermark 100 points wide and 50 tall on a 600 by 800 page:
anchor | offsetX, offsetY | rotation | Where it lands |
|---|---|---|---|
"center" | 0, 0 | 0 | x 250–350, y 375–425 |
"center" | 0, 0 | 90 | Its centre stays at (300, 400); x 275–325, y 350–450 |
"topRight" | -24, 24 | 0 | x 476–576, y 24–74 |
"bottomLeft" | 0, 0 | 90 | x 0–50, y 700–800 |
Copies across the page: tile
placement: {
tile: {
(gapX, gapY, rotation);
}
}- Rotate the watermark about its own centre and take its bounding box, as above.
- The first copy's bounding box stands in the page's top-left corner.
- Each next copy in a row stands
gapXto the right of the one before, and each next rowgapYbelow. The gap runs from one bounding box to the next, not from centre to centre, and can't be negative.
Each copy turns on its own centre, so the rows stay straight. A watermark 100 by 50 with gapX: 50, gapY: 30 and no rotation has copies at x 0, 150, 300, … and y 0, 80, 160, ….
Spreadsheets and long pages
A worksheet's grid is far larger than the part in use, so for at the "page" is its used range, from A1: "center" is the centre of the data, and moves as the data grows. tile fills the whole grid, so use it to keep a watermark in view as a sheet scrolls. Use it the same way for a document shown without page breaks, which is one long page.