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

image-processing.mddocs/reference/

Image Processing

Image processing capabilities providing the foundational infrastructure for converting raw image data into binary representations suitable for barcode decoding. The system follows a three-stage pipeline: luminance extraction from various image formats, binarization using adaptive thresholding algorithms, and binary bitmap representation for reader consumption.

Package Information

  • Package Name: @zxing/library
  • Package Type: npm
  • Module: core/
  • Language: TypeScript
  • Purpose: Convert images to binary bitmaps for barcode detection

Architecture

The image processing pipeline consists of three main layers:

  1. LuminanceSource: Abstract grayscale data extraction from various image formats (RGB, ARGB, YUV, Canvas)

    • Converts color images to grayscale (luminance values 0-255)
    • Platform-independent interface
    • Supports cropping and rotation (implementation-dependent)
  2. Binarizer: Conversion of grayscale luminance data to binary black/white pixels using thresholding algorithms

    • HybridBinarizer: Local block-based thresholding (recommended default)
    • GlobalHistogramBinarizer: Global histogram-based thresholding (faster, less accurate)
    • Strategy pattern allows algorithm selection
  3. BinaryBitmap: High-level wrapper providing unified interface for barcode readers

    • Lazy evaluation of binary matrix conversion
    • Caches matrix after first access
    • Supports cropping and rotation when underlying source supports them

Core Imports

import {
  // Binary bitmap
  BinaryBitmap,
  
  // Binarizers
  Binarizer,
  HybridBinarizer,
  GlobalHistogramBinarizer,
  
  // Luminance sources
  LuminanceSource,
  RGBLuminanceSource,
  PlanarYUVLuminanceSource,
  InvertedLuminanceSource,
  HTMLCanvasElementLuminanceSource,
  
  // Supporting types
  BitArray,
  BitMatrix
} from '@zxing/library';

Capabilities

BinaryBitmap

Binary representation of images for barcode decoding, serving as the primary input format for all Reader implementations. Provides lazy evaluation of binary matrix conversion and supports cropping and rotation operations when underlying luminance source supports them.

/**
 * Binary representation of image for barcode decoding
 * Core bitmap class used by all ZXing readers
 * Provides unified interface over different binarization strategies
 */
class BinaryBitmap {
  /**
   * Create binary bitmap from binarizer
   * @param binarizer - Binarizer: binarizer instance containing luminance source and conversion logic
   * @throws IllegalArgumentException if binarizer is null
   */
  constructor(binarizer: Binarizer);

  /**
   * Get image width in pixels
   * @returns number: width from underlying luminance source
   */
  getWidth(): number;

  /**
   * Get image height in pixels
   * @returns number: height from underlying luminance source
   */
  getHeight(): number;

  /**
   * Get single row of binary data as bit array
   * Intended for 1D barcode decoding with optional sharpening
   * @param y - number: row index in [0, height)
   * @param row - BitArray: optional preallocated BitArray for reuse (performance optimization)
   * @returns BitArray: binary row where true = black, false = white
   * @throws NotFoundException if row cannot be binarized
   */
  getBlackRow(y: number, row: BitArray): BitArray;

  /**
   * Get entire image as 2D binary matrix
   * Result is cached after first call for performance
   * Intended for 2D barcode decoding without sharpening
   * @returns BitMatrix: binary matrix where true = black, false = white
   * @throws NotFoundException if image cannot be binarized
   */
  getBlackMatrix(): BitMatrix;

  /**
   * Check if cropping is supported by underlying luminance source
   * @returns boolean: true if crop() can be called safely
   */
  isCropSupported(): boolean;

  /**
   * Create cropped version of bitmap
   * Only callable if isCropSupported() returns true
   * @param left - number: left coordinate in [0, width)
   * @param top - number: top coordinate in [0, height)
   * @param width - number: width of crop rectangle
   * @param height - number: height of crop rectangle
   * @returns BinaryBitmap: new bitmap with cropped image data
   * @throws UnsupportedOperationException if cropping not supported
   */
  crop(left: number, top: number, width: number, height: number): BinaryBitmap;

  /**
   * Check if rotation is supported by underlying luminance source
   * @returns boolean: true if rotation methods can be called safely
   */
  isRotateSupported(): boolean;

  /**
   * Create version rotated 90 degrees counterclockwise
   * Only callable if isRotateSupported() returns true
   * @returns BinaryBitmap: new bitmap with rotated image data
   * @throws UnsupportedOperationException if rotation not supported
   */
  rotateCounterClockwise(): BinaryBitmap;

  /**
   * Create version rotated 45 degrees counterclockwise
   * Only callable if isRotateSupported() returns true
   * @returns BinaryBitmap: new bitmap with rotated image data
   * @throws UnsupportedOperationException if rotation not supported
   */
  rotateCounterClockwise45(): BinaryBitmap;

  /**
   * String representation of binary matrix for debugging
   * @returns string: visualization of bitmap
   */
  toString(): string;
}

Usage Examples:

import {
  BinaryBitmap,
  HybridBinarizer,
  RGBLuminanceSource,
  BitMatrix,
  BitArray,
  MultiFormatReader,
  Result
} from '@zxing/library';

// Basic usage with RGB image data
const luminanceSource = new RGBLuminanceSource(
  imageData,  // Uint8ClampedArray: pixel data
  width,      // number: image width
  height      // number: image height
);

const binarizer = new HybridBinarizer(luminanceSource);
const bitmap = new BinaryBitmap(binarizer);

console.log("Bitmap size:", bitmap.getWidth(), "×", bitmap.getHeight());

// Get binary matrix for 2D barcode decoding (QR Code, Data Matrix, Aztec)
const matrix: BitMatrix = bitmap.getBlackMatrix();
console.log("Binary matrix:", matrix.getWidth(), "×", matrix.getHeight());

