qrcode-reader-vue3
Version:
A set of Vue.js components for detecting and decoding QR codes.
150 lines (125 loc) • 4.43 kB
text/typescript
import { DropImageFetchError } from "./errors";
import { BarcodeDetector, type DetectedBarcode } from "barcode-detector";
import { eventOn } from "./util";
export interface QRCodeLocation {
topLeftCorner: { x: number; y: number };
topRightCorner: { x: number; y: number };
bottomRightCorner: { x: number; y: number };
bottomLeftCorner: { x: number; y: number };
topLeftFinderPattern: Record<string, never>;
topRightFinderPattern: Record<string, never>;
bottomLeftFinderPattern: Record<string, never>;
}
export interface QRCodeResult {
content: string | null;
location: QRCodeLocation | null;
imageData: ImageData | null;
binaryData: Uint8ClampedArray | null;
}
const adaptOldFormat = (detectedCodes: DetectedBarcode[]): QRCodeResult => {
if (detectedCodes.length > 0) {
const [firstCode] = detectedCodes;
const [topLeftCorner, topRightCorner, bottomRightCorner, bottomLeftCorner] =
firstCode.cornerPoints;
return {
content: firstCode.rawValue,
location: {
topLeftCorner,
topRightCorner,
bottomRightCorner,
bottomLeftCorner,
// not supported by native API:
topLeftFinderPattern: {},
topRightFinderPattern: {},
bottomLeftFinderPattern: {},
},
imageData: null,
binaryData: null,
};
} else {
return {
content: null,
location: null,
imageData: null,
binaryData: null,
};
}
};
interface ScanState {
lastScanned: number;
contentBefore: string | null;
locationBefore: QRCodeLocation | null;
binaryData: Uint8ClampedArray | null;
}
export interface KeepScanningOptions {
detectHandler: (result: QRCodeResult) => void;
locateHandler: (detectedCodes: DetectedBarcode[]) => void;
minDelay: number;
}
/**
* Continuously extracts frames from camera stream and tries to read
* potentially pictured QR codes.
*/
export const keepScanning = (
videoElement: HTMLVideoElement,
options: KeepScanningOptions
): void => {
const barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] });
const { detectHandler, locateHandler, minDelay } = options;
const processFrame =
(state: ScanState) =>
async (timeNow: number): Promise<void> => {
if (videoElement.readyState > 1) {
const { lastScanned, contentBefore, locationBefore } = state;
if (timeNow - lastScanned >= minDelay) {
const detectedCodes = await barcodeDetector.detect(videoElement);
const { content, location, imageData, binaryData } =
adaptOldFormat(detectedCodes);
if (content !== null && content !== contentBefore) {
detectHandler({ content, location, imageData, binaryData });
}
if (location !== null || locationBefore !== null) {
locateHandler(detectedCodes);
}
window.requestAnimationFrame(
processFrame({
lastScanned: timeNow,
contentBefore: content ?? contentBefore,
locationBefore: location,
binaryData: binaryData,
})
);
} else {
window.requestAnimationFrame(processFrame(state));
}
}
};
processFrame({
contentBefore: null,
locationBefore: null,
lastScanned: performance.now(),
binaryData: null,
})(performance.now());
};
const imageElementFromUrl = async (url: string): Promise<HTMLImageElement> => {
if (url.startsWith("http") && url.includes(location.host) === false) {
throw new DropImageFetchError();
}
const image = document.createElement("img");
image.src = url;
await eventOn(image, "load");
return image;
};
export const processFile = async (
file: File | Blob | ImageBitmap | ImageData
): Promise<QRCodeResult> => {
const barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] });
const detectedCodes = await barcodeDetector.detect(file);
return adaptOldFormat(detectedCodes);
};
export const processUrl = async (url: string): Promise<QRCodeResult> => {
const barcodeDetector = new BarcodeDetector({ formats: ["qr_code"] });
const image = await imageElementFromUrl(url);
const detectedCodes = await barcodeDetector.detect(image);
return adaptOldFormat(detectedCodes);
};