UNPKG

pdq-wasm

Version:

WebAssembly bindings for Meta's PDQ perceptual image hashing algorithm

303 lines (273 loc) 9.58 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>PDQ WASM - Browser Example</title> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 900px; margin: 50px auto; padding: 0 20px; line-height: 1.6; } h1 { color: #333; border-bottom: 3px solid #007bff; padding-bottom: 10px; } .container { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; } .image-upload { margin: 20px 0; } input[type="file"] { margin: 10px 0; } button { background: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 14px; margin: 5px; } button:hover { background: #0056b3; } button:disabled { background: #ccc; cursor: not-allowed; } .result { background: white; padding: 15px; border-radius: 4px; margin: 15px 0; border-left: 4px solid #007bff; } .hash { font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all; background: #f4f4f4; padding: 10px; border-radius: 4px; margin: 10px 0; } .preview { max-width: 200px; margin: 10px 0; border-radius: 4px; border: 2px solid #ddd; } .comparison { background: #e7f3ff; padding: 15px; border-radius: 4px; margin: 15px 0; } .distance { font-size: 24px; font-weight: bold; color: #007bff; } .similarity { font-size: 18px; color: #28a745; } code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-family: 'Courier New', monospace; } </style> </head> <body> <h1>PDQ WASM - Browser Example</h1> <div class="container"> <p>This example demonstrates how to use the PDQ perceptual hashing algorithm in the browser using WebAssembly.</p> <p>Upload one or two images to generate their PDQ hashes and compare them.</p> </div> <div class="container"> <h2>Image 1</h2> <div class="image-upload"> <input type="file" id="image1Input" accept="image/*"> <button id="hash1Btn" disabled>Generate Hash</button> </div> <div id="result1"></div> </div> <div class="container"> <h2>Image 2</h2> <div class="image-upload"> <input type="file" id="image2Input" accept="image/*"> <button id="hash2Btn" disabled>Generate Hash</button> </div> <div id="result2"></div> </div> <div class="container"> <h2>Comparison</h2> <button id="compareBtn" disabled>Compare Images</button> <div id="comparisonResult"></div> </div> <div class="container"> <h2>How it works</h2> <ol> <li>The PDQ WASM module is loaded automatically when the page loads</li> <li>When you upload an image, it's decoded using the Canvas API</li> <li>The raw pixel data is passed to the PDQ algorithm</li> <li>A 256-bit perceptual hash is generated</li> <li>Two hashes can be compared using Hamming distance</li> </ol> <p><strong>Hamming Distance:</strong> The number of bits that differ between two hashes (0-256). Lower values indicate more similar images.</p> <p><strong>Similarity:</strong> Percentage similarity based on the Hamming distance (100% - (distance/256 * 100)).</p> </div> <!-- PDQ WASM - Zero Configuration Required! --> <script type="module"> // This example demonstrates the CDN-first approach // Option 1: Use CDN (recommended for getting started) // import { PDQ } from 'https://unpkg.com/pdq-wasm@0.3.9/dist/esm/index.js'; // Option 2: Use local build (for development) // You may need to serve this with a local server due to CORS restrictions import { PDQ } from '../../dist/esm/index.js'; let hash1 = null; let hash2 = null; // Initialize PDQ - Now with ZERO configuration! // The library automatically loads WASM from CDN if not specified console.log('Initializing PDQ WASM module...'); await PDQ.init(); // That's it! No wasmUrl needed for browsers console.log('PDQ WASM module initialized'); console.log('✓ WASM loaded automatically from CDN'); const image1Input = document.getElementById('image1Input'); const image2Input = document.getElementById('image2Input'); const hash1Btn = document.getElementById('hash1Btn'); const hash2Btn = document.getElementById('hash2Btn'); const compareBtn = document.getElementById('compareBtn'); const result1 = document.getElementById('result1'); const result2 = document.getElementById('result2'); const comparisonResult = document.getElementById('comparisonResult'); // Enable buttons when files are selected image1Input.addEventListener('change', () => { hash1Btn.disabled = !image1Input.files[0]; }); image2Input.addEventListener('change', () => { hash2Btn.disabled = !image2Input.files[0]; }); // Hash image 1 hash1Btn.addEventListener('click', async () => { const file = image1Input.files[0]; if (!file) return; try { hash1 = await hashImage(file); displayResult(result1, hash1, file); updateCompareButton(); } catch (err) { result1.innerHTML = `<div class="result" style="border-left-color: #dc3545;">Error: ${err.message}</div>`; } }); // Hash image 2 hash2Btn.addEventListener('click', async () => { const file = image2Input.files[0]; if (!file) return; try { hash2 = await hashImage(file); displayResult(result2, hash2, file); updateCompareButton(); } catch (err) { result2.innerHTML = `<div class="result" style="border-left-color: #dc3545;">Error: ${err.message}</div>`; } }); // Compare hashes compareBtn.addEventListener('click', () => { if (!hash1 || !hash2) return; const distance = PDQ.hammingDistance(hash1.hash, hash2.hash); const similarity = PDQ.similarity(hash1.hash, hash2.hash); const isSimilar = PDQ.areSimilar(hash1.hash, hash2.hash); // Uses default threshold of 31 comparisonResult.innerHTML = ` <div class="comparison"> <h3>Comparison Results</h3> <p><strong>Hamming Distance:</strong> <span class="distance">${distance}</span> / 256</p> <p><strong>Similarity:</strong> <span class="similarity">${similarity.toFixed(2)}%</span></p> <p><strong>Are they similar?</strong> ${isSimilar ? '✅ Yes (distance ≤ 31)' : '❌ No (distance > 31)'}</p> <p><small>PDQ uses a recommended threshold of 31 bits difference for determining duplicates.</small></p> </div> `; }); function updateCompareButton() { compareBtn.disabled = !(hash1 && hash2); } async function hashImage(file) { // Create an image element const img = new Image(); const url = URL.createObjectURL(file); return new Promise((resolve, reject) => { img.onload = () => { try { // Create canvas and get image data const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, img.width, img.height); // Extract RGB data (PDQ expects RGB or grayscale) const rgbData = new Uint8Array(img.width * img.height * 3); let rgbIndex = 0; for (let i = 0; i < imageData.data.length; i += 4) { rgbData[rgbIndex++] = imageData.data[i]; // R rgbData[rgbIndex++] = imageData.data[i + 1]; // G rgbData[rgbIndex++] = imageData.data[i + 2]; // B // Skip alpha channel (imageData.data[i + 3]) } // Generate PDQ hash const result = PDQ.hash({ data: rgbData, width: img.width, height: img.height, channels: 3 }); URL.revokeObjectURL(url); resolve({ hash: result.hash, quality: result.quality, hex: PDQ.toHex(result.hash), width: img.width, height: img.height, url: URL.createObjectURL(file) }); } catch (err) { URL.revokeObjectURL(url); reject(err); } }; img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Failed to load image')); }; img.src = url; }); } function displayResult(element, result, file) { element.innerHTML = ` <div class="result"> <img src="${result.url}" class="preview" alt="Preview"> <p><strong>Dimensions:</strong> ${result.width} x ${result.height}</p> <p><strong>Quality:</strong> ${result.quality}</p> <p><strong>Hash (hex):</strong></p> <div class="hash">${result.hex}</div> <p><strong>Hash (first 8 bytes):</strong></p> <div class="hash">${Array.from(result.hash.slice(0, 8)).map(b => b.toString(16).padStart(2, '0')).join(' ')}</div> </div> `; } </script> </body> </html>