Chrome Extension Media Handling: Image Upload, Processing and Screenshot Capture (2026)

AppBooster Team · · 9 min read
Image editing and media processing on screen

Media handling is one of the more nuanced areas of Chrome extension development. Whether you’re building a screenshot tool, an image editor, a design utility, or a productivity extension that processes user-uploaded files, you’ll hit the same set of challenges: capturing page content, processing pixels efficiently, handling user input across drag-and-drop and clipboard, and writing files to disk cleanly. This guide covers each of these in depth with working code.

1. Screenshot Capture with chrome.tabs.captureVisibleTab

The most direct way to capture a screenshot in a Chrome extension is chrome.tabs.captureVisibleTab. It captures the visible portion of the currently active tab and returns a base64-encoded data URL.

// manifest.json - required permission
// "permissions": ["activeTab", "tabs"]

async function captureVisibleTab() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

  const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
    format: "png",  // or "jpeg"
    quality: 92,    // only applies to jpeg
  });

  return dataUrl; // "data:image/png;base64,..."
}

One important constraint: captureVisibleTab only works from a background service worker or extension page (popup, options page). You cannot call it from a content script. If you need to trigger capture from a content script, send a message to the service worker:

// content-script.js
chrome.runtime.sendMessage({ type: "CAPTURE_TAB" }, (response) => {
  console.log("Screenshot data URL:", response.dataUrl);
});

// service-worker.js
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === "CAPTURE_TAB") {
    chrome.tabs.captureVisibleTab(sender.tab.windowId, { format: "png" })
      .then((dataUrl) => sendResponse({ dataUrl }));
    return true; // keep message channel open for async response
  }
});

2. Full-Page Screenshot Techniques

Capturing beyond the visible viewport requires a scrolling capture approach. The idea: scroll the page in increments, capture each viewport, then stitch the images together on a canvas.

// service-worker.js
async function captureFullPage(tabId) {
  // Get page dimensions from content script
  const [{ result }] = await chrome.scripting.executeScript({
    target: { tabId },
    func: () => ({
      scrollHeight: document.documentElement.scrollHeight,
      scrollWidth: document.documentElement.scrollWidth,
      viewportHeight: window.innerHeight,
      viewportWidth: window.innerWidth,
      devicePixelRatio: window.devicePixelRatio,
    }),
  });

  const { scrollHeight, viewportHeight, viewportWidth, devicePixelRatio } = result;
  const captures = [];
  let scrollY = 0;

  while (scrollY < scrollHeight) {
    // Scroll to position
    await chrome.scripting.executeScript({
      target: { tabId },
      func: (y) => window.scrollTo(0, y),
      args: [scrollY],
    });

    // Small delay to allow repaint
    await new Promise((r) => setTimeout(r, 150));

    const dataUrl = await chrome.tabs.captureVisibleTab({ format: "png" });
    captures.push({ dataUrl, scrollY });

    scrollY += viewportHeight;
  }

  // Restore scroll position
  await chrome.scripting.executeScript({
    target: { tabId },
    func: () => window.scrollTo(0, 0),
  });

  return stitchCaptures(captures, {
    viewportWidth,
    viewportHeight,
    scrollHeight,
    devicePixelRatio,
  });
}

Stitching happens on an OffscreenCanvas (covered in the next section) to avoid blocking the UI thread.

3. Image Processing with Canvas API and OffscreenCanvas

For any non-trivial image manipulation — cropping, compositing, color adjustment, annotation — reach for the Canvas API. In service workers where there is no DOM, use OffscreenCanvas.

Converting a data URL to ImageBitmap:

async function dataUrlToImageBitmap(dataUrl) {
  const res = await fetch(dataUrl);
  const blob = await res.blob();
  return createImageBitmap(blob);
}

Stitching multiple viewport screenshots:

async function stitchCaptures(captures, { viewportWidth, viewportHeight, scrollHeight, devicePixelRatio }) {
  const scale = devicePixelRatio;
  const canvas = new OffscreenCanvas(viewportWidth * scale, scrollHeight * scale);
  const ctx = canvas.getContext("2d");

  for (const { dataUrl, scrollY } of captures) {
    const bitmap = await dataUrlToImageBitmap(dataUrl);
    ctx.drawImage(bitmap, 0, scrollY * scale);
    bitmap.close();
  }

  const blob = await canvas.convertToBlob({ type: "image/png" });
  return URL.createObjectURL(blob);
}

Cropping a region from a screenshot:

function cropImage(sourceCanvas, x, y, width, height) {
  const canvas = new OffscreenCanvas(width, height);
  const ctx = canvas.getContext("2d");
  ctx.drawImage(sourceCanvas, x, y, width, height, 0, 0, width, height);
  return canvas;
}

OffscreenCanvas is available in service workers in Chrome 69+. Use it whenever you process images in the background to keep your extension responsive.

4. Drag-and-Drop Image Upload in Popups

Extension popups are standard HTML pages, so the File API and drag-and-drop events work exactly as they do on the web.

<!-- popup.html -->
<div id="drop-zone" class="drop-zone">
  <p>Drop image here or <label for="file-input">browse</label></p>
  <input type="file" id="file-input" accept="image/*" hidden />
</div>
// popup.js
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("file-input");

dropZone.addEventListener("dragover", (e) => {
  e.preventDefault();
  dropZone.classList.add("drag-over");
});

dropZone.addEventListener("dragleave", () => {
  dropZone.classList.remove("drag-over");
});

dropZone.addEventListener("drop", (e) => {
  e.preventDefault();
  dropZone.classList.remove("drag-over");
  const file = e.dataTransfer.files[0];
  if (file && file.type.startsWith("image/")) {
    processImageFile(file);
  }
});

fileInput.addEventListener("change", () => {
  const file = fileInput.files[0];
  if (file) processImageFile(file);
});

async function processImageFile(file) {
  const bitmap = await createImageBitmap(file);
  console.log(`Image: ${bitmap.width}x${bitmap.height}, type: ${file.type}`);
  // Pass bitmap to your canvas pipeline
}

One popup-specific gotcha: popups close when they lose focus. If you open a file picker dialog, the popup may close before the user selects a file. Work around this by opening a dedicated extension page (chrome.windows.create) for any file-heavy workflows instead of using the popup.

5. Image Compression and Format Conversion to WebP

Reducing image file size before upload or storage is straightforward with the Canvas API. WebP typically achieves 25–34% smaller file sizes than PNG at equivalent quality.

async function convertToWebP(inputBlob, quality = 0.85) {
  const bitmap = await createImageBitmap(inputBlob);
  const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
  const ctx = canvas.getContext("2d");
  ctx.drawImage(bitmap, 0, 0);
  bitmap.close();

  return canvas.convertToBlob({
    type: "image/webp",
    quality,
  });
}

async function resizeAndCompress(inputBlob, maxWidth = 1280, quality = 0.85) {
  const bitmap = await createImageBitmap(inputBlob);

  const scale = Math.min(1, maxWidth / bitmap.width);
  const width = Math.round(bitmap.width * scale);
  const height = Math.round(bitmap.height * scale);

  const canvas = new OffscreenCanvas(width, height);
  const ctx = canvas.getContext("2d");
  ctx.drawImage(bitmap, 0, 0, width, height);
  bitmap.close();

  const blob = await canvas.convertToBlob({ type: "image/webp", quality });
  console.log(`Compressed: ${inputBlob.size} → ${blob.size} bytes`);
  return blob;
}

For very large images, consider processing in chunks or using createImageBitmap with a resizeWidth option to offload the scaling work:

const bitmap = await createImageBitmap(inputBlob, {
  resizeWidth: 1280,
  resizeQuality: "high",
});

6. Clipboard Image Handling (Paste from Clipboard)

Many power users expect to paste screenshots directly from their clipboard. The Clipboard API makes this straightforward in extension pages.

// popup.js or options page
document.addEventListener("paste", async (e) => {
  const items = e.clipboardData?.items;
  if (!items) return;

  for (const item of items) {
    if (item.type.startsWith("image/")) {
      const blob = item.getAsFile();
      if (blob) {
        await handleClipboardImage(blob);
        break;
      }
    }
  }
});

// Programmatic clipboard read (requires "clipboardRead" permission)
async function readImageFromClipboard() {
  try {
    const items = await navigator.clipboard.read();
    for (const item of items) {
      const imageType = item.types.find((t) => t.startsWith("image/"));
      if (imageType) {
        const blob = await item.getType(imageType);
        return blob;
      }
    }
  } catch (err) {
    console.error("Clipboard read failed:", err);
  }
  return null;
}

Add "clipboardRead" to your permissions array in manifest.json if you’re using the programmatic API. The paste event listener works without extra permissions.

7. Saving Images to Disk with chrome.downloads

Once you have a processed image blob or data URL, saving it to the user’s filesystem is handled by chrome.downloads.download.

// manifest.json: "permissions": ["downloads"]