// Matrix is cached - subsequent calls return same instance
const matrix2: BitMatrix = bitmap.getBlackMatrix();
console.log("Same instance:", matrix === matrix2); // true

// Get single row for 1D barcode decoding (Code 128, EAN-13, etc.)
const middleRow: number = Math.floor(height / 2);
const row: BitArray = bitmap.getBlackRow(middleRow, undefined);
console.log("Row size:", row.getSize());

// Reuse BitArray for performance (in loops)
let reuseRow: BitArray | undefined;
for (let y = 0; y < height; y += 10) {
  reuseRow = bitmap.getBlackRow(y, reuseRow);
  // Process row for 1D barcode scanning...
}

// Crop to region of interest
if (bitmap.isCropSupported()) {
  const cropped: BinaryBitmap = bitmap.crop(100, 100, 200, 200);
  console.log("Cropped:", cropped.getWidth(), "×", cropped.getHeight());
  
  // Use cropped bitmap for focused decoding
  const reader = new MultiFormatReader();
  try {
    const result: Result = reader.decode(cropped);
    console.log("Decoded from crop:", result.getText());
  } catch (error) {
    console.error("Crop decode failed:", error);
  }
}

// Rotate for alternative orientations
if (bitmap.isRotateSupported()) {
  const rotated: BinaryBitmap = bitmap.rotateCounterClockwise();
  console.log("Rotated 90° CCW");
  
  // Try decoding rotated image
  const reader = new MultiFormatReader();
  try {
    const result: Result = reader.decode(rotated);
    console.log("Decoded from rotated:", result.getText());
  } catch (error) {
    console.error("Rotated decode failed:", error);
  }
}

// Debug visualization
console.log("Bitmap visualization:");
console.log(bitmap.toString());

Binarizer (Abstract)

Abstract base class for converting luminance data to binary (1-bit) representation. Allows polymorphic algorithm selection - expensive high-quality techniques for servers, fast methods for mobile devices.

/**
 * Abstract base class for luminance-to-binary conversion
 * Provides interface for different binarization algorithms
 * Subclasses implement specific thresholding strategies
 */
abstract class Binarizer {
  /**
   * Create binarizer for luminance source
   * @param source - LuminanceSource: grayscale pixel data source
   */
  protected constructor(source: LuminanceSource);

  /**
   * Get underlying luminance source
   * @returns LuminanceSource: source providing grayscale data
   */
  getLuminanceSource(): LuminanceSource;

  /**
   * Convert single row to binary data
   * May return cached data - assume expensive operation
   * Intended for 1D decoding with optional sharpening
   * @param y - number: row index in [0, height)
   * @param row - BitArray: optional preallocated BitArray for reuse
   * @returns BitArray: binary row where true = black pixel
   * @throws NotFoundException if row cannot be binarized
   */
  abstract getBlackRow(y: number, row: BitArray): BitArray;

  /**
   * Convert entire image to binary matrix
   * Assume expensive operation - do not call repeatedly
   * Result should be cached by BinaryBitmap
   * Intended for 2D decoding - may differ from getBlackRow() output
   * @returns BitMatrix: binary matrix where true = black pixel
   * @throws NotFoundException if image cannot be binarized (insufficient contrast)
   */
  abstract getBlackMatrix(): BitMatrix;

  /**
   * Create new binarizer instance with pristine state
   * Needed because implementations may be stateful (caching)
   * @param source - LuminanceSource: luminance source for new instance
   * @returns Binarizer: fresh binarizer of same concrete type
   */
  abstract createBinarizer(source: LuminanceSource): Binarizer;

  /**
   * Get image width from underlying source
   * @returns number: width in pixels
   */
  getWidth(): number;

  /**
   * Get image height from underlying source
   * @returns number: height in pixels
   */
  getHeight(): number;
}

Design Notes:

The abstract Binarizer class enables strategy pattern for binarization algorithms. Implementations must handle:

  • State management: Caching results for performance
  • Algorithm-specific logic: Thresholding strategy (global vs local)
  • Factory method: Creating fresh instances with new source
  • Sharpening: Optional for getBlackRow() (1D barcodes benefit from edge enhancement)

HybridBinarizer

Hybrid local/global thresholding binarizer combining histogram approach for 1D readers with local block-based thresholding for 2D readers. Recommended default implementation for most use cases.

/**
 * Hybrid binarization using local thresholding for 2D, global for 1D
 * Best for high frequency barcode images with black data on white backgrounds
 * Handles severe shadows and gradients better than global approach
 * Default binarizer recommended for library users
 */
class HybridBinarizer extends GlobalHistogramBinarizer {
  /**
   * Create hybrid binarizer with given luminance source
   * @param source - LuminanceSource: grayscale pixel data
   */
  constructor(source: LuminanceSource);

  /**
   * Get binary matrix using local block-based thresholding
   * Uses 5×5 grid of 8×8 pixel blocks for local threshold calculation
   * Falls back to global histogram for images smaller than 40×40 pixels
   * Result is cached by BinaryBitmap after first call
   * @returns BitMatrix: locally thresholded binary data
   * @throws NotFoundException if binarization fails (insufficient contrast)
   */
  getBlackMatrix(): BitMatrix;

  /**
   * Create new HybridBinarizer with fresh state
   * @param source - LuminanceSource: luminance source for new instance
   * @returns Binarizer: new HybridBinarizer instance
   */
  createBinarizer(source: LuminanceSource): Binarizer;
}

Algorithm Details:

HybridBinarizer uses sophisticated local thresholding:

  1. Block Division: Image divided into 8×8 pixel blocks
  2. Grid: Blocks arranged in grid (requires minimum 5×5 blocks = 40×40 pixels)
  3. Black Point Calculation: For each block, calculates average luminance as threshold
  4. 5×5 Averaging: Each pixel's threshold is average of 5×5 grid of surrounding blocks
  5. Dynamic Range: Blocks with low contrast (< 24 luminance range) use neighbor thresholds
  6. Threshold Application: Each pixel compared to its interpolated block threshold
  7. Fallback: Images < 40×40 use global histogram method

