How to Securely Add Page and Image Watermarks with Canvas and CSS
This article explains how to protect confidential business documents by implementing page‑level watermarks using canvas and pseudo‑elements, and how to add robust watermarks to images via OSS parameters or client‑side canvas processing, while preventing removal through MutationObserver.
Background
In daily operations, the company distributes internal documents (recipes, cleaning standards, etc.) to franchisees, which must not be leaked. To protect these assets, both text‑based pages and images need watermarks.
Page‑level watermarks prevent unauthorized copying of the whole page.
Image watermarks protect saved pictures from being used without the mark.
Page Watermark
Design Options
Option 1: Fixed‑position div elements (creates many DOM nodes).
Option 2: Fixed‑position canvas element (creates a canvas node).
Option 3: Canvas + pseudo‑class (no extra elements, good compatibility).
Option 4: SVG + pseudo‑class (no extra elements, slightly poorer compatibility).
All options can be removed by deleting the element or its class name. Considering cost and security, the final choice is Option 3 (canvas + pseudo‑class) , supplemented with a MutationObserver to block class‑name removal.
Core functions:
Generate a background image from signature data using Canvas.
Apply the generated image to a pseudo‑element covering the target area.
Use MutationObserver to restore the class if it is removed.
Code Implementation
Generate Canvas Background
interface IImgOptions {
content: string[]; // watermark texts
canvasHeight: number;
canvasWidth: number;
}
const createImgBase = (options: IImgOptions) => {
const { content, canvasHeight, canvasWidth } = options;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = canvasHeight;
canvas.height = canvasWidth;
if (ctx) {
ctx.rotate((-10 * Math.PI) / 180);
ctx.fillStyle = 'rgba(100,100,100,0.2)';
ctx.font = '40px';
content.forEach((text, index) => {
ctx.fillText(text, 10, 30 * (index + 1));
});
}
return canvas.toDataURL('image/png');
};Apply Pseudo‑Element
const genWaterMark = ({
content,
className,
canvasHeight = 140,
canvasWidth = 150,
}) => {
const dataURL = createImgBase({ content, canvasHeight, canvasWidth });
const defaultStyle = document.createElement('style');
defaultStyle.innerHTML = `.${className}::after {
content: '';
display: block;
width: 100%;
height: 100vh;
background-image: url(${dataURL});
background-repeat: repeat;
pointer-events: none;
position: fixed;
top: 0;
left: 0;
}`;
document.head.appendChild(defaultStyle);
};
// usage in a React component
const Content = () => {
useEffect(() => {
genWaterMark({
content: [userName, userPhone, '内部机密材料', '严禁外泄!'],
className: 'my-page-container',
});
}, []);
return (
<div className="my-page-container" id="my-page-container">
<div className="my-info">
<div className="title">这是测试标题</div>
<div className="content">// ...机密内容</div>
</div>
</div>
);
};Prevent Class Removal with MutationObserver
const listenerDOMChange = (className) => {
const targetNode = document.querySelector(`.${className}`);
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'class' &&
targetNode
) {
const curClassVal = targetNode.getAttribute('class') || '';
if (curClassVal.indexOf(className) === -1) {
targetNode.setAttribute('class', `${className} ${curClassVal}`);
}
}
}
});
observer.observe(targetNode, { attributes: true });
};
const genWaterMark = ({ content, className, canvasHeight = 140, canvasWidth = 150 }) => {
listenerDOMChange(className);
const dataURL = createImgBase({ content, canvasHeight, canvasWidth });
const defaultStyle = document.createElement('style');
// style content omitted for brevity
document.head.appendChild(defaultStyle);
};Notes
Full‑page watermarks may need to be limited to specific regions; use position: absolute in the pseudo‑element to target a container.
const genWaterMark = ({ content, className, canvasHeight = 140, canvasWidth = 150 }) => {
const dataURL = createImgBase({ content, canvasHeight, canvasWidth });
const defaultStyle = document.createElement('style');
defaultStyle.innerHTML = `.${className}::after {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-image: url(${dataURL});
background-repeat: repeat;
pointer-events: none;
}`;
document.head.appendChild(defaultStyle);
};Image Watermark
Design Options
Option 1: Server‑side watermark (secure but heavy load).
Option 2: OSS watermark (convenient but not universal).
Option 3: Canvas watermark (secure but slower).
The article focuses on the latter two front‑end solutions.
OSS Watermark
Convert OSS URL to Watermarked URL
// Encode watermark text for OSS URL
const getSafeBase64Code = (name) => {
return window
.btoa(unescape(encodeURIComponent(name)))
.replace(/+/g, '-')
.replace(/\//g, '_');
};
const genOSSImageWaterMark = (imgSrc) => {
const { userName, userPhone } = JSON.parse(window.localStorage.getItem('userInfo') || '{}');
return `${imgSrc}?x-oss-process=image/watermark,text_${getSafeBase64Code(`${userName}-${userPhone}`)},rotate_325,t_10,color_000000,size_30,fill_1,g_nw,x_30,y_30`;
};
// usage in React
const ImageWaterMark = () => (
<Image src={genOSSImageWaterMark('xxx图片地址xxx')} />
);Handling Transparent PNGs and Font Size
const genOSSImageWaterMark = (imgSrc) => {
const imgType = imgSrc.split('.').slice(-1)[0];
const { userName, userPhone } = JSON.parse(window.localStorage.getItem('userInfo') || '{}');
return `${imgSrc}?${imgType !== 'jpg' ? 'x-oss-process=image/format,jpg,' : ''}x-oss-process=image/watermark,text_${getSafeBase64Code(`${userName}-${userPhone}`)},rotate_325,t_10,color_000000,size_30,fill_1,g_nw,x_30,y_30`;
};Dynamic Font Size Based on Image Ratio
const getImageWH = async (src) => {
const img = new Image();
img.src = src;
await new Promise((resolve) => (img.onload = resolve));
return { width: img.width, height: img.height };
};
const genOSSImageWaterMark = async (imgSrc) => {
const { userName, userPhone } = JSON.parse(window.localStorage.getItem('userInfo') || '{}');
const imgType = imgSrc.split('.').slice(-1)[0];
const { width, height } = await getImageWH(imgSrc);
const min = Math.min(width, height);
const size = Math.ceil(min / 40); // adjust font size
return `${imgSrc}?${imgType !== 'jpg' ? 'x-oss-process=image/format,jpg,' : ''}x-oss-process=image/watermark,text_${getSafeBase64Code(`${userName}-${userPhone}`)},rotate_325,t_100,color_ff0000,size_${size},fill_1,g_nw,x_30,y_30`;
};Canvas‑Based Image Watermark
Workflow
Convert image URL to a canvas.
Draw watermark text on the canvas.
Export the canvas as a data URL.
const genOSSImageWaterMark = async (imgSrc) => {
const canvas = document.createElement('canvas');
await imgSrc2Canvas(canvas, imgSrc);
addWatermark(canvas);
return canvas.toDataURL('image/png');
};Helper: Image to Canvas
const imgSrc2Canvas = (cav, imgSrc) => {
return new Promise(async (resolve) => {
const image = new Image();
image.src = imgSrc;
image.setAttribute('crossOrigin', 'anonymous');
await new Promise((resolve) => (image.onload = resolve));
cav.width = image.width;
cav.height = image.height;
const ctx = cav.getContext('2d');
if (ctx) ctx.drawImage(image, 0, 0);
resolve(cav);
});
};Draw Text Watermark
const addWatermark = async (canvas) => {
const { userName, userPhone } = JSON.parse(window.localStorage.getItem('userInfo') || '{}');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgba(100,100,100,0.2)';
ctx.font = '24px serif';
ctx.rotate((5 * Math.PI) / 180);
const repeatX = Math.floor(canvas.width / 240);
const repeatY = Math.floor(canvas.height / 150);
for (let i = 0; i < repeatX; i++) {
for (let j = 1; j < repeatY; j++) {
ctx.fillText(`${userName}-${userPhone}`, 240 * 2 * i, 150 * j);
}
}
};Common Issues
Cross‑origin images pollute the canvas; set crossOrigin='anonymous' on the image element.
Attempting to export the canvas before the image loads results in a transparent output; wait for onload before drawing.
// Set crossOrigin to avoid canvas tainting
image.setAttribute('crossOrigin', 'anonymous');
// Wait for image load before using canvas
await new Promise((resolve) => (image.onload = resolve));Summary
The article covers two main topics: page watermarks and image watermarks. Page watermarks are straightforward—use canvas to generate a background image and apply it via a pseudo‑element. Image watermarks are more involved; they require either OSS‑based processing or client‑side canvas manipulation, with attention to performance, format conversion, and cross‑origin restrictions. For high security, server‑side watermarking is preferred, but front‑end solutions are viable when OSS is used.
Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
