Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
648 changes: 306 additions & 342 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"ndarray-linear-interpolate": "^1.0.0",
"ndarray-pixels": "^5.0.1",
"nyc": "^17.1.0",
"sharp": "^0.34.5",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is sharp necessary? doesn't sharp require native code? or is there a browser version of it now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed sharp from package.json in commit d88f28a. It was installed during initial development for test image creation but is not used by the library code.

"sinon": "^21.0.0",
"sinon-chai": "^3.7.0",
"source-map-loader": "^1.1.1",
Expand Down
6 changes: 6 additions & 0 deletions src/preprocessing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type { PreprocessorFunction } from './preprocessor';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the purpose of this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment explaining its purpose in d88f28a. It's a standard barrel file that re-exports the public API of the preprocessing module, allowing users to import from './preprocessing' rather than './preprocessing/preprocessor'.

export {
addBorder,
applyPreprocessors,
Preprocessors,
} from './preprocessor';
131 changes: 131 additions & 0 deletions src/preprocessing/preprocessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import ImageWrapper from '../common/image_wrapper';

/**
* A preprocessor function that transforms image data.
* Preprocessors are applied to the image data after frame grabbing
* but before barcode localization and decoding.
*
* IMPORTANT: Preprocessors should maintain the same image dimensions.
* The returned ImageWrapper should have the same size as the input.
*
* @param imageWrapper The image wrapper to process
* @returns The processed image wrapper (same size as input)
*/
export type PreprocessorFunction = (imageWrapper: ImageWrapper) => ImageWrapper;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be generally faster if we expected the user to operate on the existing imagewrapper, or would that be more likely that we'd just have the user making copies of the image in memory anyway?

Does returning it automatically imply a copy? I think we need to do whatever encourages the user to make this operation as fast as possible.

Also, since this will be in the public interface, and people will be writing external libraries of these functions potentially, I'd like it to have a name that more accurately describes what this is for, like QuaggaImagePreprocessor ... something like that... ideas?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to QuaggaImagePreprocessor and updated the documentation to clarify that preprocessors should modify the imageWrapper in place and return the same instance. This avoids unnecessary memory allocations and copies. The return value enables chaining but the actual implementation modifies in place. Updated in d88f28a.


/**
* Built-in preprocessor: Adds a white border around the image.
* This is useful for barcodes that lack sufficient quiet zone (whitespace)
* around them. When a barcode is generated or cropped without proper margins,
* the decoder may fail to detect it. Adding a border simulates the whitespace
* that would naturally exist when displaying the barcode on paper or screen.
*
* The image is shrunk slightly and centered, with white border pixels added
* around it. The output size remains the same as the input size.
*
* @param borderSize Number of pixels of white border to add on each side
* @returns A preprocessor function that adds the border
*
* @example
* // Add 10 pixels of white border around all images
* config.preprocessing = [Quagga.Preprocessors.addBorder(10)];
*/
export function addBorder(borderSize: number): PreprocessorFunction {
return (imageWrapper: ImageWrapper): ImageWrapper => {
if (borderSize <= 0) {
return imageWrapper;
}

const width = imageWrapper.size.x;
const height = imageWrapper.size.y;

// Calculate the inner image area (shrunk to make room for border)
const innerWidth = width - (borderSize * 2);
const innerHeight = height - (borderSize * 2);

// If border is too large for the image, just fill with white
if (innerWidth <= 0 || innerHeight <= 0) {
for (let i = 0; i < imageWrapper.data.length; i++) {
imageWrapper.data[i] = 255;
}
return imageWrapper;
}

// Calculate scale factors for shrinking
const scaleX = innerWidth / width;
const scaleY = innerHeight / height;

// Create a temporary copy of the original data
// Note: We need to handle both TypedArray and Array<number> types
const originalData = imageWrapper.data instanceof Uint8Array
? new Uint8Array(imageWrapper.data)
: new Uint8Array(imageWrapper.data);

// Fill the entire image with white first
if (imageWrapper.data instanceof Uint8Array) {
imageWrapper.data.fill(255);
} else {
for (let i = 0; i < imageWrapper.data.length; i++) {
imageWrapper.data[i] = 255;
}
}

// Copy the shrunk image into the center using bilinear interpolation
for (let y = 0; y < innerHeight; y++) {
for (let x = 0; x < innerWidth; x++) {
// Map destination coordinates to source coordinates
const srcX = x / scaleX;
const srcY = y / scaleY;

// Bilinear interpolation
const x0 = Math.floor(srcX);
const y0 = Math.floor(srcY);
const x1 = Math.min(x0 + 1, width - 1);
const y1 = Math.min(y0 + 1, height - 1);

const fx = srcX - x0;
const fy = srcY - y0;

const v00 = originalData[y0 * width + x0];
const v10 = originalData[y0 * width + x1];
const v01 = originalData[y1 * width + x0];
const v11 = originalData[y1 * width + x1];

const v0 = v00 * (1 - fx) + v10 * fx;
const v1 = v01 * (1 - fx) + v11 * fx;
const value = Math.round(v0 * (1 - fy) + v1 * fy);

// Write to destination with border offset
const destIdx = (y + borderSize) * width + (x + borderSize);
imageWrapper.data[destIdx] = value;
}
}

return imageWrapper;
};
}

