DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

How to Build a Chrome Extension for Scanning Barcode & QR Code with Multiple Input Methods

Barcode and QR code scanning is essential for many workflows—from inventory management and product research to accessing promotional content and document processing. While mobile apps dominate this space, a browser extension offers unique advantages: it's always available during web browsing without app switching, can extract barcodes directly from web pages through right-click context menus, capture and decode screenshots of any visible content, and process files instantly without uploading to external services. This keeps your data private and eliminates the friction of taking photos with your phone when the information is already on your screen.

In this tutorial, we'll build a feature-rich Chrome extension that demonstrates these capabilities by implementing five different input methods: camera scanning, screenshot capture with area selection, file uploads (including PDFs), drag-and-drop, and context menu integration for web images. We'll use Dynamsoft Barcode Reader SDK for high-accuracy decoding and leverage modern Chrome Extension APIs including the Side Panel API for persistent UI, OAuth for secure authentication, and OffscreenCanvas for efficient image processing. By the end, you'll have a complete understanding of building production-ready browser extensions with complex features.

Demo Video: Chrome Extension for Scanning Barcodes & QR Codes

Project Overview

Key Features:

  • Camera scanning in a new tab
  • Screenshot area selection with custom cursor
  • File upload (images and multi-page PDFs)
  • Right-click context menu on images
  • Drag & drop support
  • Modern side panel UI with settings
  • Secure OAuth authentication

Tech Stack:

  • Chrome Extension Manifest V3
  • Dynamsoft Barcode Reader SDK
  • Side Panel API (Chrome 114+)
  • Content Scripts & Background Service Worker

Setting Up Manifest V3

First, create a manifest.json file with the essential permissions:

{
    "manifest_version": 3,
    "name": "Barcode & QR Code Scanner",
    "version": "1.0.1",
    "description": "Scan barcodes and QR codes from images, PDFs, and camera using Dynamsoft SDK",
    "permissions": [
        "storage",
        "cookies",
        "activeTab",
        "tabs",
        "scripting",
        "sidePanel",
        "contextMenus"
    ],
    "host_permissions": [
        "<all_urls>"
    ],
    "side_panel": {
        "default_path": "index.html"
    },
    "background": {
        "service_worker": "background.js"
    },
    "content_security_policy": {
        "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • sidePanel: Displays persistent UI alongside browsing
  • scripting: For dynamic screenshot selector injection
  • contextMenus: Right-click image scanning
  • wasm-unsafe-eval: Required for Dynamsoft's WebAssembly SDK
  • host_permissions: <all_urls>: Allows screenshot and context menu on all sites

Implementing Side Panel UI

Side panel barcode scanner UI

HTML Structure

Create index.html with a two-column layout:

<body>
    <div class="main-container">
        <!-- Left Column: Main Content -->
        <div class="main-content">
            <h1>Barcode Scanner</h1>

            <!-- License Status -->
            <div class="license-row">
                <span id="loginStatus">Please login</span>
                <button id="loginButton">
                    <svg><!-- Google icon --></svg>
                    Login with Google
                </button>
            </div>

            <!-- Action Buttons -->
            <div class="controls">
                <button id="scan" title="Scan with Camera">📷</button>
                <label for="file" class="file-label" title="Upload File">
                    📁
                    <input type="file" id="file" accept="image/*,application/pdf" />
                </label>
                <button id="screenshot" title="Capture Screenshot">✂️</button>
            </div>

            <!-- Drag & Drop Zone -->
            <div id="dropZone" class="drop-zone">
                <div class="drop-icon">📥</div>
                <div class="drop-text">Drag & Drop images or PDFs here</div>
            </div>

            <!-- Scanner Display -->
            <div id="divScanner">
                <div id="container"></div>
            </div>

            <!-- Results -->
            <textarea id="result" readonly></textarea>
        </div>

        <!-- Right Column: Sidebar -->
        <div class="right-sidebar">
            <button id="openSettings" class="sidebar-btn">⚙️</button>
            <button id="cartBtn" class="sidebar-btn">🛒</button>
            <div class="user-section">
                <div class="user-avatar">👤</div>
                <div class="user-name">Guest</div>
            </div>
        </div>
    </div>
</body>
Enter fullscreen mode Exit fullscreen mode

Responsive Layout with Flexbox

Key CSS for the side panel layout:

/* Main Container */
.main-container {
    display: flex;
    height: 100vh;
    width: 100%;
    overflow: hidden;
}

/* Main Content Area */
.main-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    padding: 20px 15px;
    overflow-y: auto;
    min-width: 0;
}

/* Scanner - Fills Available Space */
#divScanner {
    flex: 1;
    min-height: 300px;
    max-height: 600px;
    border-radius: 12px;
    background: white;
}

