Canvas Image Watermark Implementation: From Positioning to Tiled Patterns#

Adding watermarks to images is a common requirement—copyright protection, brand visibility, you name it. Recently built an online watermark tool, and here’s how it works under the hood.

The Essence of Watermarks#

It boils down to layering semi-transparent text or graphics over an image. Core code:

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)  // Draw original image first
ctx.fillText('Watermark', x, y)  // Then draw watermark

But when building an actual tool, the details make or break the experience.

Five Positioning Algorithms#

Where should the watermark go? Seems simple, but there are five scenarios:

function getWatermarkPosition(
  canvas: HTMLCanvasElement,
  textWidth: number,
  fontSize: number,
  position: string
) {
  const padding = 20

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

Gotcha 1: Y Coordinate Isn’t the Top of Text

fillText(x, y) uses y as the baseline position, not the top edge. So for a top-left watermark, the Y coordinate should be fontSize + padding, not just padding.

Gotcha 2: Text Width Needs Measuring

Chinese and English characters, different fonts—widths vary wildly. Can’t estimate:

ctx.font = `${fontSize}px sans-serif`
const metrics = ctx.measureText(text)
const textWidth = metrics.width

Only this gives accurate positioning.

Transparency Handling#

Watermarks need semi-transparency to avoid obscuring the original. Two approaches:

Approach 1: globalAlpha#

ctx.globalAlpha = 0.5  // 50% transparency
ctx.fillText('Watermark', x, y)
ctx.globalAlpha = 1  // Reset to avoid affecting subsequent draws

Approach 2: RGBA Color#

ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'  // White, 50% transparent
ctx.fillText('Watermark', x, y)

Key differences:

Approach Use Case Pros Cons
globalAlpha Mixed text and graphics Unified control Affects subsequent draws
rgba color Single-color text No state pollution Only works for single color

My tool uses globalAlpha because it handles future image watermarks better with unified control.

Performance Optimization for Real-Time Preview#

When users adjust parameters, they need instant feedback. But large images can cause lag with constant redrawing.

Solution: Scaled Preview

// Limit preview canvas size
const maxW = 800
const maxH = 600
let w = img.width
let h = img.height

if (w > maxW) {
  h = h * maxW / w
  w = maxW
}
if (h > maxH) {
  w = w * maxH / h
  h = maxH
}

canvas.width = w
canvas.height = h
ctx.drawImage(img, 0, 0, w, h)

Critical: Scale Font Size Too

const scaledFontSize = fontSize * (w / img.width)
ctx.font = `${scaledFontSize}px sans-serif`

The preview image is scaled down, so the font must scale too—otherwise the watermark appears disproportionately large.

When exporting, use original dimensions to maintain quality.

Advanced: Tiled Watermarks#

Single-point watermarks are easy to crop. Tiled patterns are safer. Implementation:

function drawTiledWatermark(
  ctx: CanvasRenderingContext2D,
  text: string,
  canvasWidth: number,
  canvasHeight: number,
  gap: number = 100
) {
  ctx.save()
  ctx.globalAlpha = 0.3
  ctx.font = '20px sans-serif'
  ctx.fillStyle = '#ffffff'

  // Rotate -15 degrees to prevent easy cropping
  ctx.translate(canvasWidth / 2, canvasHeight / 2)
  ctx.rotate((-15 * Math.PI) / 180)
  ctx.translate(-canvasWidth / 2, -canvasHeight / 2)

  const textWidth = ctx.measureText(text).width

  for (let y = 0; y < canvasHeight + gap; y += gap) {
    for (let x = 0; x < canvasWidth + textWidth + gap; x += textWidth + gap) {
      ctx.fillText(text, x, y)
    }
  }

  ctx.restore()
}

Key Points:

  1. Rotation Angle: -15° is common—good anti-cropping effect
  2. Spacing Calculation: X spacing = text width + gap, Y spacing = gap
  3. Boundary Handling: y < canvasHeight + gap ensures edges have watermarks too

Export Format Selection#

canvas.toDataURL() supports three formats:

canvas.toDataURL('image/png')   // Lossless, supports transparency, larger files
canvas.toDataURL('image/jpeg', 0.9)  // Lossy, no transparency, smaller files
canvas.toDataURL('image/webp', 0.9)  // Modern format, smallest size

JPEG doesn’t support transparency—semi-transparent watermarks get a black background. That’s why watermark tools typically use PNG.

But PNG files are large. Give users a choice:

const format = originalFormat === 'image/jpeg' ? 'image/jpeg' : 'image/png'
const url = canvas.toDataURL(format, 0.9)

If the original is JPEG, export as JPEG (with opaque watermark); otherwise use PNG.

Complete Workflow#

function addWatermark(image: HTMLImageElement, options: WatermarkOptions) {
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')

  canvas.width = image.width
  canvas.height = image.height

  // 1. Draw original image
  ctx.drawImage(image, 0, 0)

  // 2. Set watermark style
  ctx.font = `${options.fontSize}px ${options.fontFamily}`
  ctx.fillStyle = options.color
  ctx.globalAlpha = options.opacity

  // 3. Calculate position
  const textWidth = ctx.measureText(options.text).width
  const { x, y } = getWatermarkPosition(canvas, textWidth, options.fontSize, options.position)

  // 4. Draw watermark
  ctx.fillText(options.text, x, y)

  // 5. Export
  return canvas.toDataURL('image/png')
}

Live Tool#

Built an online tool based on these principles: Image Watermark Tool

Features:

  • Five positioning modes
  • Custom font size, color, opacity
  • Real-time preview
  • Drag-and-drop upload

The technical implementation isn’t complex, but nailing the user experience takes thought. Hope this helps!


Related Tools: Image Compress | Image Crop