async function saveImageToDisk(blob, filename = "screenshot.png") {
  const url = URL.createObjectURL(blob);

  const downloadId = await chrome.downloads.download({
    url,
    filename,
    saveAs: true, // prompt user with Save As dialog
    conflictAction: "uniquify",
  });

  // Clean up object URL after download starts
  chrome.downloads.onChanged.addListener(function onChanged({ id, state }) {
    if (id === downloadId && state?.current === "complete") {
      URL.revokeObjectURL(url);
      chrome.downloads.onChanged.removeListener(onChanged);
    }
  });

  return downloadId;
}

// Save without prompting (silent save to Downloads folder)
async function silentSave(blob, filename) {
  const url = URL.createObjectURL(blob);
  await chrome.downloads.download({ url, filename, saveAs: false });
  // Revoke after a short delay since we can't track completion as easily
  setTimeout(() => URL.revokeObjectURL(url), 60_000);
}

Always revoke object URLs after use. Leaked object URLs hold references to blobs in memory and can cause significant memory growth in long-running extensions.

8. Practical Example: Screenshot Annotation Tool

Here’s a minimal but complete annotation tool that ties all the above concepts together. It captures the active tab, lets the user draw on it, and saves the result.

Architecture:

  • Popup triggers capture and opens a dedicated annotation page
  • Annotation page receives the screenshot via chrome.storage.session
  • User draws on a canvas overlay
  • Export saves the merged image
// service-worker.js
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === "START_ANNOTATION") {
    (async () => {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" });

      // Store screenshot temporarily in session storage
      await chrome.storage.session.set({ pendingScreenshot: dataUrl });

      // Open annotation page
      await chrome.windows.create({
        url: chrome.runtime.getURL("annotation.html"),
        type: "popup",
        width: 1024,
        height: 768,
      });

      sendResponse({ ok: true });
    })();
    return true;
  }
});
// annotation.js (runs in annotation.html)
const canvas = document.getElementById("annotation-canvas");
const ctx = canvas.getContext("2d");
let isDrawing = false;
let image = null;

async function init() {
  const { pendingScreenshot } = await chrome.storage.session.get("pendingScreenshot");
  if (!pendingScreenshot) return;

  const bitmap = await dataUrlToImageBitmap(pendingScreenshot);
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  ctx.drawImage(bitmap, 0, 0);
  image = bitmap;

  // Clear from session storage
  await chrome.storage.session.remove("pendingScreenshot");
}

// Drawing tools
canvas.addEventListener("mousedown", (e) => {
  isDrawing = true;
  ctx.beginPath();
  ctx.moveTo(e.offsetX, e.offsetY);
});

canvas.addEventListener("mousemove", (e) => {
  if (!isDrawing) return;
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.strokeStyle = "#ef4444";
  ctx.lineWidth = 3;
  ctx.lineCap = "round";
  ctx.stroke();
});

canvas.addEventListener("mouseup", () => { isDrawing = false; });

// Export
document.getElementById("save-btn").addEventListener("click", async () => {
  canvas.toBlob(async (blob) => {
    const compressed = await convertToWebP(blob, 0.92);
    await saveImageToDisk(compressed, `annotation-${Date.now()}.webp`);
  }, "image/png");
});

async function dataUrlToImageBitmap(dataUrl) {
  const res = await fetch(dataUrl);
  const blob = await res.blob();
  return createImageBitmap(blob);
}

init();

This pattern — capture in the service worker, pass via chrome.storage.session, process in a dedicated page — keeps concerns separated and avoids the popup-closes-on-blur problem entirely.

Key Takeaways

  • Use captureVisibleTab from service workers only; relay through message passing from content scripts.
  • Full-page screenshots require scroll-and-stitch; use OffscreenCanvas to composite without blocking.
  • OffscreenCanvas + convertToBlob is your workhorse for image processing in service workers.
  • Drag-and-drop and paste work in extension pages out of the box — use a dedicated window for file-heavy UIs.
  • Always convert to WebP for storage and upload to minimize size.
  • Revoke object URLs after chrome.downloads.download completes to prevent memory leaks.
  • For multi-step media workflows, chrome.storage.session is the clean way to pass blobs between extension pages without serializing to base64 unnecessarily.

With these building blocks you can implement anything from a quick screenshot grabber to a full in-extension image editor. The APIs are stable, performant, and available without any third-party dependencies.

Share this article

Build better extensions with free tools

Icon generator, MV3 converter, review exporter, and more — no signup needed.

Related Articles