/* Results - Fixed Height */
#result {
    height: 120px;
    flex-shrink: 0;
    resize: vertical;
}

/* Right Sidebar - Fixed Width */
.right-sidebar {
    width: 60px;
    background: rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(10px);
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px 0;
}
Enter fullscreen mode Exit fullscreen mode

Why This Works:

  • flex: 1 on scanner makes it fill available space
  • flex-shrink: 0 on results prevents it from shrinking
  • Fixed sidebar width ensures consistent UI
  • overflow-y: auto allows scrolling when needed

OAuth Authentication Flow

The Dynamsoft Barcode Reader SDK requires a license key to function. Rather than hardcoding a license or asking users to manually register, we implement a seamless OAuth flow using Google authentication. Users click a login button that opens Google's OAuth consent screen, then upon successful authentication, they're automatically logged into the Dynamsoft customer portal. This grants them a 30-day free trial license without any manual registration or form filling.

Google OAuth Flow

The authentication flow works as follows:

  1. User clicks "Login" → Extension opens Dynamsoft's Google OAuth endpoint in a popup window
  2. User authenticates with Google → Dynamsoft portal validates and creates/logs into their account
  3. Portal sets authentication cookies (DynamsoftToken, DynamsoftUser) in the browser
  4. Extension reads these cookies and requests a trial license from Dynamsoft's API
  5. License is kept in memory
  6. On subsequent visits, if valid cookies exist, the extension auto-requests a license without showing the login button

Background Service Worker

The background script handles OAuth popup and cookie-based authentication:

// background.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request.action === 'openAuthPopup') {
        const authUrl = `https://www.dynamsoft.com/api-common/Api/User/Login/Google?redirectUri=${encodeURIComponent(request.redirectUri)}`;

        chrome.windows.create({
            url: authUrl,
            type: 'popup',
            width: 600,
            height: 700
        }, (window) => {
            sendResponse({ windowId: window.id });
        });

        return true; // Async response
    }

    if (request.action === 'getCookie') {
        chrome.cookies.get({
            url: 'https://www.dynamsoft.com',
            name: request.cookieName
        }, (cookie) => {
            sendResponse({ cookie: cookie });
        });

        return true;
    }

    if (request.action === 'requestTrialLicense') {
        fetch('https://www.dynamsoft.com/api-portal/Api/Trial/RequestFromWeb', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'DynamsoftToken': request.token,
                'DynamsoftUser': request.userId
            },
            body: JSON.stringify({
                Email: request.email,
                SolutionId: 2,
                PackageId: 13,
                RequestSource: 7
            })
        })
        .then(response => response.json())
        .then(data => sendResponse({ success: true, data: data }))
        .catch(error => sendResponse({ success: false, error: error.message }));

        return true;
    }
});
Enter fullscreen mode Exit fullscreen mode

Side Panel Authentication Logic

// app.js
async function getCookie(name) {
    return new Promise((resolve) => {
        chrome.runtime.sendMessage(
            { action: 'getCookie', cookieName: name },
            (response) => {
                resolve(response?.cookie?.value || null);
            }
        );
    });
}

// Check for existing auth on page load
(async function checkExistingAuth() {
    try {
        const token = await getCookie('DynamsoftToken');
        const userId = await getCookie('DynamsoftUser');

        if (token && userId) {
            // User is already logged in - hide login button
            loginButton.style.display = 'none';
            loginStatus.textContent = 'Restoring session...';

            // Request trial license directly
            await requestTrialLicense(
                decodeURIComponent(token), 
                decodeURIComponent(userId)
            );
        }
    } catch (error) {
        console.log('No existing auth found:', error);
    }
})();

