🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

女朋友又給我出難題了:解鎖網頁禁用複製 + 一鍵提取圖片文字

女朋友做廣告策劃,每天要從海量網站和素材中摘抄文案。

微信或飛書截圖都有 OCR,但她總要“切微信/飛書 → 識別 → 複製 → 切回瀏覽器”,來回折騰好麻煩,經常被打斷思路。

兩個最常見的煩惱

  • 禁止複製的頁面:設計靈感站、素材站、文庫類網站,明明能看到文字,就是選不了、右鍵也沒用,只能手敲。
  • 圖片裡的文字無法快速提取:看到一張圖,得切到微信、點“提取文字”、等識別、複製、再切回來粘貼。

一張圖,好幾個步驟,來回切換三次。

有天晚上她在趕方案,一邊操作一邊念叨:“太麻煩了,思路都斷了……”

我說:“要不我給你寫個插件?”

於是週末兩天,做了這個 「圖文解鎖器」

  1. 一鍵解除網頁限制 —— 禁選中、禁右鍵的網站,點一下就能正常複製
  2. 瀏覽器裡直接拖框識別 —— 不用切微信,看到哪裡框哪裡,幾秒出結果
  3. 所有操作在側邊欄完成 —— 不遮擋頁面,不用來回切窗口

週一她用上之後的評價:“終於順手了!那些惡心的禁止複製網站現在隨便複製,圖片識別也不用跳來跳去了。”

下面講講開發過程和技術實現 👇

效果演示: 解鎖網頁禁用複製 + 一鍵提取圖片文字

copy-everything.gif

功能特性

1. 解除網頁限制

  • 一鍵解除複製限制
  • 恢復右鍵菜單
  • 允許文本選中
  • 支持動態加載的網頁內容解除限制

2. OCR 文字識別(方式與特性)

方式 說明 使用場景
頁面截圖 快速截取當前可見區域 一鍵識別網頁全部內容
自選區域 拖拽選擇任意區域 只識別你選中的特定區域
點擊上傳 選擇本地圖片文件 識別本地圖片中的文字
拖拽上傳 拖入圖片即可識別 方便上傳圖片並識別文字
  • 基於騰訊雲 OCR API(每月 1000 次免費額度)
  • 內置圖片預覽,識別前可確認內容
  • 識別結果一鍵複製,支持後續粘貼使用
  • 識別流程:選擇方式 → 預覽 → 開始識別 → 複製/下載

快速開始(安裝使用)

1. 獲取代碼

2. 安裝擴展(本地加載)

  1. 打開 Chrome,地址欄輸入 chrome://extensions/
  2. 右上角開啟“開發者模式”
  3. 點擊“加載已解壓的擴展程序”
  4. 選擇插件目錄
  5. 安裝完成

小貼士:Side Panel 依賴較新版本 Chrome,建議 114+。

安裝copy-everything.gif

3. 首次使用

  • 點擊插件圖標打開側邊欄
  • 點擊「一鍵解除」即可解除頁面複製限制
  • 使用截圖功能(頁面截圖 / 自選區域)或本地上傳(點擊上傳 / 拖拽上傳)進行 OCR 識別
  • 首次體驗可直接使用以下帳號(每月 1000 次免費額度):
    • SecretId: AKIDLUQ7aqsjNmwufWFm590d1BxXs0xgBRTH
    • SecretKey: c09OVP4aw75oIYZMvFO8j5C5uiIgspIc
    • 將 SecretId 和 SecretKey 填入插件側邊欄後保存設置,即可開始圖文識別

注:如免費額度用完,請根據下方指導申請屬於你自己的帳號哦。👇

騰訊雲 OCR 開通與配置

插件支持騰訊雲 OCR,每月有約 1000 次免費額度,足夠日常使用。首次識別前,請按以下步驟開通並配置:

  1. 進入 騰訊雲文字識別控制台 console.cloud.tencent.com/ocr/v2/over…,勾選條款並開通服務。
  2. 前往 API 密鑰管理 console.cloud.tencent.com/cam/capi 獲取自己的 SecretId 和 SecretKey。
  3. 將 SecretId 和 SecretKey 填入插件側邊欄並保存設置,即可開始圖文識別。

注意事項

  1. API 費用:騰訊雲 OCR 每個月有1000個請求的免費額度,超出後按量計費
  2. 權限說明
    • activeTab:訪問當前標籤頁
    • scripting:注入腳本
    • storage:保存配置
    • sidePanel:側邊欄功能
  3. 兼容性
    • Chrome 114+ (Side Panel API 要求)
    • 部分網站可能有額外防護

技術實現原理

