Browser-Side Image Format Conversion: Canvas toBlob Quality Parameter and Color Space Pitfalls#

I recently built an image format converter and discovered that browser-side image conversion, while seemingly simple, has many technical details worth discussing. This article covers the conversion principles for PNG/JPEG/WebP formats and the pitfalls I encountered.

The Core of Image Format Conversion: Canvas toBlob#

The core API for browser-side image format conversion is canvas.toBlob():

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')

// 1. Load original image
const img = new Image()
img.onload = () => {
  canvas.width = img.width
  canvas.height = img.height
  ctx.drawImage(img, 0, 0)

  // 2. Convert format
  canvas.toBlob(
    (blob) => {
      if (!blob) return
      const url = URL.createObjectURL(blob)
      // 3. Trigger download
      const a = document.createElement('a')
      a.href = url
      a.download = `converted.${format}`
      a.click()
      URL.revokeObjectURL(url)  // Remember to release memory
    },
    `image/${format}`,  // MIME type
    quality / 100       // Quality parameter (only effective for lossy formats)
  )
}
img.src = originalImageBase64

The core logic is three steps: load original image → Canvas draw → toBlob export. But each step has pitfalls.

PNG to JPEG: The Transparency Channel Trap#

PNG supports Alpha transparency channels, but JPEG does not. If you directly convert a PNG with transparency to JPEG, the transparent areas become black:

// ❌ Wrong: Transparent areas turn black
ctx.drawImage(pngImage, 0, 0)
canvas.toBlob(callback, 'image/jpeg', 0.9)

The solution is to fill a white background before conversion:

// ✅ Correct: Draw white background first
ctx.fillStyle = '#FFFFFF'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(pngImage, 0, 0)

This issue is easily overlooked because most test images are opaque. However, in practice, user-uploaded PNGs often have transparent areas.

The Truth About Quality Parameter: Only Effective for Lossy Formats#

The third parameter of toBlob is quality, ranging from 0-1. But this parameter only works for lossy formats:

Format Quality Parameter Lossy? Typical Size
PNG ❌ Ignored Lossless Largest
JPEG ✅ Effective Lossy Medium
WebP ✅ Effective Optional Smallest

PNG is a lossless format—no matter what quality value you pass, the output is the same. The quality parameter only matters for JPEG and WebP:

// JPEG quality 90% (recommended)
canvas.toBlob(callback, 'image/jpeg', 0.9)

// WebP quality 85%
canvas.toBlob(callback, 'image/webp', 0.85)

// PNG ignores quality parameter
canvas.toBlob(callback, 'image/png')  // Any value after second param is ignored

Recommended quality settings:

  • JPEG: 85-95% balances quality and size; below 80% shows noticeable artifacts
  • WebP: 80-90%; 25-35% smaller than JPEG at equivalent quality
  • PNG: No need to set—it’s already lossless

Browser Compatibility: WebP Support#

WebP has the best compression efficiency, but compatibility requires attention:

// Check if browser supports WebP
async function supportsWebP(): Promise<boolean> {
  const canvas = document.createElement('canvas')
  canvas.width = 1
  canvas.height = 1
  const blob = await new Promise<Blob | null>(resolve =>
    canvas.toBlob(resolve, 'image/webp')
  )
  return blob !== null && blob.size > 0
}

Compatibility data (2026):

  • Chrome 32+ ✅
  • Firefox 65+ ✅
  • Safari 14+ ✅
  • Edge 18+ ✅

All mainstream browsers now support WebP, but if you need to support older Safari (iOS 13 and below), you’ll still need a JPEG fallback.

EXIF Metadata Loss Problem#

When converting images using Canvas, all EXIF metadata is lost. This includes:

  • Camera equipment info (model, lens)
  • GPS location data
  • Capture timestamp
  • Aperture, shutter speed, ISO settings

The reason is simple: Canvas only stores pixel data. EXIF information is stored in the file header, and drawImage only extracts pixels.

If you need to preserve EXIF, you must manually extract and re-inject it:

// Extract EXIF (requires exif-js or similar)
import EXIF from 'exif-js'

const exifData = await EXIF.getData(imageFile)

// Convert image...
const blob = await convertImage(imageFile, 'jpeg')

// Re-inject EXIF (requires piexifjs or similar)
import piexif from 'piexifjs'
const exifBytes = piexif.dump(exifData)
const newBlob = piexif.insert(exifBytes, blob)

Most online tools ignore this issue, but if you need professional-grade image conversion, this step is essential.

Large Image Performance Optimization#

When image dimensions exceed 4096px, conversion can be slow or even crash the browser. Several optimization strategies:

1. Detect image dimensions, provide warnings#

img.onload = () => {
  const pixels = img.width * img.height
  if (pixels > 16_000_000) {  // 16MP
    console.warn('Large image may take longer to process')
  }
  if (pixels > 50_000_000) {  // 50MP
    alert('Image too large, recommend compressing or cropping first')
    return
  }
  // Continue processing...
}

2. Use Web Worker for background processing#

// worker.ts
self.onmessage = (e) => {
  const { imageData, format, quality } = e.data
  const canvas = new OffscreenCanvas(imageData.width, imageData.height)
  const ctx = canvas.getContext('2d')
  ctx.putImageData(imageData, 0, 0)

  canvas.convertToBlob({ type: `image/${format}`, quality })
    .then(blob => self.postMessage({ success: true, blob }))
    .catch(err => self.postMessage({ success: false, error: err.message }))
}

OffscreenCanvas runs in Web Worker without blocking the main thread. Compatibility is good (Chrome 69+, Firefox 105+, Safari 16.4+).

Format Selection Recommendations#

Choose format based on use case:

Scenario Recommended Format Reason
Photos JPEG 90% Small size, acceptable quality
Icons/Screenshots PNG Sharp edges, supports transparency
Web images WebP 85% Smallest size, modern browser support
Photos needing transparency WebP 50%+ smaller than PNG

A practical decision tree:

function suggestFormat(hasTransparency: boolean, isPhoto: boolean): string {
  if (hasTransparency && isPhoto) return 'webp'  // Photos with transparency
  if (hasTransparency) return 'png'              // Icons, screenshots
  if (isPhoto) return 'jpeg'                     // Regular photos
  return 'png'                                   // Default
}

Real-World Implementation#

Based on these principles, I built an online tool: Image Format Converter

Key features:

  • Supports PNG/JPEG/WebP format conversion
  • Adjustable quality parameter for JPEG/WebP
  • Automatic PNG transparency channel handling
  • Real-time conversion preview

The code isn’t complex, but handling these details well significantly improves user experience.


Related tools: Image Compress | Image Crop