250 lines
9.8 KiB
Markdown
250 lines
9.8 KiB
Markdown
---
|
||
title: "How to Build a Lightweight Chrome Extension (Manifest V3) for Real-Time Image & Canvas Inspection"
|
||
description: "A step-by-step developer guide to building a Manifest V3 browser extension with image scanning, Canvas extraction, context menu triggers, and shadow DOM overlays."
|
||
tags: ["chromeextension", "javascript", "webdev", "browser"]
|
||
canonical_url: "https://greenlenspro.com/"
|
||
cover_image: "https://greenlenspro.com/images/blog/chrome-extension-manifest-v3.jpg"
|
||
---
|
||
|
||
# How to Build a Lightweight Chrome Extension (Manifest V3) for Real-Time Image & Canvas Inspection
|
||
|
||
Browser extensions are one of the most effective ways to make AI models immediately accessible to users across the web. Instead of navigating to a dedicated web app, users can inspect images, scan elements (`pflanzen scanner`), or perform visual AI queries on any web page with a single right-click or hover action.
|
||
|
||
However, migrating to or building on **Chrome Extension Manifest V3 (MV3)** introduces strict security policies, background service worker lifecycles, and CORS restrictions that break traditional DOM scraping techniques.
|
||
|
||
In this deep-dive tutorial, we'll walk through the implementation of a zero-dependency Chrome extension inspired by the [GreenLens Chrome Extension](https://greenlenspro.com/). You will learn how to set up Manifest V3 service workers, extract cross-origin images or Canvas elements without triggering CORS errors, and render isolated hover UI overlays using the Shadow DOM.
|
||
|
||
---
|
||
|
||
## 1. Extension Architecture under Manifest V3
|
||
|
||
Under Manifest V3, background scripts no longer run persistently in a DOM-enabled background page. Instead, they operate as **ephemeral Service Workers** that spin down after periods of inactivity.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A[DOM / Webpage Image] -->|Hover / Context Menu| B[Content Script `scan.js`]
|
||
B -->|Chrome Message Passing| C[MV3 Service Worker `background.js`]
|
||
C -->|API Fetch / Model Inference| D[Remote AI API / GreenLens Backend]
|
||
D -->|Diagnostic Payload| C
|
||
C -->|Message Response| E[Shadow DOM Overlay in `scan.js`]
|
||
```
|
||
|
||
### Key Components:
|
||
- **`manifest.json`**: Declares permissions (`contextMenus`, `activeTab`, `scripting`, `storage`).
|
||
- **`background.js` (Service Worker)**: Registers context menus, handles message queues, and dispatches external API requests.
|
||
- **`scan.js` (Content Script)**: Injected into host pages, inspects hovered elements, captures Canvas data, and renders Shadow DOM overlays.
|
||
- **`popup.js` / `popup.html`**: Extension popup UI for quick status checks and manual URL uploads.
|
||
|
||
---
|
||
|
||
## 2. Defining Manifest V3 (`manifest.json`)
|
||
|
||
To inspect image elements on web pages (`pflanze erkennen`), your manifest must declare explicit permissions for host access while maintaining minimal scope for Chrome Web Store approval.
|
||
|
||
```json
|
||
{
|
||
"manifest_version": 3,
|
||
"name": "GreenLens - Instant Plant Scanner & Disease Identifier",
|
||
"version": "1.0.1",
|
||
"description": "Scan any plant image or flower photo across the web to identify species and diagnose health symptoms.",
|
||
"permissions": [
|
||
"contextMenus",
|
||
"activeTab",
|
||
"storage"
|
||
],
|
||
"host_permissions": [
|
||
"https://*/*",
|
||
"http://*/*"
|
||
],
|
||
"background": {
|
||
"service_worker": "background.js",
|
||
"type": "module"
|
||
},
|
||
"content_scripts": [
|
||
{
|
||
"matches": ["<all_urls>"],
|
||
"js": ["shared.js", "scan.js"],
|
||
"css": ["scan.css"]
|
||
}
|
||
],
|
||
"action": {
|
||
"default_popup": "popup.html",
|
||
"default_icon": "icons/icon-48.png"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Background Service Worker & Context Menu Setup (`background.js`)
|
||
|
||
The service worker creates a custom context menu item when the user right-clicks any image on a website. When clicked, it passes the target image URL or data URI to your vision recognition backend.
|
||
|
||
```javascript
|
||
// background.js - MV3 Service Worker
|
||
|
||
chrome.runtime.onInstalled.addListener(() => {
|
||
chrome.contextMenus.create({
|
||
id: "greenlens-scan-image",
|
||
title: "🌱 Scan with GreenLens (Identify & Diagnose)",
|
||
contexts: ["image"]
|
||
});
|
||
});
|
||
|
||
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||
if (info.menuItemId === "greenlens-scan-image" && tab?.id) {
|
||
const imageUrl = info.srcUrl;
|
||
|
||
// Send message to content script to display loading state
|
||
chrome.tabs.sendMessage(tab.id, {
|
||
action: "INITIATE_SCAN",
|
||
imageUrl: imageUrl
|
||
});
|
||
|
||
try {
|
||
const response = await fetch("https://greenlenspro.com/v1/scan", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ imageUrl: imageUrl })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
// Dispatch results back to content script
|
||
chrome.tabs.sendMessage(tab.id, {
|
||
action: "RENDER_RESULT",
|
||
result: data
|
||
});
|
||
} catch (error) {
|
||
console.error("Scan API Error:", error);
|
||
chrome.tabs.sendMessage(tab.id, {
|
||
action: "SCAN_ERROR",
|
||
error: "Failed to scan target image."
|
||
});
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Extracting Canvas & CORS Images in Content Script (`scan.js`)
|
||
|
||
Websites often render images inside `<canvas>` tags or block direct CORS fetching via `crossorigin="anonymous"`. To bypass CORS hurdles safely without proxying, content scripts can convert image elements into base64 Data URIs directly inside the client browser.
|
||
|
||
```javascript
|
||
// scan.js - Content Script Element Extraction
|
||
|
||
function convertElementToDataUri(imgElement) {
|
||
return new Promise((resolve, reject) => {
|
||
// Handling standard <img> tags
|
||
if (imgElement.tagName.toLowerCase() === 'img') {
|
||
const canvas = document.createElement('canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
canvas.width = imgElement.naturalWidth || imgElement.width;
|
||
canvas.height = imgElement.naturalHeight || imgElement.height;
|
||
|
||
try {
|
||
ctx.drawImage(imgElement, 0, 0);
|
||
resolve(canvas.toDataURL('image/jpeg', 0.85));
|
||
} catch (err) {
|
||
// Tainted canvas fallback: return src URL directly
|
||
resolve(imgElement.src);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Handling HTML5 Canvas elements
|
||
if (imgElement.tagName.toLowerCase() === 'canvas') {
|
||
try {
|
||
resolve(imgElement.toDataURL('image/jpeg', 0.85));
|
||
} catch (err) {
|
||
reject(new Error("Canvas tainted by cross-origin data."));
|
||
}
|
||
return;
|
||
}
|
||
|
||
reject(new Error("Unsupported element type for scanning."));
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Isolated UI Overlays using Shadow DOM
|
||
|
||
Injecting popup UI overlays directly into arbitrary third-party web pages usually results in CSS style leaks. Global stylesheets from the host site can ruin your extension's typography, buttons, and layout.
|
||
|
||
The solution is wrapping your UI in an isolated **Shadow DOM root**.
|
||
|
||
```javascript
|
||
// scan.js - Isolated Shadow DOM Popup Injector
|
||
|
||
function injectResultModal(scanData) {
|
||
let hostElement = document.getElementById('greenlens-shadow-host');
|
||
|
||
if (!hostElement) {
|
||
hostElement = document.createElement('div');
|
||
hostElement.id = 'greenlens-shadow-host';
|
||
document.body.appendChild(hostElement);
|
||
}
|
||
|
||
// Create shadow root if it doesn't already exist
|
||
const shadowRoot = hostElement.shadowRoot || hostElement.attachShadow({ mode: 'open' });
|
||
|
||
shadowRoot.innerHTML = `
|
||
<style>
|
||
.gl-card {
|
||
position: fixed;
|
||
bottom: 24px;
|
||
right: 24px;
|
||
width: 320px;
|
||
background: #ffffff;
|
||
color: #16181d;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
z-index: 999999;
|
||
}
|
||
.gl-title { font-size: 16px; font-weight: 700; color: #16794a; margin: 0 0 6px; }
|
||
.gl-sub { font-size: 13px; color: #4a4f5a; margin-bottom: 12px; }
|
||
.gl-badge { display: inline-block; background: #e8f4ed; color: #16794a; padding: 4px 8px; border-radius: 6px; font-size: 11px; font-weight: 600; }
|
||
.gl-close { float: right; cursor: pointer; border: 0; background: none; font-size: 16px; color: #888; }
|
||
</style>
|
||
<div class="gl-card">
|
||
<button class="gl-close" onclick="this.getRootNode().host.remove()">×</button>
|
||
<div class="gl-badge">${scanData.confidence || '98% Match'}</div>
|
||
<h3 class="gl-title">${scanData.species || 'Pflanze erkannt'}</h3>
|
||
<p class="gl-sub">${scanData.diagnosis || 'Healthy foliage detected.'}</p>
|
||
<a href="https://greenlenspro.com" target="_blank" style="color:#16794a; font-weight:600; font-size:12px; text-decoration:none;">View complete care guide →</a>
|
||
</div>
|
||
`;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Comparing Chrome Extension Scanners vs. Google Lens
|
||
|
||
While generic visual search engines like `google lens pflanzen erkennen` return broad web image matches, a domain-tailored Chrome extension offers specialized capabilities:
|
||
|
||
| Feature | Generic Visual Search (`google lens`) | Specialized MV3 Extension (GreenLens) |
|
||
|---|---|---|
|
||
| **Species Identification (`pflanzen bestimmen`)** | Broad web search matches | Specialized Botanical Taxonomy Models |
|
||
| **Health Diagnosis (`pflanzenkrankheiten erkennen`)** | Limited to visual similarity | Multi-symptom chlorosis & pest detection |
|
||
| **Context Integration** | Opens external tab | In-page Shadow DOM Overlay |
|
||
| **Canvas & WebGL Support** | No direct element inspection | Client-side Data URI canvas extraction |
|
||
|
||
---
|
||
|
||
## Summary & Developer Checklist
|
||
|
||
1. **Adopt MV3 Service Workers:** Treat background tasks as stateless, event-driven functions.
|
||
2. **Prevent CSS Contamination:** Always use `attachShadow({ mode: 'open' })` for injected content script UI elements.
|
||
3. **Handle Canvas Fallbacks:** Gracefully degrade between DOM src attributes, canvas data URIs, and remote URLs.
|
||
4. **Minimal Permissions Strategy:** Request only `contextMenus` and `activeTab` to ensure rapid Chrome Web Store review.
|
||
|
||
To see a live implementation of an in-browser plant scanner, test out the [GreenLens Browser Extension Engine](https://greenlenspro.com/).
|