document.addEventListener('DOMContentLoaded', () => { // DOM elements const imageUpload = document.getElementById('imageUpload'); const preview = document.getElementById('preview'); const scanCanvas = document.getElementById('scanCanvas'); const status = document.getElementById('status'); const result = document.getElementById('result'); // Camera elements - will be created dynamically let videoElement; let cameraContainer; let cameraControls; let startCameraButton; let stopCameraButton; let switchCameraButton; let enhancementToggle; let debugModeToggle; let cameraStream = null; let activeCameraId = null; let availableCameras = []; let isScanning = false; let scanInterval = null; let useImageEnhancement = true; // Default to true let debugMode = false; // Default to false let focusModeActive = false; // Check if ZXing library is loaded if (typeof ZXing === 'undefined') { status.textContent = 'Error: ZXing library not loaded'; result.textContent = 'Please check your internet connection and reload the page.'; console.error('ZXing library not loaded'); return; } // Initialize ZXing code reader and hints const codeReader = new ZXing.BrowserMultiFormatReader(); const hints = new Map(); const formats = [ ZXing.BarcodeFormat.QR_CODE, ZXing.BarcodeFormat.DATA_MATRIX, ZXing.BarcodeFormat.AZTEC, ZXing.BarcodeFormat.PDF_417, ZXing.BarcodeFormat.EAN_13, ZXing.BarcodeFormat.EAN_8, ZXing.BarcodeFormat.UPC_A, ZXing.BarcodeFormat.UPC_E, ZXing.BarcodeFormat.CODE_128, ZXing.BarcodeFormat.CODE_39, ZXing.BarcodeFormat.CODE_93, ZXing.BarcodeFormat.ITF, ZXing.BarcodeFormat.CODABAR ]; hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, formats); hints.set(ZXing.DecodeHintType.TRY_HARDER, true); // Initialize the app function init() { status.textContent = 'Ready to scan barcodes. Upload an image or use camera.'; createCameraElements(); // Check if we're in a secure context (HTTPS or localhost) if (!isSecureContext()) { status.textContent = 'Camera access requires HTTPS. Current connection is not secure.'; const warningDiv = document.createElement('div'); warningDiv.className = 'security-warning'; warningDiv.innerHTML = 'Security Warning: Camera access requires a secure connection (HTTPS). ' + 'You are currently on an insecure connection, which may prevent camera access. ' + 'Please access this page via HTTPS or use localhost for testing.'; document.querySelector('.container').insertBefore(warningDiv, document.querySelector('.upload-section')); } checkCameraSupport(); } // Check if we're in a secure context (HTTPS or localhost) function isSecureContext() { // Check if the context is secure using the SecureContext API if (window.isSecureContext === true) { return true; } // Fallback check for older browsers return location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1'; } // Create camera UI elements function createCameraElements() { // Create camera container cameraContainer = document.createElement('div'); cameraContainer.id = 'camera-container'; cameraContainer.className = 'camera-container'; cameraContainer.style.display = 'none'; // Create video element videoElement = document.createElement('video'); videoElement.id = 'camera-feed'; videoElement.autoplay = true; videoElement.playsInline = true; // Create camera controls cameraControls = document.createElement('div'); cameraControls.className = 'camera-controls'; // Create camera buttons startCameraButton = document.createElement('button'); startCameraButton.id = 'start-camera'; startCameraButton.textContent = 'Start Camera'; startCameraButton.addEventListener('click', startCamera); stopCameraButton = document.createElement('button'); stopCameraButton.id = 'stop-camera'; stopCameraButton.textContent = 'Stop Camera'; stopCameraButton.style.display = 'none'; stopCameraButton.addEventListener('click', stopCamera); switchCameraButton = document.createElement('button'); switchCameraButton.id = 'switch-camera'; switchCameraButton.textContent = 'Switch Camera'; switchCameraButton.style.display = 'none'; switchCameraButton.addEventListener('click', switchCamera); // Create capture image button const captureImageButton = document.createElement('button'); captureImageButton.id = 'capture-image'; captureImageButton.textContent = 'Capture Image'; captureImageButton.style.display = 'none'; captureImageButton.addEventListener('click', captureImage); // Create focus mode button const focusModeButton = document.createElement('button'); focusModeButton.id = 'focus-mode'; focusModeButton.textContent = 'Focus Mode'; focusModeButton.style.display = 'none'; focusModeButton.addEventListener('click', toggleFocusMode); // Create retry button for permission issues const retryPermissionButton = document.createElement('button'); retryPermissionButton.id = 'retry-permission'; retryPermissionButton.textContent = 'Retry Camera Permission'; retryPermissionButton.style.display = 'none'; retryPermissionButton.addEventListener('click', () => { retryPermissionButton.style.display = 'none'; status.textContent = 'Requesting camera permission...'; checkCameraSupport(); }); // Create enhancement toggle const enhancementContainer = document.createElement('div'); enhancementContainer.className = 'enhancement-toggle-container'; enhancementToggle = document.createElement('input'); enhancementToggle.type = 'checkbox'; enhancementToggle.id = 'enhancement-toggle'; enhancementToggle.checked = useImageEnhancement; enhancementToggle.addEventListener('change', (e) => { useImageEnhancement = e.target.checked; status.textContent = useImageEnhancement ? 'Image enhancement enabled. This may help detect barcodes in difficult lighting.' : 'Image enhancement disabled. Use this if barcodes are not being detected correctly.'; }); const enhancementLabel = document.createElement('label'); enhancementLabel.htmlFor = 'enhancement-toggle'; enhancementLabel.textContent = 'Enable image enhancement'; enhancementContainer.appendChild(enhancementToggle); enhancementContainer.appendChild(enhancementLabel); // Create debug mode toggle const debugContainer = document.createElement('div'); debugContainer.className = 'enhancement-toggle-container debug-toggle-container'; debugModeToggle = document.createElement('input'); debugModeToggle.type = 'checkbox'; debugModeToggle.id = 'debug-toggle'; debugModeToggle.checked = debugMode; debugModeToggle.addEventListener('change', (e) => { debugMode = e.target.checked; status.textContent = debugMode ? 'Debug mode enabled. Processing details will be shown.' : 'Debug mode disabled.'; // Create or remove debug info element let debugInfo = document.getElementById('debug-info'); if (debugMode) { if (!debugInfo) { debugInfo = document.createElement('div'); debugInfo.id = 'debug-info'; debugInfo.className = 'debug-info'; result.parentNode.appendChild(debugInfo); } } else { if (debugInfo) { debugInfo.remove(); } } }); const debugLabel = document.createElement('label'); debugLabel.htmlFor = 'debug-toggle'; debugLabel.textContent = 'Debug mode'; debugContainer.appendChild(debugModeToggle); debugContainer.appendChild(debugLabel); // Append elements cameraControls.appendChild(startCameraButton); cameraControls.appendChild(stopCameraButton); cameraControls.appendChild(switchCameraButton); cameraControls.appendChild(captureImageButton); cameraControls.appendChild(focusModeButton); cameraControls.appendChild(retryPermissionButton); cameraControls.appendChild(enhancementContainer); cameraControls.appendChild(debugContainer); cameraContainer.appendChild(videoElement); // Insert camera elements into the DOM const uploadSection = document.querySelector('.upload-section'); uploadSection.parentNode.insertBefore(cameraContainer, uploadSection.nextSibling); uploadSection.parentNode.insertBefore(cameraControls, uploadSection.nextSibling); } // Check if camera is supported function checkCameraSupport() { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { console.warn('Camera access not supported in this browser'); startCameraButton.disabled = true; startCameraButton.title = 'Camera not supported in this browser'; status.textContent = 'Camera not supported in this browser. Try using a modern browser like Chrome, Firefox, or Edge.'; return false; } // First try to access the camera to trigger permission prompt navigator.mediaDevices.getUserMedia({ video: true }) .then(stream => { // Stop the stream immediately after getting permission stream.getTracks().forEach(track => track.stop()); // Now enumerate devices after permission is granted return navigator.mediaDevices.enumerateDevices(); }) .then(devices => { availableCameras = devices.filter(device => device.kind === 'videoinput'); console.log('Available cameras:', availableCameras); if (availableCameras.length === 0) { startCameraButton.disabled = true; startCameraButton.title = 'No cameras detected'; status.textContent = 'No cameras detected on your device'; return; } // Enable camera button startCameraButton.disabled = false; startCameraButton.title = 'Start camera for barcode scanning'; status.textContent = 'Camera permission granted. Click "Start Camera" to begin scanning.'; // Show switch camera button if multiple cameras available if (availableCameras.length > 1) { switchCameraButton.style.display = 'inline-block'; } }) .catch(error => { console.error('Error accessing camera:', error); if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') { status.textContent = 'Camera permission denied. Please click "Retry Camera Permission" and allow camera access when prompted.'; startCameraButton.disabled = true; startCameraButton.title = 'Camera permission denied'; // Show retry permission button const retryButton = document.getElementById('retry-permission'); if (retryButton) { retryButton.style.display = 'inline-block'; } } else { // Try to enumerate devices anyway, in case permissions were granted before navigator.mediaDevices.enumerateDevices() .then(devices => { availableCameras = devices.filter(device => device.kind === 'videoinput'); console.log('Available cameras (without permission):', availableCameras); if (availableCameras.length === 0) { startCameraButton.disabled = true; startCameraButton.title = 'No cameras detected'; status.textContent = 'No cameras detected on your device'; } else { startCameraButton.disabled = false; startCameraButton.title = 'Start camera for barcode scanning'; if (availableCameras.length > 1) { switchCameraButton.style.display = 'inline-block'; } } }) .catch(enumError => { console.error('Error enumerating devices:', enumError); startCameraButton.disabled = true; startCameraButton.title = 'Error accessing camera information'; status.textContent = 'Error accessing camera. Please check if your camera is connected and working properly.'; }); } }); return true; } // Start camera function startCamera() { // Hide image preview and show camera preview.style.display = 'none'; cameraContainer.style.display = 'block'; // Update buttons startCameraButton.style.display = 'none'; stopCameraButton.style.display = 'inline-block'; // Show focus mode and capture image buttons const focusModeButton = document.getElementById('focus-mode'); const captureImageButton = document.getElementById('capture-image'); if (focusModeButton) { focusModeButton.style.display = 'inline-block'; } if (captureImageButton) { captureImageButton.style.display = 'inline-block'; } // Clear previous results status.textContent = 'Starting camera...'; result.textContent = ''; // Try to get cameras again if none were detected initially if (availableCameras.length === 0) { navigator.mediaDevices.enumerateDevices() .then(devices => { availableCameras = devices.filter(device => device.kind === 'videoinput'); console.log('Re-checking available cameras:', availableCameras); continueStartCamera(); }) .catch(error => { console.error('Error re-enumerating devices:', error); continueStartCamera(); }); } else { continueStartCamera(); } } // Continue camera startup after checking for cameras function continueStartCamera() { // Select camera (use first camera by default or previously selected) const cameraId = activeCameraId || (availableCameras.length > 0 ? availableCameras[0].deviceId : null); if (!cameraId) { // Try a more generic approach if no specific camera ID is available const constraints = { video: { facingMode: 'environment' // Prefer back camera } }; startCameraWithConstraints(constraints); } else { // Use specific camera ID const constraints = { video: { deviceId: { exact: cameraId }, width: { ideal: 1280 }, height: { ideal: 720 } } }; startCameraWithConstraints(constraints); } } // Start camera with specific constraints function startCameraWithConstraints(constraints) { navigator.mediaDevices.getUserMedia(constraints) .then(stream => { cameraStream = stream; videoElement.srcObject = stream; // Get the actual device ID from the stream const videoTrack = stream.getVideoTracks()[0]; if (videoTrack) { const settings = videoTrack.getSettings(); activeCameraId = settings.deviceId; console.log('Active camera ID:', activeCameraId); console.log('Camera settings:', settings); } // Wait for video to be ready videoElement.onloadedmetadata = () => { status.textContent = 'Camera active. Point at a barcode to scan.'; startBarcodeScanning(); }; }) .catch(error => { console.error('Error starting camera with constraints:', error, constraints); // If failed with specific constraints, try generic constraints if (constraints.video.deviceId) { console.log('Trying generic camera constraints...'); startCameraWithConstraints({ video: { facingMode: 'environment' } }); } else { status.textContent = 'Error starting camera: ' + (error.message || 'Unknown error'); stopCamera(); } }); } // Stop camera function stopCamera() { // Stop scanning stopBarcodeScanning(); // Stop camera stream if (cameraStream) { cameraStream.getTracks().forEach(track => track.stop()); cameraStream = null; } // Update UI videoElement.srcObject = null; cameraContainer.style.display = 'none'; startCameraButton.style.display = 'inline-block'; stopCameraButton.style.display = 'none'; // Hide focus mode and capture image buttons const focusModeButton = document.getElementById('focus-mode'); const captureImageButton = document.getElementById('capture-image'); if (focusModeButton) { focusModeButton.style.display = 'none'; } if (captureImageButton) { captureImageButton.style.display = 'none'; } // Remove focus mode if active document.body.classList.remove('focus-mode-active'); status.textContent = 'Camera stopped. Upload an image or restart camera.'; } // Switch between available cameras function switchCamera() { if (availableCameras.length <= 1) return; // Find next camera in the list const currentIndex = availableCameras.findIndex(camera => camera.deviceId === activeCameraId); const nextIndex = (currentIndex + 1) % availableCameras.length; activeCameraId = availableCameras[nextIndex].deviceId; // Restart camera with new device if (cameraStream) { stopCamera(); startCamera(); } } // Start continuous barcode scanning function startBarcodeScanning() { if (isScanning) return; isScanning = true; // Add scanning indicator const scanIndicator = document.createElement('div'); scanIndicator.id = 'scan-indicator'; scanIndicator.className = 'scan-indicator'; cameraContainer.appendChild(scanIndicator); // Process frames at regular intervals scanInterval = setInterval(() => { if (!videoElement || !cameraStream) return; // Capture current frame const width = videoElement.videoWidth; const height = videoElement.videoHeight; if (width === 0 || height === 0) return; // Skip if video dimensions aren't available yet // Set canvas dimensions to match video scanCanvas.width = width; scanCanvas.height = height; // Draw video frame on canvas const ctx = scanCanvas.getContext('2d'); ctx.drawImage(videoElement, 0, 0, width, height); // Add scanning guide overlay drawScanningGuide(ctx, width, height); // Process the frame processVideoFrame(ctx, width, height); }, 100); // Scan more frequently (every 100ms instead of 200ms) } // Stop barcode scanning function stopBarcodeScanning() { isScanning = false; if (scanInterval) { clearInterval(scanInterval); scanInterval = null; } // Remove scanning indicator if it exists const scanIndicator = document.getElementById('scan-indicator'); if (scanIndicator) { scanIndicator.remove(); } } // Draw scanning guide overlay function drawScanningGuide(ctx, width, height) { // Draw a semi-transparent guide rectangle in the center const guideSize = Math.min(width, height) * (focusModeActive ? 0.5 : 0.7); // Smaller guide in focus mode const x = (width - guideSize) / 2; const y = (height - guideSize) / 2; // Draw outer darkened area ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; ctx.fillRect(0, 0, width, height); // Draw transparent center ctx.clearRect(x, y, guideSize, guideSize); // Draw guide border ctx.strokeStyle = focusModeActive ? 'rgba(231, 76, 60, 0.8)' : 'rgba(52, 152, 219, 0.8)'; ctx.lineWidth = focusModeActive ? 6 : 4; ctx.strokeRect(x, y, guideSize, guideSize); // Draw corner markers const markerLength = guideSize * 0.1; ctx.lineWidth = focusModeActive ? 8 : 6; // Top-left corner ctx.beginPath(); ctx.moveTo(x, y + markerLength); ctx.lineTo(x, y); ctx.lineTo(x + markerLength, y); ctx.stroke(); // Top-right corner ctx.beginPath(); ctx.moveTo(x + guideSize - markerLength, y); ctx.lineTo(x + guideSize, y); ctx.lineTo(x + guideSize, y + markerLength); ctx.stroke(); // Bottom-right corner ctx.beginPath(); ctx.moveTo(x + guideSize, y + guideSize - markerLength); ctx.lineTo(x + guideSize, y + guideSize); ctx.lineTo(x + guideSize - markerLength, y + guideSize); ctx.stroke(); // Bottom-left corner ctx.beginPath(); ctx.moveTo(x + markerLength, y + guideSize); ctx.lineTo(x, y + guideSize); ctx.lineTo(x, y + guideSize - markerLength); ctx.stroke(); // Add text instruction ctx.font = '16px Arial'; ctx.fillStyle = 'white'; ctx.textAlign = 'center'; if (focusModeActive) { ctx.fillText('FOCUS MODE: Position barcode in the red box', width / 2, y + guideSize + 30); // Add crosshair in focus mode const centerX = x + guideSize / 2; const centerY = y + guideSize / 2; const crosshairSize = guideSize * 0.1; ctx.strokeStyle = 'rgba(231, 76, 60, 0.8)'; ctx.lineWidth = 2; // Horizontal line ctx.beginPath(); ctx.moveTo(centerX - crosshairSize, centerY); ctx.lineTo(centerX + crosshairSize, centerY); ctx.stroke(); // Vertical line ctx.beginPath(); ctx.moveTo(centerX, centerY - crosshairSize); ctx.lineTo(centerX, centerY + crosshairSize); ctx.stroke(); } else { ctx.fillText('Position barcode within the box', width / 2, y + guideSize + 30); } } // Process video frame for barcode detection function processVideoFrame(ctx, width, height) { try { // Pulse the scan indicator to show active scanning const scanIndicator = document.getElementById('scan-indicator'); if (scanIndicator) { scanIndicator.classList.add('pulse'); setTimeout(() => { scanIndicator.classList.remove('pulse'); }, 50); } // Update debug info if (debugMode) { updateDebugInfo('Processing frame', { width, height }); } // Store original image data for fallback const originalImageData = ctx.getImageData(0, 0, width, height); // Apply image enhancements to improve detection if enabled if (useImageEnhancement) { enhanceImage(ctx, width, height); if (debugMode) { updateDebugInfo('Image enhancement applied'); } } // Create a data URL from the canvas for the BrowserMultiFormatReader const dataUrl = scanCanvas.toDataURL('image/png'); // Try direct MultiFormatReader approach first (more reliable for video) try { // Create a ZXing HTMLCanvasElementLuminanceSource const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(scanCanvas); // Create a MultiFormatReader with hints const reader = new ZXing.MultiFormatReader(); // Set up hints with all supported formats and try harder flag const newHints = new Map(); newHints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, formats); newHints.set(ZXing.DecodeHintType.TRY_HARDER, true); // Add these additional hints for better video frame detection newHints.set(ZXing.DecodeHintType.PURE_BARCODE, false); newHints.set(ZXing.DecodeHintType.CHARACTER_SET, "UTF-8"); newHints.set(ZXing.DecodeHintType.ASSUME_GS1, false); reader.setHints(newHints); // Try different binarizers for better detection const binarizers = [ new ZXing.HybridBinarizer(luminanceSource), new ZXing.GlobalHistogramBinarizer(luminanceSource) ]; let decodedResult = null; let usedBinarizer = ''; let errorMessages = []; // Try each binarizer for (const binarizer of binarizers) { if (decodedResult) break; // Stop if we already found a result const binaryBitmap = new ZXing.BinaryBitmap(binarizer); usedBinarizer = binarizer instanceof ZXing.HybridBinarizer ? 'HybridBinarizer' : 'GlobalHistogramBinarizer'; if (debugMode) { updateDebugInfo(`Trying ${usedBinarizer}`); } try { // Try to decode the image using the binary bitmap const result = reader.decode(binaryBitmap); decodedResult = { text: result.getText(), format: result.getBarcodeFormat(), resultPoints: result.getResultPoints() }; if (debugMode) { updateDebugInfo(`Success with ${usedBinarizer}`, decodedResult); } } catch (error) { // Store error for debugging errorMessages.push(`${usedBinarizer}: ${error.message || 'Unknown error'}`); if (debugMode) { updateDebugInfo(`Failed with ${usedBinarizer}`, { error: error.message || 'Unknown error' }); } } } // If we got a result from the direct approach if (decodedResult) { // Barcode detected successfully status.textContent = 'Barcode recognized successfully!'; // Format the result const resultText = `Code: ${decodedResult.text}\nFormat: ${decodedResult.format}`; document.getElementById('result').textContent = resultText; // Draw the barcode location on the canvas if (decodedResult.resultPoints && decodedResult.resultPoints.length > 0) { drawBarcodeLocation(ctx, decodedResult.resultPoints); } // Play success sound playSuccessBeep(); // Pause scanning briefly after successful detection stopBarcodeScanning(); setTimeout(startBarcodeScanning, 2000); return; } // If direct approach failed, try with original image if we enhanced it if (useImageEnhancement) { // Restore original image ctx.putImageData(originalImageData, 0, 0); if (debugMode) { updateDebugInfo('Trying with original (non-enhanced) image'); } // Try again with original image for (const binarizer of binarizers) { if (decodedResult) break; // Stop if we already found a result const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(scanCanvas); const binaryBitmap = new ZXing.BinaryBitmap(binarizer); usedBinarizer = binarizer instanceof ZXing.HybridBinarizer ? 'HybridBinarizer (original)' : 'GlobalHistogramBinarizer (original)'; try { // Try to decode the image using the binary bitmap const result = reader.decode(binaryBitmap); decodedResult = { text: result.getText(), format: result.getBarcodeFormat(), resultPoints: result.getResultPoints() }; if (debugMode) { updateDebugInfo(`Success with ${usedBinarizer}`, decodedResult); } } catch (error) { // Store error for debugging errorMessages.push(`${usedBinarizer}: ${error.message || 'Unknown error'}`); if (debugMode) { updateDebugInfo(`Failed with ${usedBinarizer}`, { error: error.message || 'Unknown error' }); } } } // If we got a result from the original image if (decodedResult) { // Barcode detected successfully status.textContent = 'Barcode recognized successfully!'; // Format the result const resultText = `Code: ${decodedResult.text}\nFormat: ${decodedResult.format}`; document.getElementById('result').textContent = resultText; // Draw the barcode location on the canvas if (decodedResult.resultPoints && decodedResult.resultPoints.length > 0) { drawBarcodeLocation(ctx, decodedResult.resultPoints); } // Play success sound playSuccessBeep(); // Pause scanning briefly after successful detection stopBarcodeScanning(); setTimeout(startBarcodeScanning, 2000); return; } } // If all direct methods failed, fall back to BrowserMultiFormatReader if (debugMode) { updateDebugInfo('Trying BrowserMultiFormatReader fallback'); } // Use the BrowserMultiFormatReader to decode the image codeReader.decodeFromImageUrl(dataUrl) .then(result => { if (result) { // Barcode detected successfully status.textContent = 'Barcode recognized successfully!'; // Format the result const resultText = `Code: ${result.text}\nFormat: ${result.format}`; document.getElementById('result').textContent = resultText; if (debugMode) { updateDebugInfo('Success with BrowserMultiFormatReader', { text: result.text, format: result.format }); } // Draw the barcode location on the canvas if available if (result.resultPoints && result.resultPoints.length > 0) { drawBarcodeLocation(ctx, result.resultPoints); } // Play success sound playSuccessBeep(); // Pause scanning briefly after successful detection stopBarcodeScanning(); setTimeout(startBarcodeScanning, 2000); } }) .catch((error) => { // No barcode detected with any method if (debugMode) { updateDebugInfo('Failed with all methods', { error: error.message || 'Unknown error', allErrors: errorMessages.join(', ') }); } }); } catch (directError) { // If direct MultiFormatReader approach fails completely, fall back to BrowserMultiFormatReader if (debugMode) { updateDebugInfo('Direct MultiFormatReader failed, trying BrowserMultiFormatReader', { error: directError.message || 'Unknown error' }); } // Use the BrowserMultiFormatReader to decode the image codeReader.decodeFromImageUrl(dataUrl) .then(result => { if (result) { // Barcode detected successfully status.textContent = 'Barcode recognized successfully!'; // Format the result const resultText = `Code: ${result.text}\nFormat: ${result.format}`; document.getElementById('result').textContent = resultText; if (debugMode) { updateDebugInfo('Success with BrowserMultiFormatReader', { text: result.text, format: result.format }); } // Draw the barcode location on the canvas if available if (result.resultPoints && result.resultPoints.length > 0) { drawBarcodeLocation(ctx, result.resultPoints); } // Play success sound playSuccessBeep(); // Pause scanning briefly after successful detection stopBarcodeScanning(); setTimeout(startBarcodeScanning, 2000); } }) .catch((error) => { // No barcode detected with any method if (debugMode) { updateDebugInfo('Failed with all methods', { error: error.message || 'Unknown error' }); } }); } } catch (error) { console.error('Error processing video frame:', error); if (debugMode) { updateDebugInfo('Error processing frame', { error: error.message || 'Unknown error' }); } } } // Update debug information function updateDebugInfo(action, data = {}) { if (!debugMode) return; const debugInfo = document.getElementById('debug-info'); if (!debugInfo) return; const timestamp = new Date().toLocaleTimeString(); const entry = document.createElement('div'); entry.className = 'debug-entry'; let content = `${timestamp}: ${action}`; if (Object.keys(data).length > 0) { content += ''; } entry.innerHTML = content; // Add to the top if (debugInfo.firstChild) { debugInfo.insertBefore(entry, debugInfo.firstChild); } else { debugInfo.appendChild(entry); } // Limit entries to 10 while (debugInfo.children.length > 10) { debugInfo.removeChild(debugInfo.lastChild); } } // Enhance image to improve barcode detection function enhanceImage(ctx, width, height) { try { // Get image data const imageData = ctx.getImageData(0, 0, width, height); const data = imageData.data; // Try different enhancement approaches // First, store the original image data const originalData = new Uint8ClampedArray(data); // Image enhancement parameters - adjust based on focus mode const brightness = focusModeActive ? 20 : 15; // -255 to 255 const contrast = focusModeActive ? 1.4 : 1.3; // 0 to 2+ const threshold = focusModeActive ? 135 : 128; // 0 to 255 (middle value for better results) // Apply brightness, contrast, and threshold for (let i = 0; i < data.length; i += 4) { // Apply brightness data[i] += brightness; // R data[i + 1] += brightness; // G data[i + 2] += brightness; // B // Apply contrast data[i] = Math.min(255, Math.max(0, ((data[i] - 128) * contrast) + 128)); // R data[i + 1] = Math.min(255, Math.max(0, ((data[i + 1] - 128) * contrast) + 128)); // G data[i + 2] = Math.min(255, Math.max(0, ((data[i + 2] - 128) * contrast) + 128)); // B // Calculate grayscale value const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; // Apply threshold for better barcode detection // This creates higher contrast between dark and light areas if (gray < threshold) { data[i] = 0; // R data[i + 1] = 0; // G data[i + 2] = 0; // B } else { data[i] = 255; // R data[i + 1] = 255; // G data[i + 2] = 255; // B } } // Put the modified image data back on the canvas ctx.putImageData(imageData, 0, 0); // Apply sharpening filter applySharpening(ctx, width, height); // Store this enhanced version for potential fallback const enhancedImageData = ctx.getImageData(0, 0, width, height); // If debug mode is on, we'll keep the enhanced image visible if (!debugMode) { // For non-debug mode, we'll try a less aggressive approach as a fallback // This will be used in the next scan cycle if the current one fails window.lastEnhancedImageData = enhancedImageData; window.originalImageData = originalData; window.lastImageDimensions = { width, height }; } } catch (error) { console.error('Error enhancing image:', error); } } // Apply sharpening filter to the image function applySharpening(ctx, width, height) { try { // Get image data const imageData = ctx.getImageData(0, 0, width, height); const data = imageData.data; const dataBackup = new Uint8ClampedArray(data); // Sharpening kernel - less aggressive const kernel = [ 0, -0.5, 0, -0.5, 3, -0.5, 0, -0.5, 0 ]; // Apply convolution for (let y = 1; y < height - 1; y++) { for (let x = 1; x < width - 1; x++) { const offset = (y * width + x) * 4; // For each color channel for (let c = 0; c < 3; c++) { let val = 0; // Apply kernel for (let ky = -1; ky <= 1; ky++) { for (let kx = -1; kx <= 1; kx++) { const idx = ((y + ky) * width + (x + kx)) * 4 + c; val += dataBackup[idx] * kernel[(ky + 1) * 3 + (kx + 1)]; } } // Set the new value data[offset + c] = Math.min(255, Math.max(0, val)); } } } // Put the modified image data back on the canvas ctx.putImageData(imageData, 0, 0); } catch (error) { console.error('Error applying sharpening:', error); } } // Play a success beep sound function playSuccessBeep() { try { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.type = 'sine'; oscillator.frequency.setValueAtTime(1800, audioContext.currentTime); oscillator.frequency.setValueAtTime(1200, audioContext.currentTime + 0.1); gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.3); } catch (error) { console.error('Error playing success beep:', error); } } // Handle image upload imageUpload.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; // Stop camera if running stopCamera(); // Check if file is an image if (!file.type.match('image.*')) { status.textContent = 'Error: Not an image file'; result.textContent = 'Please upload an image file (JPEG, PNG, etc.)'; return; } // Display image preview const reader = new FileReader(); reader.onload = (event) => { preview.src = event.target.result; preview.style.display = 'block'; // Process the image with ZXing after it's loaded preview.onload = () => { processImage(preview); }; }; reader.onerror = () => { status.textContent = 'Error: Failed to read file'; result.textContent = 'There was an error reading the file. Please try again.'; }; reader.readAsDataURL(file); }); // Process image with ZXing async function processImage(imageElement) { status.textContent = 'Processing image...'; result.textContent = ''; try { // Get image dimensions const width = imageElement.naturalWidth; const height = imageElement.naturalHeight; // Set canvas dimensions to match image scanCanvas.width = width; scanCanvas.height = height; // Draw image on canvas const ctx = scanCanvas.getContext('2d'); ctx.drawImage(imageElement, 0, 0, width, height); // Create a ZXing HTMLCanvasElementLuminanceSource const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(scanCanvas); const binaryBitmap = new ZXing.BinaryBitmap(new ZXing.HybridBinarizer(luminanceSource)); try { // Create a multi-format reader const reader = new ZXing.MultiFormatReader(); reader.setHints(hints); // Try to decode the image using the binary bitmap const decodedResult = reader.decode(binaryBitmap); // Barcode detected successfully status.textContent = 'Barcode recognized successfully!'; // Format the result const resultText = `Code: ${decodedResult.getText()}\nFormat: ${decodedResult.getBarcodeFormat()}`; document.getElementById('result').textContent = resultText; // Draw the barcode location on the canvas drawBarcodeLocation(ctx, decodedResult.getResultPoints()); } catch (error) { // Try alternative method if the first one fails try { // Use the BrowserMultiFormatReader as a fallback const result = await codeReader.decodeFromImageUrl(imageElement.src); // Barcode detected successfully status.textContent = 'Barcode recognized successfully!'; // Format the result const resultText = `Code: ${result.text}\nFormat: ${result.format}`; document.getElementById('result').textContent = resultText; // Draw the barcode location on the canvas if available if (result.resultPoints && result.resultPoints.length > 0) { drawBarcodeLocation(ctx, result.resultPoints); } } catch (secondError) { // Try one more approach with different binarizer try { const globalHistogramBinarizer = new ZXing.GlobalHistogramBinarizer(luminanceSource); const secondBinaryBitmap = new ZXing.BinaryBitmap(globalHistogramBinarizer); const reader = new ZXing.MultiFormatReader(); reader.setHints(hints); const thirdResult = reader.decode(secondBinaryBitmap); status.textContent = 'Barcode recognized successfully!'; const resultText = `Code: ${thirdResult.getText()}\nFormat: ${thirdResult.getBarcodeFormat()}`; document.getElementById('result').textContent = resultText; drawBarcodeLocation(ctx, thirdResult.getResultPoints()); } catch (thirdError) { // No barcode detected after all attempts status.textContent = 'No barcode detected'; result.textContent = 'Could not detect a valid barcode in the image. Please try another image with a clearer barcode.'; console.error('ZXing errors:', error, secondError, thirdError); } } } } catch (error) { // Handle any exceptions during processing status.textContent = 'Error processing image'; result.textContent = error.message || 'An unexpected error occurred'; console.error('Error in processImage:', error); } } // Draw barcode location on canvas function drawBarcodeLocation(ctx, points) { if (!points || points.length === 0) return; ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)'; // Red color for detected barcodes ctx.lineWidth = 5; ctx.beginPath(); // For QR codes and other 2D barcodes (usually 4 points) if (points.length >= 4) { ctx.moveTo(points[0].x, points[0].y); ctx.lineTo(points[1].x, points[1].y); ctx.lineTo(points[2].x, points[2].y); ctx.lineTo(points[3].x, points[3].y); ctx.lineTo(points[0].x, points[0].y); } // For 1D barcodes (usually 2 points) else if (points.length >= 2) { // Calculate the corners of a rectangle for the barcode const p1 = points[0]; const p2 = points[points.length - 1]; // Calculate the width of the barcode (perpendicular to the line) const angle = Math.atan2(p2.y - p1.y, p2.x - p1.x); const perpAngle = angle + Math.PI / 2; const width = 20; // Width of the barcode rectangle // Calculate the four corners of the rectangle const corners = [ { x: p1.x - width * Math.cos(perpAngle), y: p1.y - width * Math.sin(perpAngle) }, { x: p1.x + width * Math.cos(perpAngle), y: p1.y + width * Math.sin(perpAngle) }, { x: p2.x + width * Math.cos(perpAngle), y: p2.y + width * Math.sin(perpAngle) }, { x: p2.x - width * Math.cos(perpAngle), y: p2.y - width * Math.sin(perpAngle) } ]; // Draw the rectangle ctx.moveTo(corners[0].x, corners[0].y); ctx.lineTo(corners[1].x, corners[1].y); ctx.lineTo(corners[2].x, corners[2].y); ctx.lineTo(corners[3].x, corners[3].y); ctx.lineTo(corners[0].x, corners[0].y); } ctx.stroke(); } // Handle page visibility changes document.addEventListener('visibilitychange', () => { if (document.hidden && cameraStream) { // Pause scanning when page is not visible stopBarcodeScanning(); } else if (!document.hidden && cameraStream && !isScanning) { // Resume scanning when page becomes visible again startBarcodeScanning(); } }); // Toggle focus mode for better barcode scanning function toggleFocusMode() { focusModeActive = !focusModeActive; const focusModeButton = document.getElementById('focus-mode'); if (focusModeButton) { focusModeButton.textContent = focusModeActive ? 'Exit Focus Mode' : 'Focus Mode'; focusModeButton.classList.toggle('active', focusModeActive); } document.body.classList.toggle('focus-mode-active', focusModeActive); if (focusModeActive) { status.textContent = 'Focus Mode active. Hold barcode steady in the center of the screen.'; // Pause and restart scanning to apply new settings if (isScanning) { stopBarcodeScanning(); startBarcodeScanning(); } } else { status.textContent = 'Focus Mode disabled. Camera active.'; // Pause and restart scanning to apply new settings if (isScanning) { stopBarcodeScanning(); startBarcodeScanning(); } } } // Capture a still image from the camera feed function captureImage() { if (!videoElement || !cameraStream) { status.textContent = 'Camera not active. Cannot capture image.'; return; } try { // Temporarily pause scanning const wasScanning = isScanning; if (isScanning) { stopBarcodeScanning(); } // Get video dimensions const width = videoElement.videoWidth; const height = videoElement.videoHeight; if (width === 0 || height === 0) { status.textContent = 'Cannot capture image. Video feed not ready.'; if (wasScanning) { startBarcodeScanning(); } return; } // Create a temporary canvas for the capture const captureCanvas = document.createElement('canvas'); captureCanvas.width = width; captureCanvas.height = height; // Draw the current video frame to the canvas const ctx = captureCanvas.getContext('2d'); ctx.drawImage(videoElement, 0, 0, width, height); // Create an image element from the canvas const capturedImage = new Image(); capturedImage.onload = () => { // Display the captured image preview.src = capturedImage.src; preview.style.display = 'block'; // Hide camera feed temporarily cameraContainer.style.display = 'none'; // Process the captured image status.textContent = 'Processing captured image...'; processImage(capturedImage, true); }; // Convert canvas to data URL and set as image source capturedImage.src = captureCanvas.toDataURL('image/png'); // Show a notification status.textContent = 'Image captured from camera. Processing...'; } catch (error) { console.error('Error capturing image:', error); status.textContent = 'Error capturing image: ' + (error.message || 'Unknown error'); // Resume scanning if it was active if (wasScanning) { startBarcodeScanning(); } } } // Process image with ZXing async function processImage(imageElement, fromCamera = false) { status.textContent = 'Processing image...'; result.textContent = ''; try { // Get image dimensions const width = imageElement.naturalWidth; const height = imageElement.naturalHeight; // Set canvas dimensions to match image scanCanvas.width = width; scanCanvas.height = height; // Draw image on canvas const ctx = scanCanvas.getContext('2d'); ctx.drawImage(imageElement, 0, 0, width, height); // Apply image enhancement if enabled if (useImageEnhancement) { enhanceImage(ctx, width, height); if (debugMode) { updateDebugInfo('Image enhancement applied to captured image'); } } // Create a ZXing HTMLCanvasElementLuminanceSource const luminanceSource = new ZXing.HTMLCanvasElementLuminanceSource(scanCanvas); // Try different binarizers for better detection const binarizers = [ new ZXing.HybridBinarizer(luminanceSource), new ZXing.GlobalHistogramBinarizer(luminanceSource) ]; let decodedResult = null; let usedBinarizer = ''; let errorMessages = []; // Try each binarizer for (const binarizer of binarizers) { if (decodedResult) break; // Stop if we already found a result const binaryBitmap = new ZXing.BinaryBitmap(binarizer); usedBinarizer = binarizer instanceof ZXing.HybridBinarizer ? 'HybridBinarizer' : 'GlobalHistogramBinarizer'; if (debugMode) { updateDebugInfo(`Trying ${usedBinarizer} on captured image`); } try { // Create a multi-format reader with hints const reader = new ZXing.MultiFormatReader(); // Set up hints with all supported formats and try harder flag const newHints = new Map(); newHints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, formats); newHints.set(ZXing.DecodeHintType.TRY_HARDER, true); newHints.set(ZXing.DecodeHintType.PURE_BARCODE, false); newHints.set(ZXing.DecodeHintType.CHARACTER_SET, "UTF-8"); reader.setHints(newHints); // Try to decode the image using the binary bitmap const result = reader.decode(binaryBitmap); decodedResult = { text: result.getText(), format: result.getBarcodeFormat(), resultPoints: result.getResultPoints() }; if (debugMode) { updateDebugInfo(`Success with ${usedBinarizer} on captured image`, decodedResult); } } catch (error) { // Store error for debugging errorMessages.push(`${usedBinarizer}: ${error.message || 'Unknown error'}`); if (debugMode) { updateDebugInfo(`Failed with ${usedBinarizer} on captured image`, { error: error.message || 'Unknown error' }); } } } // If we got a result from the direct approach if (decodedResult) { // Barcode detected successfully status.textContent = 'Barcode recognized successfully!'; // Format the result const resultText = `Code: ${decodedResult.text}\nFormat: ${decodedResult.format}`; document.getElementById('result').textContent = resultText; // Draw the barcode location on the canvas if (decodedResult.resultPoints && decodedResult.resultPoints.length > 0) { drawBarcodeLocation(ctx, decodedResult.resultPoints); } // Play success sound playSuccessBeep(); // If this was from a camera capture, show a button to return to camera if (fromCamera) { showReturnToCameraButton(); } return; } // If direct approach failed, try BrowserMultiFormatReader as fallback try { // Use the BrowserMultiFormatReader as a fallback const dataUrl = scanCanvas.toDataURL('image/png'); const result = await codeReader.decodeFromImageUrl(dataUrl); // Barcode detected successfully status.textContent = 'Barcode recognized successfully!'; // Format the result const resultText = `Code: ${result.text}\nFormat: ${result.format}`; document.getElementById('result').textContent = resultText; // Draw the barcode location on the canvas if available if (result.resultPoints && result.resultPoints.length > 0) { drawBarcodeLocation(ctx, result.resultPoints); } // Play success sound playSuccessBeep(); // If this was from a camera capture, show a button to return to camera if (fromCamera) { showReturnToCameraButton(); } } catch (error) { // No barcode detected after all attempts status.textContent = 'No barcode detected'; result.textContent = 'Could not detect a valid barcode in the image. Please try again with a clearer image.'; if (debugMode) { updateDebugInfo('Failed with all methods on captured image', { error: error.message || 'Unknown error', allErrors: errorMessages.join(', ') }); } // If this was from a camera capture, show a button to return to camera if (fromCamera) { showReturnToCameraButton(); } } } catch (error) { // Handle any exceptions during processing status.textContent = 'Error processing image'; result.textContent = error.message || 'An unexpected error occurred'; console.error('Error in processImage:', error); // If this was from a camera capture, show a button to return to camera if (fromCamera) { showReturnToCameraButton(); } } } // Show a button to return to camera view after processing a captured image function showReturnToCameraButton() { // Check if button already exists let returnButton = document.getElementById('return-to-camera'); if (returnButton) { returnButton.style.display = 'inline-block'; return; } // Create return to camera button returnButton = document.createElement('button'); returnButton.id = 'return-to-camera'; returnButton.className = 'return-to-camera-btn'; returnButton.textContent = 'Return to Camera'; returnButton.addEventListener('click', () => { // Hide the preview and show the camera preview.style.display = 'none'; cameraContainer.style.display = 'block'; // Hide the return button returnButton.style.display = 'none'; // Restart scanning startBarcodeScanning(); status.textContent = 'Returned to camera. Point at a barcode to scan.'; }); // Add to the DOM const resultSection = document.getElementById('result').parentNode; resultSection.appendChild(returnButton); } // Initialize the app init(); });