// background.js — Service worker // Handles: tab screenshot, image crop, OCR API call, result relay const OCR_ENDPOINT = "http://localhost:8000/ocr"; const OCR_MODE = "recognize"; // or "parse" // ── Listen for messages from content.js ───────────────────────────────────── chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.type === "CAPTURE_REGION") { handleCapture(msg.rect, sender.tab) .then(result => sendResponse({ success: true, ...result })) .catch(error => sendResponse({ success: false, error: error.message })); return true; // keep channel open for async } if (msg.type === "PING") { checkServer().then(ok => sendResponse({ ok })); return true; } if (msg.type === "OPEN_SIDEBAR") { // Open the sidebar as a side panel in the current tab chrome.tabs.sendMessage(sender.tab.id, { type: "SHOW_SIDEBAR" }); return false; } }); // ── Capture + crop + OCR ───────────────────────────────────────────────────── async function handleCapture(rect, tab) { // 1. Capture the entire visible tab as a data URL const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png", quality: 100, }); // 2. Crop to the selected rect using OffscreenCanvas const croppedBlob = await cropImage(dataUrl, rect); // 3. Send to GLM-OCR backend const formData = new FormData(); formData.append("file", croppedBlob, "selection.png"); formData.append("mode", OCR_MODE); const res = await fetch(OCR_ENDPOINT, { method: "POST", body: formData, }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || `Server returned ${res.status}`); } const data = await res.json(); // Also store the cropped image as a data URL for display in the sidebar const croppedDataUrl = await blobToDataUrl(croppedBlob); return { text: data.text, word_count: data.word_count, char_count: data.char_count, latency_ms: data.latency_ms, mode: data.mode, device: data.device, imageDataUrl: croppedDataUrl, timestamp: new Date().toISOString(), }; } // ── Image cropping using OffscreenCanvas ───────────────────────────────────── async function cropImage(dataUrl, rect) { // Decode the full screenshot const res = await fetch(dataUrl); const blob = await res.blob(); const bitmap = await createImageBitmap(blob); // Scale rect by device pixel ratio (already baked into captureVisibleTab) // captureVisibleTab captures at device pixel ratio already, so rect coords // from getBoundingClientRect need to be scaled. const dpr = rect.dpr || 1; const sx = Math.round(rect.x * dpr); const sy = Math.round(rect.y * dpr); const sw = Math.round(rect.width * dpr); const sh = Math.round(rect.height * dpr); // Clamp to bitmap bounds const cx = Math.max(0, sx); const cy = Math.max(0, sy); const cw = Math.min(sw, bitmap.width - cx); const ch = Math.min(sh, bitmap.height - cy); const canvas = new OffscreenCanvas(cw, ch); const ctx = canvas.getContext("2d"); ctx.drawImage(bitmap, cx, cy, cw, ch, 0, 0, cw, ch); return canvas.convertToBlob({ type: "image/png" }); } function blobToDataUrl(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } // ── Server health check ─────────────────────────────────────────────────────── async function checkServer() { try { const r = await fetch("http://localhost:8000/health", { signal: AbortSignal.timeout(3000) }); const d = await r.json(); return d.status === "ok"; } catch { return false; } }