Performance Characteristics:

  • Optimal for: Images ≥ 40×40 pixels with shadows/gradients
  • Block size: 8×8 pixels (64 pixels per block)
  • Minimum dimension: 40 pixels (5 blocks × 8 pixels/block)
  • Fallback: Global histogram for smaller images
  • Best use cases: 2D barcodes (QR, Data Matrix, Aztec), challenging lighting, shadows

Usage Examples:

import {
  HybridBinarizer,
  RGBLuminanceSource,
  BinaryBitmap,
  QRCodeReader,
  Result,
  BitMatrix
} from '@zxing/library';

// Standard usage - recommended for most cases
const luminanceSource = new RGBLuminanceSource(imageData, width, height);
const binarizer = new HybridBinarizer(luminanceSource);
const bitmap = new BinaryBitmap(binarizer);

// HybridBinarizer handles:
// - Images with gradients and shadows
// - High contrast barcode regions
// - Mixed lighting conditions
// - Both bright and dark areas in same image

console.log("Image size:", bitmap.getWidth(), "×", bitmap.getHeight());

// Decode QR code with challenging lighting
const reader = new QRCodeReader();
try {
  const result: Result = reader.decode(bitmap);
  console.log("Decoded with HybridBinarizer:", result.getText());
} catch (e) {
  console.error("Decoding failed:", e);
}

// Get binary matrix to visualize binarization
const matrix: BitMatrix = bitmap.getBlackMatrix();

// Render binarized image to canvas for debugging
function visualizeBinarization(matrix: BitMatrix, canvas: HTMLCanvasElement): void {
  const ctx = canvas.getContext('2d')!;
  canvas.width = matrix.getWidth();
  canvas.height = matrix.getHeight();
  
  const imageData = ctx.createImageData(canvas.width, canvas.height);
  
  for (let y = 0; y < matrix.getHeight(); y++) {
    for (let x = 0; x < matrix.getWidth(); x++) {
      const offset = (y * matrix.getWidth() + x) * 4;
      const color = matrix.get(x, y) ? 0 : 255;
      
      imageData.data[offset] = color;
      imageData.data[offset + 1] = color;
      imageData.data[offset + 2] = color;
      imageData.data[offset + 3] = 255;
    }
  }
  
  ctx.putImageData(imageData, 0, 0);
}

const debugCanvas = document.getElementById('debug') as HTMLCanvasElement;
visualizeBinarization(matrix, debugCanvas);

// Create new binarizer for different image
const newSource = new RGBLuminanceSource(newImageData, newWidth, newHeight);
const newBinarizer: Binarizer = binarizer.createBinarizer(newSource);
const newBitmap = new BinaryBitmap(newBinarizer);

When to Use HybridBinarizer:

Use for:

  • QR Codes, Data Matrix, Aztec (2D barcodes)
  • Images with uneven lighting or shadows
  • Photos taken with mobile cameras
  • Mixed lighting conditions
  • General purpose applications (recommended default)

Not optimal for:

  • Very small images (< 40×40 pixels) - falls back to global anyway
  • Extremely low-end devices - consider GlobalHistogramBinarizer instead

GlobalHistogramBinarizer

Simple global thresholding binarizer using histogram-based black point estimation. Faster but less accurate than HybridBinarizer; suitable for low-end devices or images with uniform lighting.

/**
 * Global histogram-based binarization
 * Uses single global black point threshold for entire image
 * Fast but cannot handle shadows and gradients well
 * Suitable for low-end mobile devices with limited CPU/memory
 */
class GlobalHistogramBinarizer extends Binarizer {
  /**
   * Create global histogram binarizer
   * @param source - LuminanceSource: grayscale pixel data
   */
  constructor(source: LuminanceSource);

  /**
   * Get binary row using global threshold with sharpening
   * Applies -1 4 -1 box filter for edge enhancement (benefits 1D barcodes)
   * @param y - number: row index in [0, height)
   * @param row - BitArray: optional preallocated BitArray for reuse
   * @returns BitArray: thresholded and sharpened binary data
   * @throws NotFoundException if black point estimation fails
   */
  getBlackRow(y: number, row: BitArray): BitArray;

  /**
   * Get binary matrix using single global threshold
   * Samples 4 horizontal rows to build luminance histogram
   * No sharpening applied (intended for 2D barcodes)
   * @returns BitMatrix: globally thresholded binary data
   * @throws NotFoundException if black point estimation fails
   */
  getBlackMatrix(): BitMatrix;

  /**
   * Create new GlobalHistogramBinarizer with fresh state
   * @param source - LuminanceSource: luminance source for new instance
   * @returns Binarizer: new GlobalHistogramBinarizer instance
   */
  createBinarizer(source: LuminanceSource): Binarizer;
}

Algorithm Details:

GlobalHistogramBinarizer uses a two-peak histogram approach:

  1. Histogram Construction: Luminance values (0-255) grouped into 32 buckets (divide by 8)
  2. Row Sampling: For matrix mode, samples 4 horizontal rows at 1/5, 2/5, 3/5, 4/5 height
  3. Peak Detection: Finds two tallest histogram peaks representing dark and light values
  4. Valley Finding: Locates valley (minimum) between peaks using scoring function
  5. Threshold Selection: Valley position becomes global black point threshold
  6. Application: Compare each pixel to threshold (< threshold = black, >= threshold = white)
  7. Sharpening: For getBlackRow(), applies -1 4 -1 convolution filter for edge enhancement

