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.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
How to Securely Add Page and Image Watermarks with Canvas and CSS

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.

frontendJavaScriptCanvaswatermarksecurityOSSimage-processing
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.