TypeScript port of ZXing multi-format 1D/2D barcode image processing library
Complete guide for integrating @zxing/library with web browsers for camera scanning and barcode generation.
import { BrowserMultiFormatReader, DecodeHintType, BarcodeFormat } from '@zxing/library';
class BarcodeScanner {
private codeReader: BrowserMultiFormatReader;
private scanning = false;
constructor() {
const hints = new Map();
hints.set(DecodeHintType.POSSIBLE_FORMATS, [
BarcodeFormat.QR_CODE,
BarcodeFormat.EAN_13,
BarcodeFormat.CODE_128
]);
this.codeReader = new BrowserMultiFormatReader(hints, 500);
}
async start(videoElement: HTMLVideoElement): Promise<void> {
if (this.scanning) return;
this.scanning = true;
try {
await this.codeReader.decodeFromVideoDevice(
null, // Default camera
videoElement,
(result, error) => {
if (!this.scanning) return;
if (result) {
console.log('Scanned:', result.getText());
this.onScan(result.getText(), result.getBarcodeFormat());
}
}
);
} catch (error) {
this.scanning = false;
throw error;
}
}
stop(): void {
this.scanning = false;
this.codeReader.stopContinuousDecode();
this.codeReader.reset();
}
private onScan(text: string, format: BarcodeFormat): void {
// Override this method
}
}
// Usage
const scanner = new BarcodeScanner();
const video = document.getElementById('video') as HTMLVideoElement;
document.getElementById('start')!.onclick = () => scanner.start(video);
document.getElementById('stop')!.onclick = () => scanner.stop();async function requestCamera(): Promise<boolean> {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
stream.getTracks().forEach(track => track.stop()); // Release immediately
return true;
} catch (error: any) {
if (error.name === 'NotAllowedError') {
alert('Camera permission denied');
} else if (error.name === 'NotFoundError') {
alert('No camera found');
} else if (error.name === 'NotReadableError') {
alert('Camera in use by another app');
} else {
alert('Camera error: ' + error.message);
}
return false;
}
}
// Check before starting scanner
if (await requestCamera()) {
startScanner();
}import { BrowserMultiFormatReader } from '@zxing/library';
const codeReader = new BrowserMultiFormatReader();
// List cameras
const cameras = await codeReader.listVideoInputDevices();
// Populate dropdown
const select = document.getElementById('camera-select') as HTMLSelectElement;
cameras.forEach((device, index) => {
const option = document.createElement('option');
option.value = device.deviceId;
option.text = device.label || `Camera ${index + 1}`;
select.appendChild(option);
});
// Use selected camera
const selectedId = select.value;
const result = await codeReader.decodeOnceFromVideoDevice(selectedId, 'video');import {
QRCodeWriter,
BarcodeFormat,
EncodeHintType,
QRCodeDecoderErrorCorrectionLevel
} from '@zxing/library';
function generateQRCode(text: string, size: number = 300): HTMLCanvasElement {
const writer = new QRCodeWriter();
const hints = new Map();
hints.set(EncodeHintType.ERROR_CORRECTION, QRCodeDecoderErrorCorrectionLevel.H);
hints.set(EncodeHintType.CHARACTER_SET, 'UTF-8');
hints.set(EncodeHintType.MARGIN, 2);
const bitMatrix = writer.encode(text, BarcodeFormat.QR_CODE, size, size, hints);
// Render to canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
const moduleSize = Math.floor(size / bitMatrix.getWidth());
canvas.width = bitMatrix.getWidth() * moduleSize;
canvas.height = bitMatrix.getHeight() * moduleSize;
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#000000';
for (let y = 0; y < bitMatrix.getHeight(); y++) {
for (let x = 0; x < bitMatrix.getWidth(); x++) {
if (bitMatrix.get(x, y)) {
ctx.fillRect(x * moduleSize, y * moduleSize, moduleSize, moduleSize);
}
}
}
return canvas;
}
// Usage
const qrCanvas = generateQRCode('https://example.com');
document.getElementById('container')!.appendChild(qrCanvas);function downloadQRCode(text: string, filename: string = 'qrcode.png'): void {
const canvas = generateQRCode(text);
canvas.toBlob((blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
});
}
// Usage
document.getElementById('download')!.onclick = () => {
downloadQRCode('https://example.com', 'my-qrcode.png');
};import {
MultiFormatReader,
BinaryBitmap,
HybridBinarizer,
RGBLuminanceSource
} from '@zxing/library';
async function decodeFromFile(file: File): Promise<string> {
// Load image from file
const img = await createImageBitmap(file);
// Extract pixel data
const canvas = new OffscreenCanvas(img.width, img.height);
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
// Decode
const luminanceSource = new RGBLuminanceSource(
new Uint8ClampedArray(imageData.data),
img.width,
img.height
);
const bitmap = new BinaryBitmap(new HybridBinarizer(luminanceSource));
const reader = new MultiFormatReader();
const result = reader.decode(bitmap);
return result.getText();
}
// Usage with file input
const input = document.getElementById('file-input') as HTMLInputElement;
input.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
try {
const text = await decodeFromFile(file);
console.log('Decoded:', text);
document.getElementById('result')!.textContent = text;
} catch (error) {
console.error('Failed to decode:', error);
alert('No barcode found in image');
}
});import {
BrowserMultiFormatReader,
Result,
Exception,
NotFoundException,
BarcodeFormat
} from '@zxing/library';
class ProductionScanner {
private reader: BrowserMultiFormatReader;
private scanning = false;
private lastScanned: string | null = null;
constructor(formats?: BarcodeFormat[]) {
const hints = new Map();
if (formats) {
hints.set(DecodeHintType.POSSIBLE_FORMATS, formats);
}
this.reader = new BrowserMultiFormatReader(hints, 300);
}
async initialize(): Promise<MediaDeviceInfo[]> {
if (!BrowserCodeReader.isMediaDevicesSupported()) {
throw new Error('MediaDevices not supported');
}
return this.reader.listVideoInputDevices();
}
async startScanning(
deviceId: string | null,
videoElement: HTMLVideoElement,
onScan: (text: string, format: BarcodeFormat) => void,
onError?: (error: Error) => void
): Promise<void> {
if (this.scanning) {
throw new Error('Already scanning');
}
this.scanning = true;
this.lastScanned = null;
const callback = (result: Result | null, error?: Exception) => {
if (!this.scanning) return;
if (result) {
const text = result.getText();
// Avoid duplicate scans
if (text !== this.lastScanned) {
this.lastScanned = text;
onScan(text, result.getBarcodeFormat());
}
}
if (error && !(error instanceof NotFoundException)) {
if (onError) {
onError(new Error(error.message));
}
}
};
try {
await this.reader.decodeFromVideoDevice(deviceId, videoElement, callback);
} catch (err) {
this.scanning = false;
throw err;
}
}
stopScanning(): void {
this.scanning = false;
this.lastScanned = null;
this.reader.stopContinuousDecode();
this.reader.reset();
}
async scanImage(url: string): Promise<{ text: string; format: BarcodeFormat } | null> {
try {
const result = await this.reader.decodeFromImageUrl(url);
return {
text: result.getText(),
format: result.getBarcodeFormat()
};
} catch (error) {
return null;
}
}
dispose(): void {
this.stopScanning();
}
}
// Usage
const scanner = new ProductionScanner([BarcodeFormat.QR_CODE, BarcodeFormat.EAN_13]);
const cameras = await scanner.initialize();
console.log(`Found ${cameras.length} cameras`);
const video = document.getElementById('video') as HTMLVideoElement;
await scanner.startScanning(
null, // Default camera
video,
(text, format) => {
console.log(`Scanned ${format}: ${text}`);
document.getElementById('result')!.textContent = text;
},
(error) => {
console.error('Scanner error:', error);
}
);
// Cleanup on page unload
window.addEventListener('beforeunload', () => scanner.dispose());Always release camera resources:
const codeReader = new BrowserMultiFormatReader();
try {
const result = await codeReader.decodeOnceFromVideoDevice(undefined, 'video');
} finally {
codeReader.reset(); // Always clean up
}Modern browsers require HTTPS for camera access:
if (!window.isSecureContext && location.hostname !== 'localhost') {
alert('Camera requires HTTPS');
return;
}// Faster scanning (more CPU intensive)
const fastReader = new BrowserMultiFormatReader(hints, 100); // 100ms = 10 fps
// Slower scanning (less CPU intensive)
const slowReader = new BrowserMultiFormatReader(hints, 1000); // 1000ms = 1 fps
// Balanced (recommended)
const balancedReader = new BrowserMultiFormatReader(hints, 500); // 500ms = 2 fpstimeBetweenScansMillisPOSSIBLE_FORMATS hintreset() when doneInstall with Tessl CLI
npx tessl i tessl/npm-zxing--library@0.21.14