Performance Characteristics:

  • Speed: Faster than HybridBinarizer (single-pass, simple histogram)
  • Memory: Lower memory footprint (32-bucket histogram)
  • Accuracy: Lower for challenging lighting conditions
  • Histogram buckets: 32 (5-bit luminance quantization: value >> 3)
  • Row sampling: 4 rows for matrix binarization
  • Best use cases: Uniform lighting, 1D barcodes, resource-constrained devices, screen captures

Usage Examples:

import {
  GlobalHistogramBinarizer,
  RGBLuminanceSource,
  BinaryBitmap,
  Code128Reader,
  Result,
  BitArray
} from '@zxing/library';

// Use when image has uniform lighting
const luminanceSource = new RGBLuminanceSource(imageData, width, height);
const binarizer = new GlobalHistogramBinarizer(luminanceSource);
const bitmap = new BinaryBitmap(binarizer);

console.log("Using GlobalHistogramBinarizer for speed");

// Good for:
// - Studio photos with even lighting
// - Low-end devices (faster than HybridBinarizer)
// - Screen-captured barcodes
// - High-contrast images
// - 1D barcodes with simple backgrounds

// Example: Fast 1D barcode scanning
const reader = new Code128Reader();
const scanLineY: number = Math.floor(bitmap.getHeight() / 2);

try {
  const row: BitArray = bitmap.getBlackRow(scanLineY, undefined);
  // Row has sharpening applied for better 1D detection
  
  const result: Result = reader.decode(bitmap);
  console.log("Decoded:", result.getText());
} catch (error) {
  console.error("Failed:", error);
}

// Limitations:
// - Struggles with shadows and gradients
// - Single threshold for entire image may miss details
// - May fail on uneven lighting

// Comparing binarizers
function compareBinarizers(
  imageData: Uint8ClampedArray,
  width: number,
  height: number
): void {
  const source = new RGBLuminanceSource(imageData, width, height);
  
  // Test GlobalHistogramBinarizer
  const t1 = performance.now();
  const globalBinarizer = new GlobalHistogramBinarizer(source);
  const globalBitmap = new BinaryBitmap(globalBinarizer);
  const globalMatrix = globalBitmap.getBlackMatrix();
  const t2 = performance.now();
  
  console.log(`GlobalHistogramBinarizer: ${t2 - t1}ms`);
  
  // Test HybridBinarizer
  const t3 = performance.now();
  const hybridBinarizer = new HybridBinarizer(source);
  const hybridBitmap = new BinaryBitmap(hybridBinarizer);
  const hybridMatrix = hybridBitmap.getBlackMatrix();
  const t4 = performance.now();
  
  console.log(`HybridBinarizer: ${t4 - t3}ms`);
  console.log(`Speed ratio: ${((t4 - t3) / (t2 - t1)).toFixed(2)}x`);
}

LuminanceSource (Abstract)

Abstract source of grayscale luminance (brightness) data. Provides platform-independent interface for accessing pixel data from various image formats. Immutable by design - crop and rotation create copies.

/**
 * Abstract luminance data source providing grayscale pixel values
 * Abstracts different bitmap formats across platforms
 * Immutable interface - operations create copies to avoid state corruption
 */
abstract class LuminanceSource {
  /**
   * Create luminance source
   * @param width - number: image width in pixels
   * @param height - number: image height in pixels
   */
  protected constructor(width: number, height: number);

  /**
   * Get single row of luminance data (grayscale values 0-255)
   * Values range from 0 (black) to 255 (white)
   * Prefer fetching single row over entire matrix for 1D readers
   * @param y - number: row index in [0, height)
   * @param row - Uint8ClampedArray: optional preallocated array for reuse
   * @returns Uint8ClampedArray: grayscale values (0-255)
   * @throws IllegalArgumentException if y is out of bounds
   */
  abstract getRow(y: number, row?: Uint8ClampedArray): Uint8ClampedArray;

  /**
   * Get entire luminance matrix as row-major 2D array
   * Access pattern: luminance = matrix[y * width + x]
   * Array may be larger than width * height - use getWidth()*getHeight() for actual size
   * Do not modify returned array - it may be shared
   * @returns Uint8ClampedArray: all luminance values in row-major order
   */
  abstract getMatrix(): Uint8ClampedArray;

  /**
   * Create inverted version where black becomes white and vice versa
   * Each value transformed: newValue = 255 - oldValue
   * Used for white-on-black barcodes
   * @returns LuminanceSource: new source with inverted luminances
   */
  abstract invert(): LuminanceSource;

  /**
   * Get image width in pixels
   * @returns number: width
   */
  getWidth(): number;

  /**
   * Get image height in pixels
   * @returns number: height
   */
  getHeight(): number;

  /**
   * Check if this implementation supports cropping
   * @returns boolean: true if crop() can be called safely
   */
  isCropSupported(): boolean;

  /**
   * Create cropped version of luminance data
   * May reference original data rather than copying (zero-copy when possible)
   * Only callable if isCropSupported() returns true
   * @param left - number: left coordinate in [0, width)
   * @param top - number: top coordinate in [0, height)
   * @param width - number: width of crop rectangle
   * @param height - number: height of crop rectangle
   * @returns LuminanceSource: cropped luminance source
   * @throws UnsupportedOperationException if cropping not supported
   */
  crop(left: number, top: number, width: number, height: number): LuminanceSource;

  /**
   * Check if this implementation supports rotation
   * @returns boolean: true if rotation methods can be called safely
   */
  isRotateSupported(): boolean;

  /**
   * Create version rotated 90 degrees counterclockwise
   * Only callable if isRotateSupported() returns true
   * @returns LuminanceSource: rotated luminance source
   * @throws UnsupportedOperationException if rotation not supported
   */
  rotateCounterClockwise(): LuminanceSource;

  /**
   * Create version rotated 45 degrees counterclockwise
   * Only callable if isRotateSupported() returns true
   * @returns LuminanceSource: rotated luminance source
   * @throws UnsupportedOperationException if rotation not supported
   */
  rotateCounterClockwise45(): LuminanceSource;