async function requestTrialLicense(token, userId) {
    loginStatus.textContent = 'Getting user info...';

    chrome.runtime.sendMessage(
        {
            action: 'getUserInfo',
            token: token,
            userId: userId
        },
        (userInfoResponse) => {
            const email = userInfoResponse.data.email;
            const firstName = userInfoResponse.data.firstName;

            // Update user name in sidebar
            userNameElement.textContent = firstName;

            // Request license
            chrome.runtime.sendMessage(
                {
                    action: 'requestTrialLicense',
                    token: token,
                    userId: userId,
                    email: email
                },
                async (response) => {
                    const licenseKey = response.data.data.licenseKey;

                    localStorage.setItem('dynamsoft_license_expiry', 
                        response.data.data.expirationDate);

                    // Activate SDK
                    await activateSDK(licenseKey);

                    loginButton.style.display = 'none';
                    loginStatus.textContent = '✓ Licensed';
                }
            );
        }
    );
}
Enter fullscreen mode Exit fullscreen mode

Security Best Practice:

  • Store only expiration date for reference
  • Request fresh license on each load using auth tokens
  • Keep license in memory during session

Camera Scanning in New Tab

Due to Chrome extension limitations, camera access doesn't work reliably in side panels or popups. The solution is to open a dedicated scanner page in a new tab where camera permissions work properly.

barcode scanner ui design

Scanner Button Handler

// app.js
document.getElementById("scan").addEventListener('click', async () => {
    // Check if license exists and is valid
    const storedExpiry = localStorage.getItem('dynamsoft_license_expiry');
    if (!licenseKey || !storedExpiry) {
        alert('⚠️ No valid license. Please login to get a trial license first.');
        return;
    }

    const expiryDate = new Date(storedExpiry);
    const now = new Date();
    if (expiryDate <= now) {
        alert('⚠️ Your license has expired. Please login again to renew your trial license.');
        loginStatus.textContent = '⚠️ License expired. Please login again.';
        loginButton.style.display = 'block';
        return;
    }

    // Open scanner in a new tab for proper camera access
    chrome.tabs.create({
        url: chrome.runtime.getURL('scanner.html')
    });
});
Enter fullscreen mode Exit fullscreen mode

Scanner Page (scanner.html)

Create a dedicated scanner page with full-screen camera UI:

