barcode-scanner / app.js
kedar-bhumkar's picture
Upload 4 files
474a754 verified
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 = '<strong>Security Warning:</strong> 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 = `<strong>${timestamp}</strong>: ${action}`;
if (Object.keys(data).length > 0) {
content += '<ul>';
for (const [key, value] of Object.entries(data)) {
content += `<li><strong>${key}:</strong> ${value}</li>`;
}
content += '</ul>';
}
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();
});