CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-zxing--library

TypeScript port of ZXing multi-format 1D/2D barcode image processing library

Overview
Eval results
Files

browser-integration.mddocs/guides/

Browser Integration Guide

Complete guide for integrating @zxing/library with web browsers for camera scanning and barcode generation.

Camera Scanning

Basic Camera Setup

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();

Camera Permission Handling

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();
}

Camera Selection

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');

QR Code Generation

Generate QR Code in Browser

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);

Download QR Code as Image

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');
};

File Upload Decoding

Decode from File Input

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');
  }
});

Production Patterns

Complete Scanner Component

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());

Best Practices

Resource Management

Always release camera resources:

const codeReader = new BrowserMultiFormatReader();

try {
  const result = await codeReader.decodeOnceFromVideoDevice(undefined, 'video');
} finally {
  codeReader.reset(); // Always clean up
}

HTTPS Requirement

Modern browsers require HTTPS for camera access:

if (!window.isSecureContext && location.hostname !== 'localhost') {
  alert('Camera requires HTTPS');
  return;
}

Performance Tuning

// 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 fps

Common Issues

Black Screen

  • Ensure video element has explicit dimensions in CSS
  • Check camera permissions
  • Verify HTTPS or localhost

Performance Issues

  • Increase timeBetweenScansMillis
  • Limit POSSIBLE_FORMATS hint
  • Use GlobalHistogramBinarizer instead of HybridBinarizer
  • Reduce video resolution

Memory Leaks

  • Always call reset() when done
  • Use single scanner instance, avoid creating multiple
  • Clean up on page navigation

Related

  • Quick Start Guide
  • Node.js Integration
  • Real-World Scenarios
  • Browser API Reference

Install with Tessl CLI

npx tessl i tessl/npm-zxing--library

docs

index.md

tile.json