<!DOCTYPE html>
<html>
<head>
    <title>Barcode Scanner</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100vh;
            display: flex;
            flex-direction: column;
        }

        #scanner-container {
            flex: 1;
            position: relative;
        }

        #results {
            padding: 20px;
            background: #f5f5f5;
            border-top: 2px solid #ddd;
            max-height: 200px;
            overflow-y: auto;
        }

        .barcode-result {
            margin: 10px 0;
            padding: 10px;
            background: white;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <div id="scanner-container"></div>
    <div id="results"></div>

    <script src="libs/dynamsoft-barcode-reader-bundle/dist/dbr.bundle.js"></script>
    <script>
        let scanner;

        async function initScanner() {
            // Get license from storage
            const result = await chrome.storage.local.get(['licenseKey']);
            const licenseKey = result.licenseKey;

            if (!licenseKey) {
                alert('No license found. Please login first.');
                window.close();
                return;
            }

            // Initialize scanner with camera
            scanner = await Dynamsoft.DBR.BarcodeScanner.createInstance();
            scanner.updateRuntimeSettings('speed');

            // Set up UI
            document.getElementById('scanner-container').appendChild(scanner.getUIElement());

            // Handle scan results
            scanner.onUniqueRead = (txt, result) => {
                const resultsDiv = document.getElementById('results');
                const resultElement = document.createElement('div');
                resultElement.className = 'barcode-result';
                resultElement.innerHTML = `
                    <strong>${result.barcodeFormatString}</strong><br>
                    ${result.barcodeText}
                `;
                resultsDiv.insertBefore(resultElement, resultsDiv.firstChild);
            };

            // Start scanning
            await scanner.show();
        }

        initScanner().catch(console.error);
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Opens in new tab for proper camera permissions
  • Full-screen scanning experience
  • Real-time barcode detection with visual feedback
  • Displays results in a scrollable list
  • License validation before opening

File Upload (Images and Multi-Page PDFs)

The file upload handler supports both single images and multi-page PDF documents, processing each page separately for barcode detection.

read barcodes from uploaded files

File Input Handler

// app.js
document.getElementById("file").onchange = async function () {
    // Validate license
    const storedExpiry = localStorage.getItem('dynamsoft_license_expiry');
    if (!licenseKey || !storedExpiry) {
        alert('⚠️ No valid license. Please login to get a trial license first.');
        return;
    }

    const expiryDate = new Date(storedExpiry);
    const now = new Date();
    if (expiryDate <= now) {
        alert('⚠️ Your license has expired. Please login again to renew your trial license.');
        loginStatus.textContent = '⚠️ License expired. Please login again.';
        loginButton.style.display = 'block';
        return;
    }

    try {
        // Initialize barcode scanner
        if (barcodeScanner) {
            barcodeScanner.dispose();
        }
        barcodeScanner = new Dynamsoft.BarcodeScanner({
            license: licenseKey,
            scanMode: Dynamsoft.EnumScanMode.SM_MULTI_UNIQUE,
        });

        // Reset variables
        pdfPages = [];
        pageResults = [];
        currentPageIndex = 0;

        const files = Array.from(this.files || []);
        if (files.length) {
            toggleLoading(true);
            let fileToProcess = files[0];

            // Process the file and get all pages
            let pages = await processFile(fileToProcess);
            pdfPages = pages;
            pageResults = new Array(pages.length);

            // Process each page for barcodes
            for (let i = 0; i < pages.length; i++) {
                try {
                    let result = await barcodeScanner.decode(pages[i].blob);
                    pageResults[i] = result;
                } catch (error) {
                    console.error(`Error processing page ${i + 1}:`, error);
                    pageResults[i] = { items: [] };
                }
            }

            // Display results
            if (pages.length > 0) {
                imageContainer.style.display = "flex";
                displayPage(0);
                displayAllResults();
            }

            toggleLoading(false);
        }
    } catch (error) {
        console.error("Error processing file:", error);
        resultArea.value = `Error: ${error.message}`;
        toggleLoading(false);
    }
};
Enter fullscreen mode Exit fullscreen mode

PDF Processing with Dynamsoft Document Viewer

// app.js
async function processFile(fileToProcess) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();

        reader.onload = async function (e) {
            try {
                const blob = new Blob([e.target.result], { type: fileToProcess.type });

                if (fileToProcess.type !== "application/pdf") {
                    // Single image file
                    const url = URL.createObjectURL(blob);
                    resolve([{ blob, url }]);
                    return;
                }

                // PDF file - process all pages
                const source = {
                    fileData: blob,
                    renderOptions: {
                        renderAnnotations: "loadAnnotations"
                    }
                };

                currentDoc.deleteAllPages();
                await currentDoc.loadSource([source]);

                const settings = {
                    quality: 100,
                    saveAnnotation: false,
                };

                let pageCount = currentDoc.pages.length;
                let pages = [];

                // Convert each PDF page to JPEG
                for (let i = 0; i < pageCount; i++) {
                    const image = await currentDoc.saveToJpeg(i, settings);
                    const url = URL.createObjectURL(image);
                    pages.push({ blob: image, url });
                }

                resolve(pages);
            } catch (error) {
                reject(error);
            }
        };

        reader.onerror = reject;
        reader.readAsArrayBuffer(fileToProcess);
    });
}
Enter fullscreen mode Exit fullscreen mode

Page Navigation UI

// app.js
function displayPage(pageIndex) {
    currentPageIndex = pageIndex;

    // Display image
    const ctx = imageCanvas.getContext('2d');
    const img = new Image();
    img.onload = () => {
        imageCanvas.width = img.width;
        imageCanvas.height = img.height;
        ctx.drawImage(img, 0, 0);

        // Draw barcode highlights
        if (pageResults[pageIndex] && pageResults[pageIndex].items) {
            pageResults[pageIndex].items.forEach(item => {
                if (item.location) {
                    ctx.strokeStyle = 'lime';
                    ctx.lineWidth = 3;
                    ctx.beginPath();
                    ctx.moveTo(item.location.x1, item.location.y1);
                    ctx.lineTo(item.location.x2, item.location.y2);
                    ctx.lineTo(item.location.x3, item.location.y3);
                    ctx.lineTo(item.location.x4, item.location.y4);
                    ctx.closePath();
                    ctx.stroke();
                }
            });
        }
    };
    img.src = pdfPages[pageIndex].url;

    // Update navigation
    pageInfo.textContent = `Page ${pageIndex + 1} of ${pdfPages.length}`;
    prevPageBtn.disabled = pageIndex === 0;
    nextPageBtn.disabled = pageIndex === pdfPages.length - 1;

    displayCurrentPageResults();
}