核心技術棧

  • Manifest V3:Chrome 擴展最新規範
  • Side Panel API:側邊欄界面
  • Content Scripts:頁面注入腳本
  • Tencent Cloud OCR:騰訊雲文字識別 API
  • Canvas API:圖片裁剪

架構設計

copy-everything
├── manifest.json         # 插件配置
├── icons                 # 插件圖標
├── background.js         # 後台服務
├── content.js            # 內容腳本(核心功能)
├── inject.js             # 注入頁面腳本(核心功能)
├── sidepanel.html        # 側邊欄界面
├── sidepanel.js          # 側邊欄界面邏輯
├── sidepanel.css         # 側邊欄樣式
└── tencentOCR.js         # OCR SDK

核心代碼解析

1. 解除複製限制 (五層防護)

這是插件的核心功能之一,通過多層防護確保解除限制:
image.png

實現原理
通過 五層防護 確保解除限制

第一層:CSS 強制覆蓋

const style = document.createElement('style');
style.textContent = `
  html, body, * {
    -webkit-user-select: text !important;
    -moz-user-select: text !important;
    user-select: text !important;
  }
`;
document.head.appendChild(style);

作用:強制允許文本選中,覆蓋網站的 CSS 限制。

第二層:事件攔截(捕獲階段)

// 在捕獲階段攔截所有限制事件
const restrictedEvents = [
  'copy', 'cut', 'paste', 'contextmenu',
  'selectstart', 'dragstart', 'keydown', 'keyup', 'keypress'
];

const stopEvent = (e) => {
  e.stopPropagation();
  e.stopImmediatePropagation?.(); // 阻止其他監聽器執行
};

// 在 window、document、documentElement、body 四個層級同時監聽
[window, document, document.documentElement, document.body].forEach(target => {
  if (target) {
    restrictedEvents.forEach(eventType => {
      target.addEventListener(eventType, stopEvent, true); // ← 捕獲階段
    });
  }
});

關鍵點

  • 捕獲階段 攔截(優先級最高)
  • 使用 stopImmediatePropagation() 阻止其他監聽器
  • 在四個層級監聽(window → document → html → body)

為什麼要用捕獲階段?

事件流:捕獲階段 → 目標階段 → 冒泡階段 
        ↓
  我們在這裡攔截(優先級最高)

第三層:移除內聯事件屬性

// 移除所有事件屬性(如 oncopy="return false")
const eventAttrs = [
  'oncopy', 'oncut', 'onpaste', 'oncontextmenu',
  'onselectstart', 'onkeydown', 'ondragstart'
];

document.querySelectorAll(eventAttrs.map(a => `[${a}]`).join(','))
  .forEach(el => {
    eventAttrs.forEach(attr => el.removeAttribute(attr));
  });

作用:移除 HTML 標籤上的內聯事件(如 oncopy="return false"

第四層:API 劫持(重寫 preventDefault)

// 注入到頁面上下文,重寫原生方法
const script = document.createElement('script');
script.textContent = `
  (function() {
    if (window.__preventDefaultDisabled) return;
    window.__preventDefaultDisabled = true;

    const blockedEvents = new Set(${JSON.stringify(restrictedEvents)});
    const originalPreventDefault = Event.prototype.preventDefault;

    Event.prototype.preventDefault = function() {
      if (blockedEvents.has(this.type)) return; // ← 禁用 preventDefault
      return originalPreventDefault.call(this);
    };
  })();
`;
document.documentElement.appendChild(script);
script.remove();

為什麼要注入 <script> 標籤?

  • Content Script 運行在 隔離環境(Isolated World)
  • 無法直接修改頁面的 Event.prototype
  • 通過 <script> 標籤注入到 頁面上下文(Main World)

💡 深入理解腳本通信機制
如果你想深入了解插件腳本通信機制,強烈推薦閱讀: 大部分人都錯了!這才是 Chrome 插件多腳本通信的正確姿勢 這篇文章用原理+示例拆解了常見誤區與正確做法,能幫你更透徹地理解本插件的通信機制。

第五層:動態內容監聽

// 監聽 DOM 變化,自動處理動態加載的元素
const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    mutation.addedNodes?.forEach((node) => {
      if (node instanceof Element) {
        // 移除事件屬性
        eventAttrs.forEach(attr => node.removeAttribute(attr));
        // 強制允許選中
        node.style?.setProperty('user-select', 'text', 'important');
      }
    });
  });
});

observer.observe(document.documentElement, {
  childList: true,
  subtree: true
});

作用:監聽 DOM 變化,自動處理動態加載的元素(如 React/Vue 渲染的內容)。

2. 自選區域截圖

三步完成截圖

  1. 按下按鈕 → 屏幕變暗
  2. 拖動框 → :按住鼠標拖出一個框,選出你想要的區域
  3. 鬆開手 → 自動截取框內內容