  /**
   * ASCII art representation of luminance data for debugging
   * Characters: '#' (darkest), '+' (dark), '.' (light), ' ' (lightest)
   * @returns string: multi-line string visualization
   */
  toString(): string;
}

Design Principles:

  • Immutability: Operations return new instances to prevent reader conflicts
  • Lazy Evaluation: getRow() preferred for 1D readers to avoid loading entire image
  • Platform Abstraction: Hides platform-specific bitmap formats (Canvas, ImageData, YUV buffers)
  • Optional Features: Crop and rotate support varies by implementation

RGBLuminanceSource

Luminance source for RGB/ARGB pixel data from standard image formats. Converts color pixels to grayscale using green-weighted average. Supports cropping but not rotation.

/**
 * Luminance source from RGB/ARGB pixel arrays
 * Used for decoding standard image file formats
 * Converts color to grayscale using (R + 2G + B) / 4 formula
 * Green channel weighted 2× because human vision is most sensitive to green
 * Supports zero-copy cropping but not rotation
 */
class RGBLuminanceSource extends LuminanceSource {
  /**
   * Create RGB luminance source with optional cropping
   * Accepts either Uint8ClampedArray (pre-computed grayscale) or
   * Int32Array (ARGB pixels to be converted to grayscale)
   * @param luminances - Uint8ClampedArray|Int32Array: grayscale data or ARGB pixels
   *   - Uint8ClampedArray: already grayscale (0-255)
   *   - Int32Array: ARGB format (A<<24 | R<<16 | G<<8 | B)
   * @param width - number: image width in pixels
   * @param height - number: image height in pixels
   * @param dataWidth - number: optional full data width if cropped (default: width)
   * @param dataHeight - number: optional full data height if cropped (default: height)
   * @param left - number: optional left offset of crop region (default: 0)
   * @param top - number: optional top offset of crop region (default: 0)
   * @throws IllegalArgumentException if crop rectangle exceeds image bounds
   */
  constructor(
    luminances: Uint8ClampedArray | Int32Array,
    width: number,
    height: number,
    dataWidth?: number,
    dataHeight?: number,
    left?: number,
    top?: number
  );

  /**
   * Get row of luminance data from RGB source
   * Efficient single-row access for 1D barcode scanning
   * @param y - number: row index in [0, height)
   * @param row - Uint8ClampedArray: optional preallocated array
   * @returns Uint8ClampedArray: grayscale values (0-255)
   * @throws IllegalArgumentException if y is out of bounds
   */
  getRow(y: number, row?: Uint8ClampedArray): Uint8ClampedArray;

  /**
   * Get entire luminance matrix
   * Returns original array if requesting full image (zero-copy optimization)
   * Otherwise copies only cropped region
   * @returns Uint8ClampedArray: row-major grayscale data
   */
  getMatrix(): Uint8ClampedArray;

  /**
   * RGB luminance sources support cropping
   * @returns boolean: always returns true
   */
  isCropSupported(): boolean;

  /**
   * Create cropped version referencing same luminance data
   * Zero-copy operation - references original array
   * @param left - number: crop left coordinate (relative to current view)
   * @param top - number: crop top coordinate (relative to current view)
   * @param width - number: crop width
   * @param height - number: crop height
   * @returns LuminanceSource: new RGBLuminanceSource viewing cropped region
   */
  crop(left: number, top: number, width: number, height: number): LuminanceSource;

  /**
   * Create inverted version for white-on-black barcodes
   * @returns LuminanceSource: InvertedLuminanceSource wrapping this instance
   */
  invert(): LuminanceSource;
  
  /**
   * RGB sources do not support rotation
   * @returns boolean: always returns false
   */
  isRotateSupported(): boolean;
}

Color to Grayscale Conversion:

For Int32Array input (ARGB format):

Pixel format: (A << 24) | (R << 16) | (G << 8) | B

Extraction:
  r = (pixel >> 16) & 0xFF
  g = (pixel >> 7) & 0x1FE   // Extracts 2*G efficiently
  b = pixel & 0xFF

Grayscale formula:
  luminance = (r + g + b) / 4
  luminance = (R + 2G + B) / 4

Green weighted 2× because:
  - Human vision most sensitive to green wavelengths
  - Better preserves perceived brightness
  - Standard practice in image processing

Usage Examples:

import {
  RGBLuminanceSource,
  HybridBinarizer,
  BinaryBitmap,
  MultiFormatReader,
  Result
} from '@zxing/library';

// From pre-computed grayscale data
const grayscaleData = new Uint8ClampedArray(width * height);
// ... fill grayscaleData with luminance values (0-255) ...

const source1 = new RGBLuminanceSource(grayscaleData, width, height);
console.log("Created from grayscale data");

// From ARGB pixel array (automatic conversion)
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = width;
canvas.height = height;

// Draw image to canvas
const img = document.getElementById('barcode-image') as HTMLImageElement;
ctx.drawImage(img, 0, 0);

// Get image data
const imageData = ctx.getImageData(0, 0, width, height);

// Create Int32Array view for ARGB pixels
const argbPixels = new Int32Array(imageData.data.buffer);
const source2 = new RGBLuminanceSource(argbPixels, width, height);
console.log("Created from ARGB data with automatic conversion");

// Crop to region of interest (zero-copy)
const cropX = 50;
const cropY = 50;
const cropWidth = 200;
const cropHeight = 200;

const cropped: LuminanceSource = source2.crop(cropX, cropY, cropWidth, cropHeight);
console.log("Cropped:", cropped.getWidth(), "×", cropped.getHeight());

// Complete decoding pipeline
const binarizer = new HybridBinarizer(source2);
const bitmap = new BinaryBitmap(binarizer);
const reader = new MultiFormatReader();