function displayCurrentPageResults() {
    resultArea.value = `=== Page ${currentPageIndex + 1} of ${pdfPages.length} ===\n`;
    if (pageResults[currentPageIndex] && pageResults[currentPageIndex].items.length > 0) {
        pageResults[currentPageIndex].items.forEach(item => {
            resultArea.value += "Text: " + item.text + "\n";
            resultArea.value += "Format: " + item.formatString + "\n\n";
        });
    } else {
        resultArea.value += "No barcodes found on this page.\n";
    }
}

// Navigation buttons
prevPageBtn.addEventListener('click', () => {
    if (currentPageIndex > 0) {
        displayPage(currentPageIndex - 1);
    }
});

nextPageBtn.addEventListener('click', () => {
    if (currentPageIndex < pdfPages.length - 1) {
        displayPage(currentPageIndex + 1);
    }
});
Enter fullscreen mode Exit fullscreen mode

Key Features:

  • Multi-page PDF support - Converts PDF pages to JPEG for processing
  • Image file support - PNG, JPEG, GIF, BMP formats
  • Page-by-page navigation - View and navigate through PDF pages
  • Visual barcode highlighting - Draw boxes around detected barcodes
  • Unified results display - Summary across all pages + page-specific results
  • License validation - Checks expiry before processing

Screenshot Capture with Area Selection

Screenshot capture for barcode reading

Content Script Injection

When the user clicks the screenshot button, inject a content script that allows area selection:

// app.js - Screenshot button handler
screenshotBtn.addEventListener('click', async () => {
    try {
        const [tab] = await chrome.tabs.query({ 
            active: true, 
            currentWindow: true 
        });

        // Inject screenshot selector
        await chrome.scripting.executeScript({
            target: { tabId: tab.id },
            files: ['screenshot-selector.js']
        });
    } catch (error) {
        alert(`Error: ${error.message}`);
    }
});
Enter fullscreen mode Exit fullscreen mode

Screenshot Selector UI

Create screenshot-selector.js with drag-to-select functionality:

// screenshot-selector.js
(function() {
    // Create overlay
    const overlay = document.createElement('div');
    overlay.style.cssText = `
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.3);
        z-index: 999999;
        cursor: crosshair;
    `;

    // Custom green crosshair cursor
    const crosshairSVG = `data:image/svg+xml,${encodeURIComponent(`
        <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
            <line x1="16" y1="0" x2="16" y2="32" stroke="lime" stroke-width="2"/>
            <line x1="0" y1="16" x2="32" y2="16" stroke="lime" stroke-width="2"/>
            <circle cx="16" cy="16" r="8" fill="none" stroke="white" stroke-width="2"/>
        </svg>
    `)}`;
    overlay.style.cursor = `url('${crosshairSVG}') 16 16, crosshair`;

    // Selection box
    const selectionBox = document.createElement('div');
    selectionBox.style.cssText = `
        position: fixed;
        border: 2px solid lime;
        background: rgba(0, 255, 0, 0.1);
        z-index: 1000000;
        display: none;
    `;

    let startX, startY, isSelecting = false;

    overlay.addEventListener('mousedown', (e) => {
        isSelecting = true;
        startX = e.clientX;
        startY = e.clientY;

        selectionBox.style.left = startX + 'px';
        selectionBox.style.top = startY + 'px';
        selectionBox.style.width = '0';
        selectionBox.style.height = '0';
        selectionBox.style.display = 'block';
    });

    overlay.addEventListener('mousemove', (e) => {
        if (!isSelecting) return;

        const width = Math.abs(e.clientX - startX);
        const height = Math.abs(e.clientY - startY);
        const left = Math.min(e.clientX, startX);
        const top = Math.min(e.clientY, startY);

        selectionBox.style.left = left + 'px';
        selectionBox.style.top = top + 'px';
        selectionBox.style.width = width + 'px';
        selectionBox.style.height = height + 'px';
    });

    overlay.addEventListener('mouseup', async (e) => {
        if (!isSelecting) return;

        const width = Math.abs(e.clientX - startX);
        const height = Math.abs(e.clientY - startY);

        if (width < 10 || height < 10) {
            cleanup();
            return;
        }

        // Hide UI before capture
        overlay.style.display = 'none';
        selectionBox.style.display = 'none';

        // Wait for UI to hide
        await new Promise(resolve => setTimeout(resolve, 50));

        // Send capture request to background
        chrome.runtime.sendMessage({
            action: 'captureScreenshot',
            selection: {
                left: Math.min(e.clientX, startX),
                top: Math.min(e.clientY, startY),
                width: width,
                height: height,
                devicePixelRatio: window.devicePixelRatio
            }
        });

        cleanup();
    });

    // ESC to cancel
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
            cleanup();
        }
    });

    function cleanup() {
        overlay.remove();
        selectionBox.remove();
    }

    document.body.appendChild(overlay);
    document.body.appendChild(selectionBox);
})();
Enter fullscreen mode Exit fullscreen mode