/**
* Applies a chain of preprocessor functions to an image wrapper.
* @param imageWrapper The image wrapper to process
* @param preprocessors Array of preprocessor functions to apply in order
* @returns The processed image wrapper (same instance, potentially modified)
*/
export function applyPreprocessors(
imageWrapper: ImageWrapper,
preprocessors: PreprocessorFunction[],
): ImageWrapper {
let result = imageWrapper;
for (const preprocessor of preprocessors) {
result = preprocessor(result);
}
return result;
}

/**
* Collection of built-in preprocessor factories.
* Users can use these or provide their own PreprocessorFunction implementations.
*/
export const Preprocessors = {
addBorder,
};
140 changes: 140 additions & 0 deletions src/preprocessing/test/preprocessor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { expect } from 'chai';
import ImageWrapper from '../../common/image_wrapper';
import { addBorder, applyPreprocessors, Preprocessors } from '../preprocessor';

describe('Preprocessors', () => {
describe('addBorder', () => {
it('should return the same image when borderSize is 0', () => {
const size = { x: 10, y: 10, type: 'XYSize' as const };
const data = new Uint8Array(100);
data.fill(128);
const wrapper = new ImageWrapper(size, data);

const preprocessor = addBorder(0);
const result = preprocessor(wrapper);

expect(result).to.equal(wrapper);
expect(result.data).to.deep.equal(data);
});

it('should return the same image when borderSize is negative', () => {
const size = { x: 10, y: 10, type: 'XYSize' as const };
const data = new Uint8Array(100);
data.fill(128);
const wrapper = new ImageWrapper(size, data);

const preprocessor = addBorder(-5);
const result = preprocessor(wrapper);

expect(result).to.equal(wrapper);
});

it('should add white border and shrink image', () => {
const size = { x: 10, y: 10, type: 'XYSize' as const };
const data = new Uint8Array(100);
data.fill(0); // Black image
const wrapper = new ImageWrapper(size, data);

const borderSize = 2;
const preprocessor = addBorder(borderSize);
const result = preprocessor(wrapper);

// Size should remain the same
expect(result.size.x).to.equal(10);
expect(result.size.y).to.equal(10);

// Border pixels should be white (255)
// Top border
for (let y = 0; y < borderSize; y++) {
for (let x = 0; x < size.x; x++) {
expect(result.data[y * size.x + x]).to.equal(255, `Top border at (${x}, ${y})`);
}
}
// Bottom border
for (let y = size.y - borderSize; y < size.y; y++) {
for (let x = 0; x < size.x; x++) {
expect(result.data[y * size.x + x]).to.equal(255, `Bottom border at (${x}, ${y})`);
}
}
// Left border
for (let y = 0; y < size.y; y++) {
for (let x = 0; x < borderSize; x++) {
expect(result.data[y * size.x + x]).to.equal(255, `Left border at (${x}, ${y})`);
}
}
// Right border
for (let y = 0; y < size.y; y++) {
for (let x = size.x - borderSize; x < size.x; x++) {
expect(result.data[y * size.x + x]).to.equal(255, `Right border at (${x}, ${y})`);
}
}

// Center should contain shrunk black image (approximately 0)
const centerX = Math.floor(size.x / 2);
const centerY = Math.floor(size.y / 2);
expect(result.data[centerY * size.x + centerX]).to.be.lessThan(50, 'Center should be dark');
});

it('should fill with white when border is too large', () => {
const size = { x: 10, y: 10, type: 'XYSize' as const };
const data = new Uint8Array(100);
data.fill(0);
const wrapper = new ImageWrapper(size, data);

// Border of 5 on each side = 10 total, which equals the image size
const preprocessor = addBorder(5);
const result = preprocessor(wrapper);

// All pixels should be white
for (let i = 0; i < result.data.length; i++) {
expect(result.data[i]).to.equal(255);
}
});
});

describe('applyPreprocessors', () => {
it('should apply preprocessors in order', () => {
const size = { x: 10, y: 10, type: 'XYSize' as const };
const data = new Uint8Array(100);
data.fill(100);
const wrapper = new ImageWrapper(size, data);

const calls: string[] = [];

const preprocessor1 = (img: ImageWrapper) => {
calls.push('first');
return img;
};

const preprocessor2 = (img: ImageWrapper) => {
calls.push('second');
return img;
};

applyPreprocessors(wrapper, [preprocessor1, preprocessor2]);

expect(calls).to.deep.equal(['first', 'second']);
});

it('should return original wrapper when no preprocessors', () => {
const size = { x: 10, y: 10, type: 'XYSize' as const };
const data = new Uint8Array(100);
const wrapper = new ImageWrapper(size, data);

const result = applyPreprocessors(wrapper, []);

expect(result).to.equal(wrapper);
});
});

describe('Preprocessors object', () => {
it('should export addBorder function', () => {
expect(Preprocessors.addBorder).to.be.a('function');
});

it('should create valid preprocessor from addBorder', () => {
const preprocessor = Preprocessors.addBorder(5);
expect(preprocessor).to.be.a('function');
});
});
});
3 changes: 3 additions & 0 deletions src/quagga.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CameraAccess from './input/camera_access';
import ImageDebug from './common/image_debug';
import ResultCollector from './analytics/result_collector';
import Config from './config/config';
import { Preprocessors } from './preprocessing';