實現步驟

這背後發生了什麼?

步驟 1:創建選框工具

// 創建遮罩層(就像給屏幕蓋了一層磨砂玻璃)
const overlay = document.createElement('div');
overlay.style.cssText = `
  position: fixed;
  left: 0; top: 0;
  width: 100vw; height: 100vh;
  background: rgba(0,0,0,0.5);  /* 半透明黑色 */
  cursor: crosshair;             /* 十字準星 */
  z-index: 999999;
`;
document.body.appendChild(overlay);

當你點擊"自選區域"後,插件會:

  • 在屏幕上蓋一層半透明黑色遮罩(讓你看清楚要選什麼)
  • 鼠標變成十字準星(提示你可以開始拖框了)
  • 準備好一個綠色邊框(等你拖出來就顯示)

2. 跟隨你的鼠標畫框(實時反饋)

// 鼠標按下 → 記錄起點
function handleMouseDown(e) {
  startX = e.clientX;  // 記住你點擊的 X 坐標
  startY = e.clientY;  // 記住你點擊的 Y 坐標
}

// 鼠標移動 → 實時更新框的大小
function handleMouseMove(e) {
  currentX = e.clientX;
  currentY = e.clientY;

  // 計算框的位置和大小(支持反向拖拽)
  const left = Math.min(startX, currentX);   // 取最小值作為左邊界
  const top = Math.min(startY, currentY);    // 取最小值作為上邊界
  const width = Math.abs(currentX - startX); // 寬度 = 绝对值
  const height = Math.abs(currentY - startY); // 高度 = 绝对值

  // 更新綠色邊框的位置
  selectionBox.style.left = left + 'px';
  selectionBox.style.top = top + 'px';
  selectionBox.style.width = width + 'px';
  selectionBox.style.height = height + 'px';
}

當你按住鼠標拖動時:

  • 記錄你 按下的位置(起點)
  • 記錄你 當前的位置(終點)
  • 用這兩個點的橫坐標和縱坐標的差值畫出一個矩形框

支持反向拖拽:不管你從左上往右下拖,還是從右下往左上拖,都能正確識別!

舉個例子

你從 (100, 100) 拖到 (300, 300)
→ 框的位置:left=100, top=100, width=200, height=200 ✅

你從 (300, 300) 拖到 (100, 100)(反向)
→ 框的位置:left=100, top=100, width=200, height=200 ✅

3. 精準裁剪(像剪刀一樣裁圖)
當你鬆開鼠標後,插件會:

  1. 先截取整個頁面(就像拍了一張全屏照片)
  2. 再裁剪出你選中的部分(就像用剪刀剪出你要的區域)

關鍵技術:Canvas 畫布裁剪

// 1. 創建一個畫布(就像準備一張白紙)
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

// 2. 設置畫布大小 = 你選中區域的大小
canvas.width = width;
canvas.height = height;

// 3. 把全屏截圖的"選中部分"畫到畫布上
ctx.drawImage(
  fullScreenImage,  // 全屏截圖
  left, top,        // 從哪裡開始裁剪(你選中區域的左上角)
  width, height,    // 裁剪多大(你選中區域的寬高)
  0, 0,             // 畫到畫布的哪裡(從畫布左上角開始)
  width, height     // 畫多大(填滿整個畫布)
);

// 4. 導出成圖片
const croppedImage = canvas.toDataURL('image/png');

用生活場景類比

  • 全屏截圖 = 拍了一張班級照
  • 你選中的區域 = 你只想要照片裡的某個人
  • Canvas 裁剪 = 用剪刀把那個人剪下來

步驟 4:四遮罩高亮效果
問題:如果只用一個全屏遮罩,選中的區域也會被遮住,用戶看不清選了什麼。

解決方案:用 4 個遮罩塊圍繞選區。

┌─────────────────────────────────┐
│    上遮罩(半透明黑色)            │
├──────┬──────────────┬───────────┤
│ 左遮罩│   選區(高亮) │  右遮罩   │
├──────┴──────────────┴───────────┤
│    下遮罩(半透明黑色)            │
└─────────────────────────────────┘

代碼實現

// 創建 4 個遮罩塊
const masks = ['top', 'right', 'bottom', 'left'].map(position => {
  const mask = document.createElement('div');
  mask.style.cssText = `
    position: fixed;
    background: rgba(0,0,0,0.5);
    pointer-events: none; /* 不阻擋鼠標事件 */
  `;
  return mask;
});