Background Screenshot Capture

// background.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request.action === 'captureScreenshot') {
        chrome.tabs.captureVisibleTab(null, { format: 'png' }, (dataUrl) => {
            // Use OffscreenCanvas for cropping in service worker
            fetch(dataUrl)
                .then(res => res.blob())
                .then(blob => createImageBitmap(blob))
                .then(bitmap => {
                    const canvas = new OffscreenCanvas(
                        request.selection.width * request.selection.devicePixelRatio,
                        request.selection.height * request.selection.devicePixelRatio
                    );
                    const ctx = canvas.getContext('2d');

                    // Crop to selection
                    ctx.drawImage(
                        bitmap,
                        request.selection.left * request.selection.devicePixelRatio,
                        request.selection.top * request.selection.devicePixelRatio,
                        request.selection.width * request.selection.devicePixelRatio,
                        request.selection.height * request.selection.devicePixelRatio,
                        0, 0,
                        request.selection.width * request.selection.devicePixelRatio,
                        request.selection.height * request.selection.devicePixelRatio
                    );

                    return canvas.convertToBlob({ type: 'image/png' });
                })
                .then(blob => {
                    const reader = new FileReader();
                    reader.onloadend = () => {
                        // Broadcast to side panel
                        chrome.runtime.sendMessage({
                            action: 'screenshotResult',
                            success: true,
                            dataUrl: reader.result
                        });
                    };
                    reader.readAsDataURL(blob);
                });
        });

        return true;
    }
});
Enter fullscreen mode Exit fullscreen mode

Key Techniques:

  • Custom SVG cursor for better visibility
  • Drag-to-select with visual feedback
  • Hide overlay before capture (prevents green tint)
  • OffscreenCanvas for service worker compatibility
  • Message broadcasting to side panel

Context Menu Integration

Context menu barcode reading

Register Context Menu

// background.js
chrome.runtime.onInstalled.addListener(() => {
    chrome.contextMenus.create({
        id: 'decodeImage',
        title: 'Scan barcode from image',
        contexts: ['image']
    });
});

chrome.contextMenus.onClicked.addListener((info, tab) => {
    if (info.menuItemId === 'decodeImage') {
        // Open side panel
        chrome.sidePanel.open({ windowId: tab.windowId });

        // Send image URL to side panel
        setTimeout(() => {
            chrome.runtime.sendMessage({
                action: 'decodeImageUrl',
                imageUrl: info.srcUrl
            });
        }, 500);
    }
});
Enter fullscreen mode Exit fullscreen mode

Handle Context Menu in Side Panel