try {
  const result: Result = reader.decode(bitmap);
  console.log("Decoded:", result.getText());
} catch (error) {
  console.error("Decode failed:", error);
}

// Invert for white-on-black barcodes
const inverted: LuminanceSource = source2.invert();
const invertedBinarizer = new HybridBinarizer(inverted);
const invertedBitmap = new BinaryBitmap(invertedBinarizer);

try {
  const invertedResult: Result = reader.decode(invertedBitmap);
  console.log("Decoded inverted:", invertedResult.getText());
} catch (error) {
  console.error("Inverted decode failed:", error);
}

// Get single row efficiently
const middleRow: number = Math.floor(source2.getHeight() / 2);
const rowData: Uint8ClampedArray = source2.getRow(middleRow);
console.log("Row luminance values:", Array.from(rowData.slice(0, 10)));

// Get entire matrix
const matrix: Uint8ClampedArray = source2.getMatrix();
console.log("Matrix size:", matrix.length, "bytes");

// Access specific pixel luminance
function getPixelLuminance(
  matrix: Uint8ClampedArray,
  x: number,
  y: number,
  width: number
): number {
  return matrix[y * width + x];
}

const pixelValue = getPixelLuminance(matrix, 100, 100, width);
console.log("Luminance at (100,100):", pixelValue);

Performance Characteristics:

  • Conversion: One-time O(n) for Int32Array input, zero-copy for Uint8ClampedArray
  • Cropping: Zero-copy reference to original data
  • getRow(): O(width) copy operation
  • getMatrix(): Zero-copy if full image, O(width × height) if cropped
  • Memory: Stores only grayscale data (1 byte per pixel)

InvertedLuminanceSource

Wrapper luminance source that inverts brightness values for decoding white-on-black barcodes. Delegates all operations to underlying source while transforming luminance values.

/**
 * Wrapper that inverts luminance values: black ↔ white
 * Each pixel transformed: newValue = 255 - oldValue
 * Useful for decoding white-on-black or negative barcodes
 * Delegates all operations to wrapped source with inversion applied
 */
class InvertedLuminanceSource extends LuminanceSource {
  /**
   * Create inverted wrapper around existing luminance source
   * @param delegate - LuminanceSource: underlying source to invert
   */
  constructor(delegate: LuminanceSource);

  /**
   * Get row with inverted luminance values
   * @param y - number: row index in [0, height)
   * @param row - Uint8ClampedArray: optional preallocated array
   * @returns Uint8ClampedArray: inverted grayscale values (255 - original)
   */
  getRow(y: number, row?: Uint8ClampedArray): Uint8ClampedArray;

  /**
   * Get matrix with inverted luminance values
   * @returns Uint8ClampedArray: inverted grayscale values (255 - original for each)
   */
  getMatrix(): Uint8ClampedArray;

  /**
   * Check if underlying source supports cropping
   * @returns boolean: delegate's crop support status
   */
  isCropSupported(): boolean;

  /**
   * Create cropped inverted source
   * @param left - number: crop left coordinate
   * @param top - number: crop top coordinate
   * @param width - number: crop width
   * @param height - number: crop height
   * @returns LuminanceSource: InvertedLuminanceSource wrapping cropped delegate
   */
  crop(left: number, top: number, width: number, height: number): LuminanceSource;

  /**
   * Check if underlying source supports rotation
   * @returns boolean: delegate's rotation support status
   */
  isRotateSupported(): boolean;

  /**
   * Return original delegate since inversion undoes itself
   * Inverting twice returns to original values
   * @returns LuminanceSource: original unwrapped source (not inverted)
   */
  invert(): LuminanceSource;

  /**
   * Create rotated inverted source (90° counterclockwise)
   * @returns LuminanceSource: InvertedLuminanceSource wrapping rotated delegate
   * @throws UnsupportedOperationException if delegate doesn't support rotation
   */
  rotateCounterClockwise(): LuminanceSource;

  /**
   * Create rotated inverted source (45° counterclockwise)
   * @returns LuminanceSource: InvertedLuminanceSource wrapping rotated delegate
   * @throws UnsupportedOperationException if delegate doesn't support rotation
   */
  rotateCounterClockwise45(): LuminanceSource;
}

Inversion Formula:

For each pixel:
  originalValue: 0 (black) to 255 (white)
  invertedValue: 255 - originalValue

Examples:
  0 (black) → 255 (white)
  64 (dark gray) → 191 (light gray)
  128 (mid-gray) → 127 (mid-gray)
  191 (light gray) → 64 (dark gray)
  255 (white) → 0 (black)

Usage Examples:

import {
  RGBLuminanceSource,
  InvertedLuminanceSource,
  HybridBinarizer,
  BinaryBitmap,
  MultiFormatReader,
  Result
} from '@zxing/library';

// Standard black-on-white barcode
const source = new RGBLuminanceSource(imageData, width, height);

// Try normal decoding first
const normalBitmap = new BinaryBitmap(new HybridBinarizer(source));
const reader = new MultiFormatReader();

let result: Result | null = null;

try {
  result = reader.decode(normalBitmap);
  console.log("Decoded normal:", result.getText());
} catch (error) {
  console.debug("Normal decoding failed, trying inverted");
}

// If normal fails, try inverted for white-on-black barcodes
if (!result) {
  const inverted: LuminanceSource = source.invert();
  const invertedBitmap = new BinaryBitmap(new HybridBinarizer(inverted));
  
  try {
    result = reader.decode(invertedBitmap);
    console.log("Decoded inverted:", result.getText());
  } catch (error) {
    console.error("Both normal and inverted failed:", error);
  }
}

// Helper function for robust decoding
function tryBothOrientations(
  imageData: Uint8ClampedArray,
  width: number,
  height: number
): Result | null {
  const source = new RGBLuminanceSource(imageData, width, height);
  const reader = new MultiFormatReader();

  // Try normal
  try {
    const normalBitmap = new BinaryBitmap(new HybridBinarizer(source));
    return reader.decode(normalBitmap);
  } catch (error) {
    console.debug("Normal failed");
  }

  // Try inverted
  try {
    const inverted = source.invert();
    const invertedBitmap = new BinaryBitmap(new HybridBinarizer(inverted));
    return reader.decode(invertedBitmap);
  } catch (error) {
    console.debug("Inverted failed");
  }

  return null;
}

// Inversion is self-reversing
const source2 = new RGBLuminanceSource(imageData, width, height);
const inverted2: LuminanceSource = source2.invert();    // InvertedLuminanceSource
const original: LuminanceSource = inverted2.invert();   // Returns source2 (original)

console.log("Double inversion returns original:", original === source2); // true

// Chaining operations
const cropped: LuminanceSource = source.crop(100, 100, 200, 200);
const croppedInverted: LuminanceSource = cropped.invert();

// Or equivalently (commutative):
const invertedCropped: LuminanceSource = source.invert().crop(100, 100, 200, 200);

Use Cases:

  • Negative barcodes: White bars on black background (e.g., night mode displays)
  • Film negatives: Inverted printed materials
  • Etched materials: Laser etching or engraving
  • Dark mode UIs: Barcodes on dark backgrounds
  • Automatic fallback: Try both normal and inverted during decoding

Performance Characteristics:

  • Overhead: O(n) transformation per getRow()/getMatrix() call
  • Memory: Allocates new array for inverted data on each access
  • Delegation: All other operations (crop, rotate) delegate to wrapped source
  • Optimization: invert().invert() returns original without double processing

Typical Workflows

Standard Image Decoding Pipeline

import {
  RGBLuminanceSource,
  HybridBinarizer,
  BinaryBitmap,
  MultiFormatReader,
  Result
} from '@zxing/library';

function decodeFromImage(imageElement: HTMLImageElement): Result | null {
  // Step 1: Load image and extract pixel data
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d')!;
  
  canvas.width = imageElement.naturalWidth || imageElement.width;
  canvas.height = imageElement.naturalHeight || imageElement.height;
  
  context.drawImage(imageElement, 0, 0);
  const imageData = context.getImageData(0, 0, canvas.width, canvas.height);

  // Step 2: Create luminance source from RGB data
  const luminanceSource = new RGBLuminanceSource(
    new Uint8ClampedArray(imageData.data),
    canvas.width,
    canvas.height
  );

  // Step 3: Apply binarization (HybridBinarizer recommended)
  const binarizer = new HybridBinarizer(luminanceSource);

  // Step 4: Create binary bitmap
  const bitmap = new BinaryBitmap(binarizer);

  // Step 5: Decode barcode
  const reader = new MultiFormatReader();
  
  try {
    const result: Result = reader.decode(bitmap);
    console.log("Decoded text:", result.getText());
    console.log("Format:", result.getBarcodeFormat());
    return result;
  } catch (error) {
    console.error("Decoding failed:", error);
    return null;
  }
}

// Usage
const img = document.getElementById('barcode-img') as HTMLImageElement;
const result = decodeFromImage(img);

if (result) {
  document.getElementById('result')!.textContent = result.getText();
}

Handling Challenging Images

import {
  RGBLuminanceSource,
  HybridBinarizer,
  GlobalHistogramBinarizer,
  BinaryBitmap,
  MultiFormatReader,
  DecodeHintType,
  LuminanceSource,
  Result
} from '@zxing/library';

function robustDecode(
  imageData: Uint8ClampedArray,
  width: number,
  height: number
): Result | null {
  const reader = new MultiFormatReader();
  const hints = new Map<DecodeHintType, any>();
  hints.set(DecodeHintType.TRY_HARDER, true);

  // Strategy 1: HybridBinarizer (handles gradients and shadows)
  try {
    const source1 = new RGBLuminanceSource(imageData, width, height);
    const binarizer1 = new HybridBinarizer(source1);
    const bitmap1 = new BinaryBitmap(binarizer1);
    return reader.decode(bitmap1, hints);
  } catch (e) {
    console.debug("Strategy 1 (HybridBinarizer) failed");
  }

  // Strategy 2: GlobalHistogramBinarizer (different threshold calculation)
  try {
    const source2 = new RGBLuminanceSource(imageData, width, height);
    const binarizer2 = new GlobalHistogramBinarizer(source2);
    const bitmap2 = new BinaryBitmap(binarizer2);
    return reader.decode(bitmap2, hints);
  } catch (e) {
    console.debug("Strategy 2 (GlobalHistogramBinarizer) failed");
  }

  // Strategy 3: Inverted with HybridBinarizer (white-on-black)
  try {
    const source3 = new RGBLuminanceSource(imageData, width, height);
    const inverted = source3.invert();
    const binarizer3 = new HybridBinarizer(inverted);
    const bitmap3 = new BinaryBitmap(binarizer3);
    return reader.decode(bitmap3, hints);
  } catch (e) {
    console.debug("Strategy 3 (Inverted) failed");
  }

  // Strategy 4: Inverted with GlobalHistogramBinarizer
  try {
    const source4 = new RGBLuminanceSource(imageData, width, height);
    const inverted2 = source4.invert();
    const binarizer4 = new GlobalHistogramBinarizer(inverted2);
    const bitmap4 = new BinaryBitmap(binarizer4);
    return reader.decode(bitmap4, hints);
  } catch (e) {
    console.debug("Strategy 4 (Inverted Global) failed");
  }

  console.error("All decoding strategies failed");
  return null;
}

// Usage
const result = robustDecode(imageData, width, height);
if (result) {
  console.log("Success:", result.getText());
} else {
  console.error("Could not decode barcode with any strategy");
}

Choosing a Binarizer

