diff --git a/.changeset/breezy-buckets-develop.md b/.changeset/breezy-buckets-develop.md new file mode 100644 index 0000000..a210b93 --- /dev/null +++ b/.changeset/breezy-buckets-develop.md @@ -0,0 +1,5 @@ +--- +'@capacitor-mlkit/barcode-scanning': minor +--- + +Added web plugin diff --git a/packages/barcode-scanning/README.md b/packages/barcode-scanning/README.md index c9080cd..c70f941 100644 --- a/packages/barcode-scanning/README.md +++ b/packages/barcode-scanning/README.md @@ -10,7 +10,7 @@ Unofficial Capacitor plugin for [ML Kit Barcode Scanning](https://developers.goo - ⏺️ Define detection area - 🏞️ Reading barcodes from images - πŸ”¦ Torch and Autofocus support -- πŸ”‹ Supports Android and iOS +- πŸ”‹ Supports Android, iOS and web For a complete list of **supported barcodes**, see [BarcodeFormat](#barcodeformat). @@ -273,6 +273,12 @@ body.barcode-scanner-active { If you can't see the camera view, make sure all elements in the DOM are not visible or have a transparent background to debug the issue. +### Web specifics + +You can customize the camera view by using the `videoElementId` option. + +Some browsers does not implement BarcodeDetector API ([see which](https://caniuse.com/mdn-api_barcodedetector)). If you need to support them you can use the [polyfill](https://www.npmjs.com/package/barcode-detector), the "Side Effects" approach. + ## API @@ -318,8 +324,6 @@ startScan(options?: StartScanOptions | undefined) => Promise Start scanning for barcodes. -Only available on Android and iOS. - | Param | Type | | ------------- | ------------------------------------------------------------- | | **`options`** | StartScanOptions | @@ -337,8 +341,6 @@ stopScan() => Promise Stop scanning for barcodes. -Only available on Android and iOS. - **Since:** 0.0.1 -------------------- @@ -400,8 +402,6 @@ isSupported() => Promise Returns whether or not the barcode scanner is supported. -Available on Android and iOS. - **Returns:** Promise<IsSupportedResult> **Since:** 0.0.1 @@ -417,8 +417,6 @@ enableTorch() => Promise Enable camera's torch (flash) during a scan session. -Only available on Android and iOS. - **Since:** 0.0.1 -------------------- @@ -653,7 +651,7 @@ addListener(eventName: 'barcodeScanned', listenerFunc: (event: BarcodeScannedEve Called when a barcode is scanned. -Available on Android and iOS. +Only available on Android and iOS. | Param | Type | | ------------------ | --------------------------------------------------------------------------------------- | @@ -675,8 +673,6 @@ addListener(eventName: 'barcodesScanned', listenerFunc: (event: BarcodesScannedE Called when barcodes are scanned. -Available on Android and iOS. - | Param | Type | | ------------------ | ----------------------------------------------------------------------------------------- | | **`eventName`** | 'barcodesScanned' | @@ -697,8 +693,6 @@ addListener(eventName: 'scanError', listenerFunc: (event: ScanErrorEvent) => voi Called when an error occurs during the scan. -Available on Android and iOS. - | Param | Type | | ------------------ | ----------------------------------------------------------------------------- | | **`eventName`** | 'scanError' | @@ -751,10 +745,11 @@ Remove all listeners for this plugin. #### StartScanOptions -| Prop | Type | Description | Since | -| ---------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----- | -| **`formats`** | BarcodeFormat[] | Improve the speed of the barcode scanner by configuring the barcode formats to scan for. | 0.0.1 | -| **`lensFacing`** | LensFacing | Configure the camera (front or back) to use. | 0.0.1 | +| Prop | Type | Description | Since | +| -------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| **`formats`** | BarcodeFormat[] | Improve the speed of the barcode scanner by configuring the barcode formats to scan for. | 0.0.1 | +| **`lensFacing`** | LensFacing | Configure the camera (front or back) to use. | 0.0.1 | +| **`videoElementId`** | string | Element ID to set on the video element to use for the camera preview. Element will be crreated, but you can customize it with your own styles. Only available on Web. | 5.1.0 | #### ReadBarcodesFromImageResult @@ -768,12 +763,12 @@ Remove all listeners for this plugin. | Prop | Type | Description | Since | | ------------------ | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`bytes`** | number[] | Raw bytes as it was encoded in the barcode. | 0.0.1 | +| **`bytes`** | number[] | Raw bytes as it was encoded in the barcode. Only available on Android and iOS. | 0.0.1 | | **`cornerPoints`** | [[number, number], [number, number], [number, number], [number, number]] | The four corner points of the barcode in clockwise order starting with top-left. This property is currently only supported by the `startScan(...)` method. | 0.0.1 | -| **`displayValue`** | string | The barcode value in a human readable format. | 0.0.1 | +| **`displayValue`** | string | The barcode value in a human readable format. Only available on Android and iOS. | 0.0.1 | | **`format`** | BarcodeFormat | The barcode format. | 0.0.1 | | **`rawValue`** | string | The barcode value in a machine readable format. | 0.0.1 | -| **`valueType`** | BarcodeValueType | The barcode value type. | 0.0.1 | +| **`valueType`** | BarcodeValueType | The barcode value type. Only available on Android and iOS. On web this property is always `BarcodeValueType.Unknown`. | 0.0.1 | #### ReadBarcodesFromImageOptions @@ -900,6 +895,15 @@ Remove all listeners for this plugin. ### Type Aliases +#### BarcodeFormat + +The possible types of barcode format that can be detected using the +Barcode Detection API. This list may change in the future. +Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API + +'aztec' | 'code_128' | 'code_39' | 'code_93' | 'codabar' | 'data_matrix' | 'ean_13' | 'ean_8' | 'itf' | 'pdf417' | 'qr_code' | 'upc_a' | 'upc_e' | 'unknown' + + #### CameraPermissionState PermissionState | 'limited' diff --git a/packages/barcode-scanning/src/barcode-detector.d.ts b/packages/barcode-scanning/src/barcode-detector.d.ts new file mode 100644 index 0000000..65dff49 --- /dev/null +++ b/packages/barcode-scanning/src/barcode-detector.d.ts @@ -0,0 +1,85 @@ + +/** + * The possible types of barcode format that can be detected using the + * Barcode Detection API. This list may change in the future. + * Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API + */ +export type BarcodeFormat = 'aztec' + | 'code_128' + | 'code_39' + | 'code_93' + | 'codabar' + | 'data_matrix' + | 'ean_13' + | 'ean_8' + | 'itf' + | 'pdf417' + | 'qr_code' + | 'upc_a' + | 'upc_e' + | 'unknown'; + +/** + * The return type of the Barcode Detect API `detect` function that + * describes a barcode that has been recognized by the API. + */ +export interface DetectedBarcode { + /** + * A DOMRectReadOnly, which returns the dimensions of a rectangle + * representing the extent of a detected barcode, aligned with the + * image + */ + boundingBox: DOMRectReadOnly; + /** + * The x and y co-ordinates of the four corner points of the detected + * barcode relative to the image, starting with the top left and working + * clockwise. This may not be square due to perspective distortions + * within the image. + */ + cornerPoints: [ + { x: number, y: number }, + { x: number, y: number }, + { x: number, y: number }, + { x: number, y: number }, + ]; + /** + * The detected barcode format + */ + format: BarcodeFormat; + + /** + * A string decoded from the barcode data + */ + rawValue: string; +} + +/** + * Options for describing how a BarcodeDetector should be initialised + */ +export interface BarcodeDetectorOptions { + /** + * Which formats the barcode detector should detect + */ + formats?: BarcodeFormat[]; +} + +/** + * The BarcodeDetector interface of the Barcode Detection API allows + * detection of linear and two dimensional barcodes in images. + */ +export class BarcodeDetector { + /** + * Initialize a Barcode Detector instance + */ + constructor(options?: BarcodeDetectorOptions); + + /** + * Retrieve the formats that are supported by the detector + */ + static getSupportedFormats(): Promise; + + /** + * Attempt to detect barcodes from an image source + */ + public detect(source: ImageBitmapSource): Promise; +} diff --git a/packages/barcode-scanning/src/definitions.ts b/packages/barcode-scanning/src/definitions.ts index b9dabdb..dc0bd59 100644 --- a/packages/barcode-scanning/src/definitions.ts +++ b/packages/barcode-scanning/src/definitions.ts @@ -4,16 +4,12 @@ export interface BarcodeScannerPlugin { /** * Start scanning for barcodes. * - * Only available on Android and iOS. - * * @since 0.0.1 */ startScan(options?: StartScanOptions): Promise; /** * Stop scanning for barcodes. * - * Only available on Android and iOS. - * * @since 0.0.1 */ stopScan(): Promise; @@ -45,16 +41,12 @@ export interface BarcodeScannerPlugin { /** * Returns whether or not the barcode scanner is supported. * - * Available on Android and iOS. - * * @since 0.0.1 */ isSupported(): Promise; /** * Enable camera's torch (flash) during a scan session. * - * Only available on Android and iOS. - * * @since 0.0.1 * @deprecated Use the [Capacitor Torch](https://capawesome.io/plugins/torch/) plugin instead. */ @@ -176,7 +168,7 @@ export interface BarcodeScannerPlugin { /** * Called when a barcode is scanned. * - * Available on Android and iOS. + * Only available on Android and iOS. * * @since 0.0.1 * @deprecated Use the `barcodesScanned` event listener instead. @@ -188,8 +180,6 @@ export interface BarcodeScannerPlugin { /** * Called when barcodes are scanned. * - * Available on Android and iOS. - * * @since 6.2.0 */ addListener( @@ -199,8 +189,6 @@ export interface BarcodeScannerPlugin { /** * Called when an error occurs during the scan. * - * Available on Android and iOS. - * * @since 0.0.1 */ addListener( @@ -245,6 +233,16 @@ export interface StartScanOptions { * @since 0.0.1 */ lensFacing?: LensFacing; + /** + * Element ID to set on the video element to use for the + * camera preview. Element will be crreated, but you can + * customize it with your own styles. + * + * Only available on Web. + * + * @since 5.1.0 + */ + videoElementId?: string; } /** @@ -478,6 +476,8 @@ export interface Barcode { /** * Raw bytes as it was encoded in the barcode. * + * Only available on Android and iOS. + * * @since 0.0.1 * @example [67, 97, 112, 97, 99, 105, 116, 111, 114, 74, 83] */ @@ -501,10 +501,12 @@ export interface Barcode { /** * The barcode value in a human readable format. * + * Only available on Android and iOS. + * * @since 0.0.1 * @example "CapacitorJS" */ - displayValue: string; + displayValue?: string; /** * The barcode format. * @@ -522,6 +524,9 @@ export interface Barcode { /** * The barcode value type. * + * Only available on Android and iOS. + * On web this property is always `BarcodeValueType.Unknown`. + * * @since 0.0.1 * @example "TEXT" */ diff --git a/packages/barcode-scanning/src/web.ts b/packages/barcode-scanning/src/web.ts index 2bee463..25b6b52 100644 --- a/packages/barcode-scanning/src/web.ts +++ b/packages/barcode-scanning/src/web.ts @@ -1,6 +1,8 @@ import { CapacitorException, ExceptionCode, WebPlugin } from '@capacitor/core'; import type { + BarcodeFormat as PluginBarcodeFormat, + BarcodesScannedEvent, BarcodeScannerPlugin, GetMaxZoomRatioResult, GetMinZoomRatioResult, @@ -16,31 +18,94 @@ import type { SetZoomRatioOptions, StartScanOptions, } from './definitions'; +import { BarcodeValueType, LensFacing } from './definitions'; + +import type { DetectedBarcode, BarcodeFormat as WebBarcodeFormat } from './barcode-detector'; export class BarcodeScannerWeb extends WebPlugin implements BarcodeScannerPlugin { - async startScan(_options?: StartScanOptions): Promise { - throw this.createUnavailableException(); + private readonly _isSupported = 'BarcodeDetector' in window; + private intervalId: number | undefined; + private stream: MediaStream | undefined; + private videoElement: HTMLVideoElement | undefined; + + async startScan(options?: StartScanOptions): Promise { + if (!window.BarcodeDetector) { + this.throwUnsupportedError(); + } + + this.videoElement = document.createElement('video'); + this.videoElement.style.position = 'absolute'; + this.videoElement.style.top = '0'; + this.videoElement.style.left = '0'; + this.videoElement.style.right = '0'; + this.videoElement.style.bottom = '0'; + this.videoElement.style.width = '100%'; + this.videoElement.style.height = '100%'; + this.videoElement.style.zIndex = '-9999'; + this.videoElement.style.objectFit = 'cover'; + if (options?.videoElementId) this.videoElement.id = options.videoElementId; + document.body.appendChild(this.videoElement); + + this.stream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: { + ideal: options?.lensFacing === LensFacing.Back ? 'environment' : 'user', + }, + }, + audio: false, + }); + this.videoElement.srcObject = this.stream; + await this.videoElement.play(); + const barcodeDetector = new BarcodeDetector({ + formats: options?.formats?.map(format => format.toLowerCase() as WebBarcodeFormat), + }); + this.intervalId = window.setInterval(async () => { + const barcodes = await barcodeDetector.detect(this.videoElement!); + if (barcodes.length === 0) { + return; + } else { + this.handleScannedBarcodes(barcodes); + } + }, 1000); // Most use-cases don't need to scan faster than once per second, so it does not make sense to waste resources by scanning more frequently. } async stopScan(): Promise { - throw this.createUnavailableException(); + if (!this._isSupported) { + this.throwUnsupportedError(); + } + if (this.intervalId) { + window.clearInterval(this.intervalId); + this.intervalId = undefined; + } + if (this.stream) { + this.stream.getTracks().forEach(track => { + track.stop(); + }); + this.stream = undefined; + } + if (this.videoElement) { + this.videoElement.remove(); + this.videoElement = undefined; + } } async readBarcodesFromImage( _options: ReadBarcodesFromImageOptions, ): Promise { - throw this.createUnavailableException(); + throw this.createUnimplementedException(); } async scan(): Promise { - throw this.createUnavailableException(); + throw this.createUnimplementedException(); } async isSupported(): Promise { - throw this.createUnavailableException(); + return { + supported: this._isSupported, + }; } async enableTorch(): Promise { @@ -64,23 +129,23 @@ export class BarcodeScannerWeb } async setZoomRatio(_options: SetZoomRatioOptions): Promise { - throw this.createUnavailableException(); + throw this.createUnimplementedException(); } async getZoomRatio(): Promise { - throw this.createUnavailableException(); + throw this.createUnimplementedException(); } async getMinZoomRatio(): Promise { - throw this.createUnavailableException(); + throw this.createUnimplementedException(); } async getMaxZoomRatio(): Promise { - throw this.createUnavailableException(); + throw this.createUnimplementedException(); } async openSettings(): Promise { - throw this.createUnavailableException(); + throw this.createUnimplementedException(); } async isGoogleBarcodeScannerModuleAvailable(): Promise { @@ -99,10 +164,44 @@ export class BarcodeScannerWeb throw this.createUnavailableException(); } + private createUnimplementedException(): CapacitorException { + return new CapacitorException( + 'This Barcode Scanner plugin method is not implemented yet on this platform.', + ExceptionCode.Unimplemented, + ); + } + private createUnavailableException(): CapacitorException { return new CapacitorException( 'This Barcode Scanner plugin method is not available on this platform.', ExceptionCode.Unavailable, ); } + + private throwUnsupportedError(): never { + throw this.unavailable( + 'Barcode Detector API not available in this browser. You can install polyfill, check README.md for more information.', + ); + } + + private handleScannedBarcodes(barcodes: DetectedBarcode[]): void { + const result: BarcodesScannedEvent = { + barcodes: barcodes.map(barcode => ({ + cornerPoints: [ + [barcode.cornerPoints[0].x, barcode.cornerPoints[0].y], + [barcode.cornerPoints[1].x, barcode.cornerPoints[1].y], + [barcode.cornerPoints[2].x, barcode.cornerPoints[2].y], + [barcode.cornerPoints[3].x, barcode.cornerPoints[3].y], + ], + rawValue: barcode.rawValue, + format: barcode.format.toUpperCase() as PluginBarcodeFormat, + valueType: BarcodeValueType.Unknown, + })), + }; + this.notifyListeners('barcodesScanned', result); + } +} + +declare global { + var BarcodeDetector: typeof import("./barcode-detector").BarcodeDetector; }