C.W.K.
Stream
Lesson 04 of 07 · published

Screenshot 과 clipping — captureVisibleTab + canvas crop

~13 min · screenshot, captureVisibleTab, canvas, clipping

Level 0Extension 입덕
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"chrome.tabs.captureVisibleTab 이 전체 tab 을 dataURL 로 줘. Selection rect 가 어느 pixel 이 중요한지 알려 줘. Lesson 4 가 그 둘을 wire 해서 모든 ClipDeck clip 이 어디서 왔는지의 tightly-cropped screenshot 휴대 가능."

chrome.tabs.captureVisibleTab

API 가 작아. 두 인자:

  • windowId — optional. 주어진 window 의 active tab capture; 생략하면 현재 window capture.
  • options — optional. { format: 'png' | 'jpeg', quality: 0..100 }. Default PNG. 텍스트 많은 page 의 screenshot 엔 quality 80 의 JPEG 가 보통 5x 작음.

data: URL 반환. Capture 가 현재 visible viewport — user 가 보는 것 — 만 cover. Off-screen content 는 capture 안 됨.

Permission: activeTab (user gesture 후) OR URL 의 host_permissions OR tabs permission. ClipDeck 이 이 시점에 관련된 셋 다 가짐.

Crop workflow

Captured 이미지가 full-tab. ClipDeck 이 user selection 의 rect 원함. 수학:

  1. Content script 에서 selection 의 getBoundingClientRect() 얻기 — viewport-relative 좌표.
  2. Device pixel ratio (window.devicePixelRatio) 얻기. Captured 이미지가 native 해상도; rect 는 CSS pixel.
  3. SW 가 dataURL + rect + dpr 받음. Offscreen canvas 에 load. 맞는 pixel 좌표에서 cropped 으로 draw.
  4. Cropped canvas 를 또 다른 dataURL 로 export. Clip 과 저장.

Canvas 위한 offscreen document

Service worker 가 DOM 사용 못 함, 그래서 normal canvas 생성 못 함. MV3 가 chrome.offscreen 도입 — SW 가 DOM API (canvas, Blob 처리 가진 fetch, audio) 필요할 때 spawn 하는 hidden document. Shape:

await chrome.offscreen.createDocument({
  url: 'offscreen.html',
  reasons: ['BLOBS'],
  justification: 'Crop captured tab screenshots for ClipDeck clips',
});

offscreen.html 이 SW 에서 메시지 받고, canvas work 하고, 결과 다시 post 하는 offscreen.js 가진 작은 page. 사용 후 chrome.offscreen.closeDocument() 호출 — 열어 두는 게 약간 메모리 낭비.

Crop 코드

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; // cropped dataURL

OffscreenCanvas 가 offscreen document 에서 지원되고 Worker-friendly canvas 제공. 9-인자 drawImage form: sx, sy, sw, sh, dx, dy, dw, dh. Source 좌표가 rect 에서; destination 이 0,0 에서 시작.

Size 예산

Typical 텍스트 selection 의 각 PNG screenshot 이 ~10–50 KB. PNG 이 텍스트엔 fine; JPEG quality 80 이 또 30–50% cut. dataURL 을 chrome.storage.local 에 직접 저장 — quota 가 약 10 MB, cropped 유지하면 몇 백 screenshot 에 plenty. Clip 카운트 자라면 공격적 압축, JPEG 전환, 또는 user 가 clip 별 screenshot opt-out 고려.

Clip 에 들어가는 것

Clip schema 확장:

{ id, text, url, title, savedAt, screenshot?: string /* dataURL */ }

Side panel 이 있으면 screenshot inline 렌더 (작은 thumbnail; click 으로 확대). Export flow 가 포함; clipboard copy 는 여전히 text 만.

captureVisibleTab + Selection.rect + offscreen canvas = 모든 clip 의 tightly-cropped, in-context screenshot. 저렴, optional, bare text 가 잃는 원래 context 보존.
captureVisibleTab 과 off-screen selection. User 의 selection 이 visible viewport 너머 걸치면 (긴 page 의 긴 highlight), captureVisibleTab 이 visible 한 것만 capture. Cropped 결과가 불완전. 두 option: scroll-and-stitch (selection 을 view 로 scroll 하면서 여러 frame capture, 합성) — heavy. 또는 rect 높이가 viewport 초과하면 screenshot 거부, text-only 로 fallback — light. ClipDeck v1 이 preview 의 한 줄 note 와 함께 light path.

Code

manifest.json — permission 에 'offscreen' 추가·json
{
  "permissions": ["storage", "activeTab", "scripting", "sidePanel", "contextMenus", "commands", "alarms", "tabs", "offscreen"]
}
background.js — captureVisibleTab + crop 을 offscreen 에 위임·javascript
// background.js — capture, 다음 offscreen document 통해 crop
async function ensureOffscreen() {
  const existing = await chrome.offscreen.hasDocument();
  if (existing) return;
  await chrome.offscreen.createDocument({
    url: "offscreen.html",
    reasons: ["BLOBS"],
    justification: "Crop captured tab screenshots for ClipDeck clips",
  });
}

async function captureAndCrop(tab, rect, dpr) {
  if (rect.height > tab.height) return null; // off-screen — 경고 참조
  const dataURL = await chrome.tabs.captureVisibleTab(tab.windowId, {
    format: "png",
  });
  await ensureOffscreen();
  const response = await chrome.runtime.sendMessage({
    type: "clipdeck:crop",
    payload: { dataURL, rect, dpr },
  });
  return response?.cropped ?? null;
}
offscreen.html — cropping script 의 minimal stub·html
<!-- offscreen.html -->
<!doctype html>
<html>
  <head><meta charset="utf-8" /></head>
  <body>
    <script src="offscreen.js"></script>
  </body>
</html>
offscreen.js — OffscreenCanvas 가 captured 이미지 crop·javascript
// offscreen.js — crop request 받고 cropped dataURL 반환
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message?.type !== "clipdeck:crop") return;
  (async () => {
    const { dataURL, rect, dpr } = message.payload;
    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));
    sendResponse({ cropped: reader.result });
  })();
  return true;
});

External links

Exercise

clipdeck/manifest.json permission 에 "offscreen" 추가. 세 번째와 네 번째 code block 사용해 clipdeck/offscreen.html 과 clipdeck/offscreen.js 생성. 두 번째 code block (captureAndCrop) 을 clipdeck/background.js 에 추가. Save-clip flow 에 wire: content script 에서 selection 텍스트 + rect capture 후, captureAndCrop(tab, rect, devicePixelRatio) 도 호출하고 결과를 새 clip 의 screenshot field 로 attach. Side panel 을 update 해서 각 clip text 옆 thumbnail 렌더. Reload, Wikipedia article 에서 clip 저장, side panel 이 highlight 된 영역의 작은 이미지 보이는지 확인.
Hint
chrome.tabs.captureVisibleTab 이 Cannot access contents of the page error 면 activeTab grant 가 current 아님 — fresh user gesture 후 호출 필수. Capture 를 여러 await 통해 defer 안 하고 save-clip handler 자체로 옮기기 시도. Cropped 이미지가 offset 됐거나 size 잘못이면 dpr 곱셈 더블 체크 — captured 이미지가 physical pixel, rect 가 CSS pixel. devicePixelRatio 가 Mac 에서 보통 1 이나 2, 대부분 Windows 화면에서 1. chrome.offscreen.createDocument 가 Only a single offscreen document may be created throw 하면 hasDocument 확인 잊은 거 — ensureOffscreen 을 idempotent 유지.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.