File size: 4,184 Bytes
0533780
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// 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;
  }
}