Files
Greenlens/docs/hashnode-devto-posts/post-02-manifest-v3-chrome-extension-scanner.md
2026-08-05 19:39:22 +02:00

9.8 KiB
Raw Permalink Blame History

title, description, tags, canonical_url, cover_image
title description tags canonical_url cover_image
How to Build a Lightweight Chrome Extension (Manifest V3) for Real-Time Image & Canvas Inspection 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.
chromeextension
javascript
webdev
browser
https://greenlenspro.com/ 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. 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.

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.

{
  "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.

// 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.

// 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.

// 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.