DEV Community

toolzip
toolzip

Posted on

How to Watermark Images in Your Browser — No Upload, No Software

Watermarking images used to mean opening Photoshop, setting up a text layer, adjusting opacity, flattening, and exporting. For a single image, that's five minutes. For fifty images, it's an afternoon.

Here's how to do it entirely in your browser using the Canvas API — no server uploads, no software, no account required.

Why Watermark Images?

A digital watermark serves several purposes depending on who's using it:

Copyright protection. Once an image is online, it can be copied and reused. A visible watermark doesn't prevent copying, but it deters casual theft and makes attribution obvious when images spread beyond their original context.

Branding. YouTubers, bloggers, and photographers add their channel name or website URL to every image. When content gets shared, the watermark travels with it.

Portfolio protection. Photographers and designers showing work to potential clients add prominent watermarks over their best work. Buyers can see the quality without getting a usable file.

Sample image protection. Stock photo sites overlay visible watermarks on previews. The watermark is integral to the business model.

The Privacy Problem with Upload-Based Tools

Most online watermarking tools work the same way: you upload the image, their server applies the watermark, you download the result.

This is fine for stock photos or public images. It becomes a problem when the image contains unreleased product shots, client work under NDA, private photos, or any content you wouldn't want stored on a third-party server, even temporarily.

Browser-based processing avoids this entirely. The file never leaves the device.

How Browser Watermarking Works

The Canvas API makes this straightforward. The core process:

  1. Draw the original image onto a canvas
  2. Draw the watermark (text or image) on top
  3. Export the canvas as a new image file
async function addTextWatermark(imageFile, text, options = {}) {
  const {
    position = 'bottom-right',
    fontSize = 24,
    color = 'rgba(255, 255, 255, 0.7)',
    fontFamily = 'Arial',
    padding = 20,
  } = options;

  // Load original image
  const img = await loadImage(imageFile);

  const canvas = document.createElement('canvas');
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;
  const ctx = canvas.getContext('2d');

  // Draw original image
  ctx.drawImage(img, 0, 0);

  // Configure text style
  ctx.font = `${fontSize}px ${fontFamily}`;
  ctx.fillStyle = color;
  ctx.textBaseline = 'bottom';

  // Add shadow for visibility on any background
  ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
  ctx.shadowBlur = 4;
  ctx.shadowOffsetX = 1;
  ctx.shadowOffsetY = 1;

  // Calculate position
  const textWidth = ctx.measureText(text).width;
  let x, y;

  switch (position) {
    case 'top-left':     x = padding; y = fontSize + padding; break;
    case 'top-right':    x = canvas.width - textWidth - padding; y = fontSize + padding; break;
    case 'bottom-left':  x = padding; y = canvas.height - padding; break;
    case 'bottom-right': x = canvas.width - textWidth - padding; y = canvas.height - padding; break;
    case 'center':       x = (canvas.width - textWidth) / 2; y = canvas.height / 2; break;
  }

  ctx.fillText(text, x, y);

  return canvas;
}

function loadImage(file) {
  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.src = URL.createObjectURL(file);
  });
}
Enter fullscreen mode Exit fullscreen mode

Adding a Logo Watermark

Logo watermarks require PNG files with transparent backgrounds. The alpha channel in the PNG becomes the transparency when composited.

async function addLogoWatermark(imageFile, logoFile, options = {}) {
  const {
    position = 'bottom-right',
    opacity = 0.7,
    scale = 0.15, // Logo width as fraction of image width
    padding = 20,
  } = options;

  const [img, logo] = await Promise.all([
    loadImage(imageFile),
    loadImage(logoFile),
  ]);

  const canvas = document.createElement('canvas');
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;
  const ctx = canvas.getContext('2d');

  // Draw original image
  ctx.drawImage(img, 0, 0);

  // Calculate logo dimensions
  const logoWidth = canvas.width * scale;
  const logoHeight = (logo.naturalHeight / logo.naturalWidth) * logoWidth;

  // Calculate position
  let x, y;
  switch (position) {
    case 'bottom-right':
      x = canvas.width - logoWidth - padding;
      y = canvas.height - logoHeight - padding;
      break;
    case 'center':
      x = (canvas.width - logoWidth) / 2;
      y = (canvas.height - logoHeight) / 2;
      break;
    // ... other positions
  }

  // Apply opacity and draw logo
  ctx.globalAlpha = opacity;
  ctx.drawImage(logo, x, y, logoWidth, logoHeight);
  ctx.globalAlpha = 1.0;

  return canvas;
}
Enter fullscreen mode Exit fullscreen mode

Tiled Watermarks

For maximum protection (the kind used on high-value previews), repeat the watermark across the entire image:

function addTiledWatermark(canvas, text, options = {}) {
  const {
    fontSize = 20,
    color = 'rgba(255, 255, 255, 0.3)',
    rotation = -30, // degrees
    spacing = 150,
  } = options;

  const ctx = canvas.getContext('2d');
  ctx.font = `${fontSize}px Arial`;
  ctx.fillStyle = color;

  const angleRad = (rotation * Math.PI) / 180;
  const textWidth = ctx.measureText(text).width;

  // Save context state
  ctx.save();

  // Rotate and tile
  for (let y = -canvas.height; y < canvas.height * 2; y += spacing) {
    for (let x = -canvas.width; x < canvas.width * 2; x += spacing + textWidth) {
      ctx.save();
      ctx.translate(x, y);
      ctx.rotate(angleRad);
      ctx.fillText(text, 0, 0);
      ctx.restore();
    }
  }

  ctx.restore();
}
Enter fullscreen mode Exit fullscreen mode

Batch Processing Multiple Files

For applying the same watermark to many images:

async function batchWatermark(files, watermarkConfig) {
  const results = [];

  for (const file of files) {
    const canvas = await addTextWatermark(file, watermarkConfig.text, watermarkConfig);

    // Convert to blob
    const blob = await new Promise(resolve => {
      canvas.toBlob(resolve, 'image/jpeg', 0.92);
    });

    results.push({
      name: `watermarked_${file.name}`,
      blob,
      url: URL.createObjectURL(blob),
    });
  }

  return results;
}
Enter fullscreen mode Exit fullscreen mode

Watermark Placement Considerations

Contrast matters: A white watermark is invisible on a white sky. Using a slight shadow (ctx.shadowColor) helps visibility on any background.

Size vs. intrusiveness: Watermarks too small to read serve no protective purpose. The sweet spot is typically 15–25% of the image width for logo watermarks.

Corner placement: Bottom-right is the convention most people expect. It's also easiest to crop out. Bottom-center is harder to remove without noticeably altering the image.

Central placement: Maximum protection, maximum intrusiveness. Reserve for sample images where the watermark is the whole point.

Try It

ToolZip's image watermark tool handles all of this in your browser — text watermarks with font, size, color, and opacity controls; logo watermarks via PNG upload; batch processing; nine position options.

toolzip.app/tools/image-watermark


ToolZip — 48 free browser-based tools. Everything runs client-side.

Top comments (0)