Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions packages/pluggableWidgets/barcode-scanner-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- We added the logic to switch to use native browser BarcodeDetector API if it is available instead of using zxing library.
- We increase ideal image resolution to improve performance on higher end devices.

## [2.4.2] - 2024-08-30

### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@mendix/barcode-scanner-web",
"widgetName": "BarcodeScanner",
"version": "2.4.2",
"version": "2.5.0",
"description": "Displays a barcode scanner",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const BarcodeScanner: FunctionComponent<BarcodeScannerContainerProps> = p
height={props.height}
widthUnit={props.widthUnit}
width={props.width}
detectionLogic={props.detectionLogic}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
<systemProperty key="Visibility" />
</propertyGroup>
</propertyGroup>

<propertyGroup caption="Dimensions">
<propertyGroup caption="Dimensions">
<property key="widthUnit" type="enumeration" defaultValue="percentage">
Expand Down Expand Up @@ -92,5 +91,15 @@
</property>
</propertyGroup>
</propertyGroup>
<propertyGroup caption="Advanced">
<property key="detectionLogic" type="enumeration" defaultValue="native">
<caption>Detection logic</caption>
<description>Choose the detection logic to use for barcode scanning.</description>
<enumerationValues>
<enumerationValue key="zxing">ZXing</enumerationValue>
<enumerationValue key="native">BarcodeDetector API (experimental fast scan, fallback to ZXing)</enumerationValue>
</enumerationValues>
</property>
</propertyGroup>
</properties>
</widget>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Alert } from "@mendix/widget-plugin-component-kit/Alert";
import { Dimensions, getDimensions } from "@mendix/widget-plugin-platform/utils/get-dimensions";
import { useCustomErrorMessage } from "../hooks/useCustomErrorMessage";
import { useReader } from "../hooks/useReader";
import { BarcodeFormatsType } from "../../typings/BarcodeScannerProps";
import { BarcodeFormatsType, BarcodeScannerContainerProps } from "../../typings/BarcodeScannerProps";

import "../ui/BarcodeScanner.scss";

Expand Down Expand Up @@ -57,6 +57,7 @@ export interface BarcodeScannerProps extends Dimensions {
class: string;
useAllFormats: boolean;
barcodeFormats?: BarcodeFormatsType[];
detectionLogic?: BarcodeScannerContainerProps["detectionLogic"];
}

