Spaces:
Sleeping
Sleeping
| // 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; | |
| } | |
| } | |