// app.js
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
    if (request.action === 'decodeImageUrl') {
        try {
            // Fetch the image
            const response = await fetch(request.imageUrl);
            const blob = await response.blob();
            const file = new File([blob], 'image.jpg', { type: blob.type });

            // Process with file input handler
            const fileInput = document.getElementById('file');
            const dataTransfer = new DataTransfer();
            dataTransfer.items.add(file);
            fileInput.files = dataTransfer.files;

            // Trigger change event
            fileInput.dispatchEvent(new Event('change', { bubbles: true }));
        } catch (error) {
            console.error('Error decoding image:', error);
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

Drag & Drop Support

Drag and drop barcode reading

HTML Drop Zone

<div id="dropZone" class="drop-zone">
    <div class="drop-icon">📥</div>
    <div class="drop-text">Drag & Drop images or PDFs here</div>
    <div class="drop-subtext">or click buttons above</div>
</div>
Enter fullscreen mode Exit fullscreen mode

Drag & Drop Handlers

// app.js
const dropZone = document.body;
const dropZoneElement = document.getElementById('dropZone');

[dropZone, dropZoneElement].forEach(element => {
    element.addEventListener('dragover', (e) => {
        e.preventDefault();
        e.stopPropagation();
        dropZoneElement.classList.add('drag-over');
    });

    element.addEventListener('dragleave', (e) => {
        e.preventDefault();
        e.stopPropagation();
        if (e.target === dropZone || e.target === dropZoneElement) {
            dropZoneElement.classList.remove('drag-over');
        }
    });

    element.addEventListener('drop', async (e) => {
        e.preventDefault();
        e.stopPropagation();
        dropZoneElement.classList.remove('drag-over');

        const files = e.dataTransfer.files;
        if (files.length > 0) {
            const file = files[0];

            // Validate file type
            const validTypes = ['image/png', 'image/jpeg', 'image/jpg', 
                               'image/gif', 'image/bmp', 'application/pdf'];
            if (!validTypes.includes(file.type)) {
                alert('Please drop an image or PDF file');
                return;
            }

            // Process file
            const fileInput = document.getElementById('file');
            const dataTransfer = new DataTransfer();
            dataTransfer.items.add(file);
            fileInput.files = dataTransfer.files;

            fileInput.dispatchEvent(new Event('change', { bubbles: true }));
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

CSS for Visual Feedback

.drop-zone {
    padding: 30px 20px;
    margin: 15px 0;
    border: 2px dashed rgba(255, 255, 255, 0.4);
    border-radius: 12px;
    background: rgba(255, 255, 255, 0.1);
    transition: all 0.3s ease;
    cursor: pointer;
}

.drop-zone:hover {
    border-color: rgba(255, 255, 255, 0.6);
    background: rgba(255, 255, 255, 0.15);
}

.drop-zone.drag-over {
    border-color: #fff;
    border-style: solid;
    background: rgba(255, 255, 255, 0.2);
    box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
}
Enter fullscreen mode Exit fullscreen mode

Security Best Practices

1. Use Background Script for CORS-Restricted APIs

// Side panel can't make CORS requests directly
// Use background script as proxy
chrome.runtime.sendMessage({
    action: 'requestTrialLicense',
    token: token,
    userId: userId,
    email: email
}, (response) => {
    // Process response
});
Enter fullscreen mode Exit fullscreen mode

2. Validate All User Inputs

// Validate file types
const validTypes = ['image/png', 'image/jpeg', 'application/pdf'];
if (!validTypes.includes(file.type)) {
    alert('Invalid file type');
    return;
}

// Validate license expiration
const expiryDate = new Date(storedExpiry);
if (expiryDate <= new Date()) {
    alert('License expired');
    return;
}
Enter fullscreen mode Exit fullscreen mode

3. Content Security Policy

{
    "content_security_policy": {
        "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 'self': Only load scripts from extension
  • 'wasm-unsafe-eval': Required for WebAssembly (Dynamsoft SDK)
  • No 'unsafe-inline': Prevents XSS attacks

4. Minimal Permissions

Only request permissions you actually use:

{
    "host_permissions": ["<all_urls>"],
    "permissions": [
        "storage",      // Settings and expiry date
        "cookies",      // OAuth authentication
        "activeTab",    // Screenshot current tab
        "scripting",    // Inject screenshot selector
        "sidePanel",    // Display UI
        "contextMenus"  // Right-click images
    ]
}
Enter fullscreen mode Exit fullscreen mode

Source Code

https://github.com/yushulx/javascript-barcode-qr-code-scanner/tree/main/examples/chrome_extension

Top comments (0)