export function BarcodeScanner({
Expand All @@ -65,18 +66,23 @@ export function BarcodeScanner({
class: className,
barcodeFormats,
useAllFormats,
detectionLogic,
...dimensions
}: BarcodeScannerProps): ReactElement | null {
const [errorMessage, setError] = useCustomErrorMessage();
const canvasMiddleRef = useRef<HTMLDivElement>(null);
const videoRef = useReader({
const reader = useReader({
onSuccess: onDetect,
onError: setError,
useCrop: showMask,
barcodeFormats,
useAllFormats,
canvasMiddleRef
canvasMiddleRef,
detectionLogic
});

const { ref: videoRef, useBrowserAPI } = reader ?? {};

const supportsCameraAccess = typeof navigator?.mediaDevices?.getUserMedia === "function";
const onCanPlay = useCallback((event: SyntheticEvent<HTMLVideoElement>) => {
if (event.currentTarget.paused) {
Expand Down Expand Up @@ -104,7 +110,7 @@ export function BarcodeScanner({

return (
<BarcodeScannerOverlay
class={className}
class={classNames(className, `mx-${useBrowserAPI ? "barcode" : "zxing"}-detector`)}
showMask={showMask}
canvasMiddleMiddleRef={canvasMiddleRef}
{...dimensions}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
exports[`Barcode scanner does not show the overlay when the user opts out of it 1`] = `
<DocumentFragment>
<div
class="mx-barcode-scanner"
class="mx-barcode-scanner mx-zxing-detector"
style="width: 100%; height: 100%;"
>
<div
Expand All @@ -20,7 +20,7 @@ exports[`Barcode scanner does not show the overlay when the user opts out of it
exports[`Barcode scanner renders video and overlay correctly 1`] = `
<DocumentFragment>
<div
class="mx-barcode-scanner"
class="mx-barcode-scanner mx-zxing-detector"
style="width: 100%; height: 100%;"
>
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { BarcodeFormatsType } from "../../typings/BarcodeScannerProps";
import type { BarcodeDetector, BarcodeDetectorOptions, BarcodeFormat, DetectedBarcode } from "./barcode-detector";

// Map Mendix barcode format types to native BarcodeDetector format strings
const mapToNativeFormat = (format: string): string => {
const formatMap: Record<string, string> = {
UPC_A: "upc_a",
UPC_E: "upc_e",
EAN_8: "ean_8",
EAN_13: "ean_13",
CODE_39: "code_39",
CODE_128: "code_128",
ITF: "itf",
QR_CODE: "qr_code",
DATA_MATRIX: "data_matrix",
AZTEC: "aztec",
PDF_417: "pdf417"
};
return formatMap[format] || format.toLowerCase();
};

// Check if BarcodeDetector API is available
export const isBarcodeDetectorSupported = (): boolean => {
return typeof globalThis !== "undefined" && "BarcodeDetector" in globalThis;
};

// Get supported formats for BarcodeDetector
export const getBarcodeDetectorSupportedFormats = async (): Promise<string[]> => {
if (!isBarcodeDetectorSupported()) {
return [];
}
try {
const detector = new window.BarcodeDetector!();
return await detector.getSupportedFormats();
} catch (error) {
console.warn("Failed to get BarcodeDetector supported formats:", error);
return [];
}
};

// Create BarcodeDetector options from widget configuration
export const createBarcodeDetectorOptions = (
useAllFormats: boolean,
barcodeFormats?: BarcodeFormatsType[]
): BarcodeDetectorOptions => {
const options: BarcodeDetectorOptions = {};

if (!useAllFormats && barcodeFormats && barcodeFormats.length > 0) {
options.formats = barcodeFormats.map(format => mapToNativeFormat(format.barcodeFormat)) as Array<
BarcodeFormat["format"]
>;
}
// If useAllFormats is true or no specific formats, don't specify formats to use all supported

return options;
};

// Create BarcodeDetector instance
export const createBarcodeDetector = (options?: BarcodeDetectorOptions): BarcodeDetector | null => {
if (!isBarcodeDetectorSupported()) {
return null;
}

try {
return new window.BarcodeDetector!(options);
} catch (error) {
console.warn("Failed to create BarcodeDetector:", error);
return null;
}
};

// Detect barcodes from video or canvas element using BarcodeDetector API
export const detectBarcodesFromElement = async (
detector: BarcodeDetector | null,
element: HTMLVideoElement | HTMLCanvasElement | null
): Promise<DetectedBarcode[]> => {
try {
if (!detector || !element || (element as HTMLVideoElement).readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
return [];
}
return await detector.detect(element);
} catch (error) {
console.warn("BarcodeDetector failed to detect:", (element as HTMLVideoElement).readyState, error);
return [];
}
};

// Convert video frame to canvas for processing
export const captureVideoFrame = (video: HTMLVideoElement, canvas?: HTMLCanvasElement): HTMLCanvasElement => {
if (!canvas) {
canvas = document.createElement("canvas");
}

canvas.width = video.videoWidth;
canvas.height = video.videoHeight;

const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
}

return canvas;
};

export const setupVideoElement = (video: HTMLVideoElement, stream: MediaStream): void => {
video.autofocus = true;
video.playsInline = true; // Fix error in Safari
video.muted = true;
video.srcObject = stream;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// TypeScript definitions for the BarcodeDetector API
// Based on https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector

// https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API#supported_barcode_formats
export interface BarcodeFormat {
format:
| "aztec"
| "code_128"
| "code_39"
| "code_93"
| "codabar"
| "data_matrix"
| "ean_13"
| "ean_8"
| "itf"
| "pdf417"
| "qr_code"
| "unknown"
| "upc_a"
| "upc_e";
}

export interface DetectedBarcode {
boundingBox: DOMRectReadOnly;
cornerPoints: ReadonlyArray<{ x: number; y: number }>;
format: BarcodeFormat["format"];
rawValue: string;
}

export interface BarcodeDetectorOptions {
formats?: Array<BarcodeFormat["format"]>;
}

export interface BarcodeDetector {
detect(image: ImageBitmapSource): Promise<DetectedBarcode[]>;
getSupportedFormats(): Promise<Array<BarcodeFormat["format"]>>;
}

export type BarcodeDetectorConstructor = new (options?: BarcodeDetectorOptions) => BarcodeDetector;

// Extend Window interface to include BarcodeDetector
declare global {
interface Window {
BarcodeDetector?: BarcodeDetectorConstructor;
}
}

export interface MxBarcodeReader {
start(onSuccess: (data: string) => void, onError: (e: Error) => void): Promise<void>;
stop(): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,30 @@ import {
HybridBinarizer,
Result
} from "@zxing/library";
import { RefObject } from "react";
import { BarcodeFormatsType } from "typings/BarcodeScannerProps";

export const mediaStreamConstraints: MediaStreamConstraints = {
audio: false,
video: {
facingMode: "environment",
width: { min: 1280, ideal: 4096, max: 4096 },
height: { min: 720, ideal: 2160, max: 2160 }
}
};

export type ReaderProps = {
onSuccess?: (data: string) => void;
onError?: (e: Error) => void;
useCrop: boolean;
barcodeFormats?: BarcodeFormatsType[];
useAllFormats: boolean;
canvasMiddleRef: RefObject<HTMLDivElement>;
detectionLogic?: "zxing" | "native";
};

export type UseReaderHook = (args: ReaderProps) => { ref: RefObject<HTMLVideoElement>; useBrowserAPI: boolean };

export const returnVideoWidthHeight = (
curVideoRef: HTMLVideoElement,
canvasMiddle: HTMLDivElement
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { RefObject } from "react";
import { BarcodeDetector, MxBarcodeReader } from "../helpers/barcode-detector";
import {
createBarcodeDetector,
createBarcodeDetectorOptions,
detectBarcodesFromElement
} from "../helpers/barcode-detector-utils";
import { mediaStreamConstraints, ReaderProps } from "../helpers/utils";

export class Reader implements MxBarcodeReader {
private videoRef: RefObject<HTMLVideoElement>;
barcodeDetector: BarcodeDetector | null;
useCrop: boolean;
stopped: boolean = false;
canvasMiddleRef: RefObject<HTMLDivElement>;
stream: MediaStream | null = null;
decodeInterval: NodeJS.Timeout | number | null = null;

constructor(args: ReaderProps, videoRef: RefObject<HTMLVideoElement>) {
this.videoRef = videoRef;
this.useCrop = args.useCrop;
this.canvasMiddleRef = args.canvasMiddleRef;
const options = createBarcodeDetectorOptions(args.useAllFormats, args.barcodeFormats);
this.barcodeDetector = createBarcodeDetector(options);
}

start = async (onSuccess: (data: string) => void, onError: (e: Error) => void): Promise<void> => {
if (this.videoRef.current === null) {
return;
}

if (this.barcodeDetector === null) {
if (onError) {
onError(new Error("Failed to create barcode detector"));
}

return;
}

this.stream = await navigator.mediaDevices.getUserMedia(mediaStreamConstraints);

this.videoRef.current.autofocus = true;
this.videoRef.current.playsInline = true; // Fix error in Safari
this.videoRef.current.muted = true;
this.videoRef.current.autoplay = true;
this.videoRef.current.srcObject = this.stream;
this.decodeInterval = setTimeout(this.decodeStream, 50, onSuccess, onError);
};

stop = (): void => {
if (this.decodeInterval) {
clearTimeout(this.decodeInterval);
}
this.stream?.getVideoTracks().forEach(track => track.stop());
this.barcodeDetector = null;
};

decodeStream = async (
// loop decode canvas till it finds a result
resolve: (value: string) => void,
reject: (reason?: Error) => void
): Promise<void> => {
try {
if (this.decodeInterval) {
clearTimeout(this.decodeInterval);
}
const detectionCode = await detectBarcodesFromElement(this.barcodeDetector, this.videoRef.current);

if (detectionCode && detectionCode.length > 0) {
if (resolve) resolve(detectionCode[0].rawValue);
} else {
this.decodeInterval = setTimeout(this.decodeStream, 50, resolve, reject);
}
} catch (error) {
console.log("decodeStream error", error);
reject(error);
}
};
}
Loading
Loading