import Quagga from './quagga/quagga';

Expand Down Expand Up @@ -174,6 +175,7 @@ const QuaggaJSStaticInterface = {
ImageDebug,
ImageWrapper,
ResultCollector,
Preprocessors,
};

export default QuaggaJSStaticInterface;
Expand All @@ -185,4 +187,5 @@ export {
ImageDebug,
ImageWrapper,
ResultCollector,
Preprocessors,
};
9 changes: 9 additions & 0 deletions src/quagga/quagga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CameraAccess from '../input/camera_access';
import FrameGrabber from '../input/frame_grabber.js';
import InputStream from '../input/input_stream/input_stream';
import BarcodeLocator from '../locator/barcode_locator';
import { applyPreprocessors, PreprocessorFunction } from '../preprocessing';
import { QuaggaContext } from '../QuaggaContext';
import { BarcodeInfo } from '../reader/barcode_reader';
import _getViewPort from './getViewPort';
Expand Down Expand Up @@ -245,6 +246,10 @@ export default class Quagga {
if (!workersUpdated) {
this.context.framegrabber.attachData(this.context.inputImageWrapper?.data);
if (this.context.framegrabber.grab()) {
// Apply preprocessing if configured
if (this.context.config?.preprocessing && this.context.config.preprocessing.length > 0 && this.context.inputImageWrapper) {
applyPreprocessors(this.context.inputImageWrapper, this.context.config.preprocessing as PreprocessorFunction[]);
}
if (!workersUpdated) {
this.locateAndDecode();
}
Expand All @@ -253,6 +258,10 @@ export default class Quagga {
} else {
this.context.framegrabber.attachData(this.context.inputImageWrapper?.data);
this.context.framegrabber.grab();
// Apply preprocessing if configured
if (this.context.config?.preprocessing && this.context.config.preprocessing.length > 0 && this.context.inputImageWrapper) {
applyPreprocessors(this.context.inputImageWrapper, this.context.config.preprocessing as PreprocessorFunction[]);
}
this.locateAndDecode();
}
};
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading