TypeScript port of ZXing multi-format 1D/2D barcode image processing library
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.
The image processing pipeline consists of three main layers:
LuminanceSource: Abstract grayscale data extraction from various image formats (RGB, ARGB, YUV, Canvas)
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)BinaryBitmap: High-level wrapper providing unified interface for barcode readers
import {
// Binary bitmap
BinaryBitmap,
// Binarizers
Binarizer,
HybridBinarizer,
GlobalHistogramBinarizer,
// Luminance sources
LuminanceSource,
RGBLuminanceSource,
PlanarYUVLuminanceSource,
InvertedLuminanceSource,
HTMLCanvasElementLuminanceSource,
// Supporting types
BitArray,
BitMatrix
} from '@zxing/library';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());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:
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:
Performance Characteristics:
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:
✗ Not optimal for:
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:
Performance Characteristics:
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`);
}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:
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 processingUsage 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:
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:
Performance Characteristics:
invert().invert() returns original without double processingimport {
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();
}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");
}| Scenario | Recommended Binarizer | Reason |
|---|---|---|
| General use (default) | HybridBinarizer | Best balance of accuracy and performance |
| Images with shadows/gradients | HybridBinarizer | Local thresholding handles uneven lighting |
| High-contrast uniform images | GlobalHistogramBinarizer | Faster, adequate for simple cases |
| Low-end mobile devices | GlobalHistogramBinarizer | Lower CPU and memory requirements |
| Small images (< 40×40) | Either | HybridBinarizer falls back to global anyway |
| Real-time camera scanning | HybridBinarizer | Better accuracy for varying conditions |
| Batch processing | HybridBinarizer | Accuracy more important than speed |
| 1D barcodes on clean background | GlobalHistogramBinarizer | Faster, sharpening helps edge detection |
| 2D barcodes (QR, Data Matrix) | HybridBinarizer | Local thresholding crucial for pattern detection |
| Screen captures | GlobalHistogramBinarizer | Usually high contrast, global threshold sufficient |
| Printed materials | HybridBinarizer | May have shadows or paper texture |
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;
}
}getBlackRow() in loops, reuse BitArray instanceNotFoundException during binarization:
Poor decode accuracy:
Install with Tessl CLI
npx tessl i tessl/npm-zxing--library