ScenarioRecommended BinarizerReason
General use (default)HybridBinarizerBest balance of accuracy and performance
Images with shadows/gradientsHybridBinarizerLocal thresholding handles uneven lighting
High-contrast uniform imagesGlobalHistogramBinarizerFaster, adequate for simple cases
Low-end mobile devicesGlobalHistogramBinarizerLower CPU and memory requirements
Small images (< 40×40)EitherHybridBinarizer falls back to global anyway
Real-time camera scanningHybridBinarizerBetter accuracy for varying conditions
Batch processingHybridBinarizerAccuracy more important than speed
1D barcodes on clean backgroundGlobalHistogramBinarizerFaster, sharpening helps edge detection
2D barcodes (QR, Data Matrix)HybridBinarizerLocal thresholding crucial for pattern detection
Screen capturesGlobalHistogramBinarizerUsually high contrast, global threshold sufficient
Printed materialsHybridBinarizerMay have shadows or paper texture

Error Handling

Image processing operations may throw these exceptions:

/**
 * Exception types for image processing
 */
interface ImageProcessingExceptions {
  /**
   * IllegalArgumentException
   * Thrown for invalid parameters (negative dimensions, out-of-bounds crop, etc.)
   */
  IllegalArgumentException: typeof IllegalArgumentException;

  /**
   * NotFoundException
   * Thrown when binarization fails (insufficient contrast, cannot determine black point)
   */
  NotFoundException: typeof NotFoundException;

  /**
   * UnsupportedOperationException
   * Thrown when operation not supported by implementation (e.g., rotation on RGBLuminanceSource)
   */
  UnsupportedOperationException: typeof UnsupportedOperationException;
}

Usage Examples:

import {
  BinaryBitmap,
  HybridBinarizer,
  RGBLuminanceSource,
  NotFoundException,
  IllegalArgumentException,
  UnsupportedOperationException,
  BitMatrix
} from '@zxing/library';

// Handle binarization failure
try {
  const source = new RGBLuminanceSource(lowContrastImageData, width, height);
  const binarizer = new HybridBinarizer(source);
  const bitmap = new BinaryBitmap(binarizer);
  const matrix: BitMatrix = bitmap.getBlackMatrix();
} catch (e) {
  if (e instanceof NotFoundException) {
    console.error("Cannot binarize image - insufficient contrast");
    console.error("Try: adjusting image contrast, different binarizer, or image preprocessing");
  } else {
    throw e;
  }
}

// Handle invalid parameters
try {
  const source = new RGBLuminanceSource(imageData, -10, 100); // Negative width
} catch (e) {
  if (e instanceof IllegalArgumentException) {
    console.error("Invalid parameters:", e.message);
  }
}

// Handle out-of-bounds crop
try {
  const source = new RGBLuminanceSource(imageData, 100, 100);
  const cropped = source.crop(90, 90, 20, 20); // Exceeds bounds (110, 110)
} catch (e) {
  if (e instanceof IllegalArgumentException) {
    console.error("Crop rectangle exceeds image bounds:", e.message);
  }
}

// Handle unsupported operations
try {
  const source = new RGBLuminanceSource(imageData, 100, 100);
  const rotated = source.rotateCounterClockwise(); // RGBLuminanceSource doesn't support rotation
} catch (e) {
  if (e instanceof UnsupportedOperationException) {
    console.error("Rotation not supported by RGBLuminanceSource");
    console.log("Rotation support:", source.isRotateSupported()); // false
  }
}

// Comprehensive error handling
function safeBinarize(
  imageData: Uint8ClampedArray,
  width: number,
  height: number
): BinaryBitmap | null {
  try {
    if (width <= 0 || height <= 0) {
      throw new IllegalArgumentException("Invalid dimensions");
    }
    
    if (imageData.length < width * height) {
      throw new IllegalArgumentException("Insufficient image data");
    }
    
    const source = new RGBLuminanceSource(imageData, width, height);
    const binarizer = new HybridBinarizer(source);
    const bitmap = new BinaryBitmap(binarizer);
    
    // Force binarization to check for contrast issues
    bitmap.getBlackMatrix();
    
    return bitmap;
  } catch (error) {
    if (error instanceof NotFoundException) {
      console.error("Binarization failed - insufficient contrast");
    } else if (error instanceof IllegalArgumentException) {
      console.error("Invalid parameters:", error.message);
    } else {
      console.error("Unexpected error:", error);
    }
    return null;
  }
}

Related Documentation

  • Core API - Reader interfaces, Result types
  • Common Utilities - BitArray, BitMatrix
  • QR Code - QR Code specific usage
  • Data Matrix - Data Matrix specific usage
  • 1D Barcodes - 1D barcode usage
  • Types and Enums - Exception types

Best Practices

Performance

  1. Reuse BitArray: When calling getBlackRow() in loops, reuse BitArray instance
  2. Cache Matrix: BinaryBitmap caches matrix - don't create multiple BinaryBitmap instances
  3. Choose Binarizer: Use GlobalHistogramBinarizer for speed, HybridBinarizer for accuracy
  4. Crop Wisely: Crop to barcode region before binarization when possible

Quality

  1. Image Size: Larger images (>= 400×400) generally decode better
  2. Contrast: Ensure sufficient contrast between barcode and background
  3. Lighting: Avoid extreme shadows or bright spots
  4. Focus: Ensure barcode is in focus
  5. Resolution: Minimum 3 pixels per module for reliable detection

Troubleshooting

NotFoundException during binarization:

  • Increase image contrast
  • Try different binarizer (Hybrid vs Global)
  • Check lighting conditions
  • Try inverted image

Poor decode accuracy:

  • Use HybridBinarizer instead of GlobalHistogramBinarizer
  • Increase image resolution
  • Improve lighting conditions
  • Ensure barcode is flat and in focus

Install with Tessl CLI

npx tessl i tessl/npm-zxing--library

docs

index.md

tile.json