// 根據選區位置更新遮罩塊大小
function updateMasks(left, top, width, height) {
  masks[0].style.cssText += `left:0; top:0; width:100vw; height:${top}px;`; // 上
  masks[1].style.cssText += `left:${left + width}px; top:${top}px; width:${window.innerWidth - left - width}px; height:${height}px;`; // 右
  masks[2].style.cssText += `left:0; top:${top + height}px; width:100vw; height:${window.innerHeight - top - height}px;`; // 下
  masks[3].style.cssText += `left:0; top:${top}px; width:${left}px; height:${height}px;`; // 左
}

高分屏適配(讓圖片更清晰)
問題:Retina 屏幕(如 MacBook)的像素密度是普通屏幕的 2 倍,如果不處理會導致截圖模糊。

解決方案:乘以設備像素比

const scale = window.devicePixelRatio || 1; // Retina 屏幕 = 2,普通屏幕 = 1

// 設置畫布大小時要乘以 scale
canvas.width = width * scale;
canvas.height = height * scale;

// 裁剪時也要乘以 scale
ctx.drawImage(
  fullScreenImage,
  left * scale, top * scale,     // ← 乘以 scale
  width * scale, height * scale, // ← 乘以 scale
  0, 0,
  width * scale, height * scale
);

完整流程

image.png

時序圖

3. 本地上傳圖片

方式一:點擊上傳

HTML

<input type="file" id="uploadInput" accept="image/*" style="display:none">
<button onclick="document.getElementById('uploadInput').click()">
  📁 上傳圖片
</button>

JavaScript

document.getElementById('uploadInput').addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (!file) return;

  const reader = new FileReader();
  reader.onload = async (event) => {
    const base64 = event.target.result.split(',')[1];
    await recognizeText(base64);
  };
  reader.readAsDataURL(file);
});

方式二:拖拽上傳

const dropZone = document.getElementById('dropZone');

// 1. 阻止默認行為
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  dropZone.addEventListener(eventName, (e) => {
    e.preventDefault();
    e.stopPropagation();
  });
});

// 2. 視覺反饋
['dragenter', 'dragover'].forEach(eventName => {
  dropZone.addEventListener(eventName, () => {
    dropZone.classList.add('drag-over');
  });
});

// 3. 處理文件
dropZone.addEventListener('drop', (e) => {
  const file = e.dataTransfer.files[0];
  if (!file.type.startsWith('image/')) {
    alert('請上傳圖片文件!');
    return;
  }

  const reader = new FileReader();
  reader.onload = async (event) => {
    const base64 = event.target.result.split(',')[1];
    await recognizeText(base64);
  };
  reader.readAsDataURL(file);
});

CSS

#dropZone {
  border: 2px dashed #ccc;
  border-radius: 8px;
  padding: 20px;
  text-align: center;
  transition: all 0.3s;
}

#dropZone.drag-over {
  border-color: #4CAF50;
  background: rgba(76, 175, 80, 0.1);
}
  • 作用:把圖片拖進上傳圖片區域,拦住瀏覽器默認行為,在 drop 時讀文件並識別或預覽。

4. 頁面截圖

const capturePageBtn = document.getElementById('capturePageBtn');

capturePageBtn.addEventListener('click', async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

  const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
    format: 'png'
  });

  const base64 = dataUrl.split(',')[1];
  await recognizeText(base64);
});

直接使用chrome插件的截圖功能
權限配置(manifest.json):

{
  "permissions": [
    "activeTab",
    "tabs"
  ]
}

5. OCR 識別

使用騰訊雲 OCR API,通過 TC3-HMAC-SHA256 簽名調用 GeneralBasicOCR 接口,每月 1000 次免費額度。

// 核心調用代碼
const ocr = new TencentOCR(secretId, secretKey);
const text = await ocr.recognizeText(imageBase64);

總結

這個插件解決了日常瀏覽網頁時的兩大痛點:

  1. 解除限制:讓你自由複製任何內容
  2. OCR 識別:快速提取圖片文字

技術亮點

技術點 難點 解決方案
解除限制 網站多層防護 五層攔截 + API 劫持
自選截圖 高分屏模糊 devicePixelRatio 適配
拖拽上傳 視覺反饋 CSS 過渡動畫
OCR 識別 API 簽名 TC3-HMAC-SHA256

希望這個插件能幫到大家!如果有問題或建議,歡迎在評論區交流~

🔗 相關鏈接

本插件僅供學習交流使用,請遵守以下原則:

  1. 合法使用:請勿用於商業用途或侵犯他人知識產權
  2. 尊重版權:遵守網站服務條款

如果覺得對您有幫助,歡迎點贊 👍 收藏 ⭐ 關注 🔔 支持一下!

往期實戰推薦:


原文出處:https://juejin.cn/post/7576197876717797414


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝15   💬10   ❤️4
407
🥈
我愛JS
📝1   💬7   ❤️3
61
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付