"chrome.tabs.captureVisibleTab gives you the whole tab as a dataURL. The Selection rect tells you which pixels matter. Lesson 4 is wiring those two together so every ClipDeck clip can carry a tightly-cropped screenshot of where it came from."
chrome.tabs.captureVisibleTab
The API is small. Two arguments:
windowId— optional. Captures the active tab of the given window; omit to capture the current window.options— optional.{ format: 'png' | 'jpeg', quality: 0..100 }. Default is PNG. JPEG with quality 80 is usually 5x smaller for screenshots of text-heavy pages.
Returns a data: URL. The capture covers only the currently-visible viewport — what the user sees. Off-screen content is not captured.
Permission: activeTab (after user gesture) OR host_permissions for the URL OR the tabs permission. ClipDeck has all three relevant ones at this point.
The Crop Workflow
Captured image is full-tab. ClipDeck wants the rect of the user's selection. The math:
- Get the selection's
getBoundingClientRect()from the content script — viewport-relative coordinates. - Get the device pixel ratio (
window.devicePixelRatio). Captured images are at native resolution; the rect is in CSS pixels. - SW receives the dataURL + rect + dpr. Load into an offscreen canvas. Draw cropped at the right pixel coordinates.
- Export the cropped canvas as another dataURL. Store with the clip.
Offscreen Document for Canvas
Service workers can't use the DOM, so they can't create a normal canvas. MV3 introduced chrome.offscreen — a hidden document the SW spawns when it needs DOM APIs (canvas, fetch with Blob handling, audio). The shape:
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['BLOBS'],
justification: 'Crop captured tab screenshots for ClipDeck clips',
});
offscreen.html is a tiny page with offscreen.js that receives messages from the SW, does the canvas work, and posts the result back. After use, call chrome.offscreen.closeDocument() — keeping it open wastes a bit of memory.
The Crop Code
Inside the offscreen document:
const img = new Image();
img.src = dataURL;
await new Promise((r) => (img.onload = r));
const canvas = new OffscreenCanvas(
Math.round(rect.width * dpr),
Math.round(rect.height * dpr),
);
const ctx = canvas.getContext('2d');
ctx.drawImage(
img,
rect.left * dpr, rect.top * dpr,
rect.width * dpr, rect.height * dpr,
0, 0,
rect.width * dpr, rect.height * dpr,
);
const blob = await canvas.convertToBlob({ type: 'image/png' });
const reader = new FileReader();
reader.readAsDataURL(blob);
await new Promise((r) => (reader.onload = r));
return reader.result; // the cropped dataURL
OffscreenCanvas is supported in the offscreen document and gives you a Worker-friendly canvas. The 9-argument drawImage form: sx, sy, sw, sh, dx, dy, dw, dh. Source coordinates come from the rect; destination starts at 0,0.
Size Budget
Each PNG screenshot of a typical text selection is ~10–50 KB. PNG is fine for text; JPEG quality 80 cuts another 30–50%. Store dataURLs directly in chrome.storage.local — the quota is around 10 MB, plenty for a few hundred screenshots if you keep them cropped. If clip counts grow, consider compressing aggressively, switching to JPEG, or letting the user opt-out of screenshots per clip.
What Goes in the Clip
Extend the clip schema:
{ id, text, url, title, savedAt, screenshot?: string /* dataURL */ }
The side panel renders the screenshot inline if present (small thumbnail; click to enlarge). The export flow includes it; the clipboard copy still only gets the text.