diff --git a/.changeset/pink-tips-unite.md b/.changeset/pink-tips-unite.md new file mode 100644 index 00000000..16cf192b --- /dev/null +++ b/.changeset/pink-tips-unite.md @@ -0,0 +1,5 @@ +--- +"zxing-wasm": minor +--- + +Support WeChat Mini Program. diff --git a/.changeset/pretty-ravens-send.md b/.changeset/pretty-ravens-send.md new file mode 100644 index 00000000..ce5c50e7 --- /dev/null +++ b/.changeset/pretty-ravens-send.md @@ -0,0 +1,5 @@ +--- +"zxing-wasm": minor +--- + +Accept `ArrayBuffer` and `Uint8Array` as input types in `readBarcodes`. diff --git a/README.md b/README.md index f0c05819..8d2eba52 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Apart from ES and CJS modules, this package also ships IIFE scripts. The registe ### [`readBarcodes`](https://zxing-wasm.deno.dev/functions/full.readBarcodes.html) -[`readBarcodes`](https://zxing-wasm.deno.dev/functions/full.readBarcodes.html) accepts an image [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob), image [`File`](https://developer.mozilla.org/docs/Web/API/File), or an [`ImageData`](https://developer.mozilla.org/docs/Web/API/ImageData) as its first argument, and various options are supported in [`ReaderOptions`](https://zxing-wasm.deno.dev/interfaces/full.ReaderOptions.html) as an optional second argument. +[`readBarcodes`](https://zxing-wasm.deno.dev/functions/full.readBarcodes.html) accepts an image [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob), image [`File`](https://developer.mozilla.org/docs/Web/API/File), [`ArrayBuffer`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [`Uint8Array`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), or an [`ImageData`](https://developer.mozilla.org/docs/Web/API/ImageData) as its first argument, and various options are supported in [`ReaderOptions`](https://zxing-wasm.deno.dev/interfaces/full.ReaderOptions.html) as an optional second argument. The return result of this function is a `Promise` of an array of [`ReadResult`](https://zxing-wasm.deno.dev/interfaces/full.ReadResult.html)s. @@ -336,6 +336,30 @@ If you want to use this library in non-web runtimes (such as Node.js, Bun, Deno, }); ``` +> [!NOTE] +> To use this library in a WeChat mini program , there are several things to keep in mind: +> +> 1. Only the `zxing-wasm` import path is supported; `zxing-wasm/reader` or `zxing-wasm/writer` is not supported. +> 2. Before using the library, you need to copy/move the `node_modules/zxing-wasm/dist/full/zxing_full.wasm` file into your project directory. +> 3. You must use `prepareZXingModule` to configure how the `.wasm` file will be fetched, loaded, and compiled before calling `readBarcodes` or `writeBarcode`. This is mandatory, and you can do so with the following code: +> +> ```typescript +> prepareZXingModule({ +> overrides: { +> instantiateWasm(imports, successCallback) { +> WXWebAssembly.instantiate("path/to/zxing_full.wasm", imports).then( +> ({ instance }) => successCallback(instance), +> ); +> return {}; +> }, +> }, +> }); +> ``` +> +> Note that WeChat mini programs use `WXWebAssembly` instead of the standard `WebAssembly`, and the first argument in `WXWebAssembly.instantiate` should point to the location where the `zxing_full.wasm` file was moved earlier. +> +> 4. This library uses a bare minimum `Blob` polyfill in the mini program environment so that no errors will be thrown if you call `writeBarcode`. However, it's recommended to use a full-fledged `Blob` polyfill for not breaking other parts of your program. + > [!IMPORTANT] > > Each version of this library has a unique corresponding `.wasm` file. If you choose to serve it yourself, please ensure that the `.wasm` file matches the version of the `zxing-wasm` library you are using. Otherwise, you may encounter unexpected errors. diff --git a/package.json b/package.json index 945c3c02..c4a90414 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ ], "main": "./dist/cjs/full/index.js", "module": "./dist/es/full/index.js", + "miniprogram": "./dist/miniprogram", "exports": { ".": { "import": "./dist/es/full/index.js", @@ -171,7 +172,8 @@ "build:es": "vite build", "build:cjs": "tsx ./scripts/build-cjs.ts", "build:iife": "tsx ./scripts/build-iife.ts", - "build": "conc \"pnpm:build:es\" \"pnpm:build:cjs\" \"pnpm:build:iife\"", + "build:miniprogram": "tsx ./scripts/build-miniprogram.ts", + "build": "conc \"pnpm:build:es\" \"pnpm:build:cjs\" \"pnpm:build:iife\" \"pnpm:build:miniprogram\"", "postbuild:es": "tsc -p ./tsconfig.pkg.json --declarationDir ./dist/es", "postbuild:cjs": "tsc -p ./tsconfig.pkg.json --declarationDir ./dist/cjs", "postbuild": "conc \"pnpm:copy:wasm\" \"pnpm:docs:build\"", @@ -189,6 +191,7 @@ "@babel/types": "^7.26.9", "@biomejs/biome": "1.9.4", "@changesets/cli": "^2.28.1", + "@napi-rs/canvas": "^0.1.67", "@types/babel__core": "^7.20.5", "@types/node": "^22.13.5", "@vitest/ui": "^3.0.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1f72d58..e2a74387 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,9 @@ importers: '@changesets/cli': specifier: ^2.28.1 version: 2.28.1 + '@napi-rs/canvas': + specifier: ^0.1.67 + version: 0.1.67 '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 @@ -717,6 +720,70 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@napi-rs/canvas-android-arm64@0.1.67': + resolution: {integrity: sha512-W+3DFG5h0WU8Vqqb3W5fNmm5/TPH5ECZRinQDK4CAKFSUkc4iZcDwrmyFG9sB4KdHazf1mFVHCpEeVMO6Mk6Zg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.67': + resolution: {integrity: sha512-xzrv7QboI47yhIHR5P5u/9KGswokuOKLiKSukr1Ku03RRJxP6lGuVtrAZAgdRg7F9FsuF2REf2yK53YVb6pMlA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.67': + resolution: {integrity: sha512-SNk9lYBr84N0gW8MZ2IrjygFtbFBILr3SEqMdHzHHuph20SQmssFvJGPZwSSCMEyKAvyqhogbmlew0te5Z4w9Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.67': + resolution: {integrity: sha512-qmBlSvUpl567bzH8tNXi82u5FrL4d0qINqd6K9O7GWGGGFmKMJdrgi2/SW3wwCTxqHBasIDdVWc4KSJfwyaoDQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.67': + resolution: {integrity: sha512-k3nAPQefkMeFuJ65Rqdnx92KX1JXQhEKjjWeKsCJB+7sIBgQUWtHo9c3etfVLv5pkWJJDFi/Zc2soNkH3E8dRA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-arm64-musl@0.1.67': + resolution: {integrity: sha512-lZwHWR1cCP408l86n3Qbs3X1oFeAYMjJIQvQl1VMZh6wo5PfI+jaZSKBUOd8x44TnVllX9yhLY9unNRztk/sUQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.67': + resolution: {integrity: sha512-PdBC9p6bLHA1W3OdA0vTHj701SB/kioGQ1uCFBRMs5KBCaMLb/H4aNi8uaIUIEvBWnxeAjoNcLU7//q0FxEosw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@napi-rs/canvas-linux-x64-gnu@0.1.67': + resolution: {integrity: sha512-kJJX6eWzjipL/LdKOWCJctc88e5yzuXri8+s0V/lN06OwuLGW62TWS3lvi8qlUrGMOfRGabSWWlB4omhASSB8w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-linux-x64-musl@0.1.67': + resolution: {integrity: sha512-jLKiPWGeN6ZzhnaLG7ex7eexsiHJ1mdtPK1qKvETIcu45dApMXyUIHvdL6XWB5gFFtj5ScHzLUxv1vkfPZsoxA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-win32-x64-msvc@0.1.67': + resolution: {integrity: sha512-K/JmkOFbc4iRZYUqJhj0jwqfHA/wNQEmTiGNsgZ6d59yF/IBNp5T0D5eg3B8ghjI8GxDYCiSJ6DNX8mC3Oh2EQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.67': + resolution: {integrity: sha512-VA4Khm/5Kg2bQGx3jXotTC4MloOG8b1Ung80exafUK0k5u6yJmIz3Q2iXeeWZs5weV+LQOEB+CPKsYwEYaGAjw==} + engines: {node: '>= 10'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2872,6 +2939,49 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@napi-rs/canvas-android-arm64@0.1.67': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.67': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.67': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.67': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.67': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.67': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.67': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.67': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.67': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.67': + optional: true + + '@napi-rs/canvas@0.1.67': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.67 + '@napi-rs/canvas-darwin-arm64': 0.1.67 + '@napi-rs/canvas-darwin-x64': 0.1.67 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.67 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.67 + '@napi-rs/canvas-linux-arm64-musl': 0.1.67 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.67 + '@napi-rs/canvas-linux-x64-gnu': 0.1.67 + '@napi-rs/canvas-linux-x64-musl': 0.1.67 + '@napi-rs/canvas-win32-x64-msvc': 0.1.67 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 diff --git a/scripts/build-cjs.ts b/scripts/build-cjs.ts index c539e885..102011f3 100644 --- a/scripts/build-cjs.ts +++ b/scripts/build-cjs.ts @@ -1,8 +1,10 @@ import { writeFile } from "node:fs/promises"; +import { rimraf } from "rimraf"; import { type LibraryOptions, build } from "vite"; import viteConfig from "../vite.config.js"; async function buildCjs() { + await rimraf("dist/cjs"); await build({ ...viteConfig, build: { diff --git a/scripts/build-miniprogram.ts b/scripts/build-miniprogram.ts new file mode 100644 index 00000000..1b0152b3 --- /dev/null +++ b/scripts/build-miniprogram.ts @@ -0,0 +1,42 @@ +import { writeFile } from "node:fs/promises"; +import { rimraf } from "rimraf"; +import { type LibraryOptions, build } from "vite"; +import viteConfig from "../vite.config.js"; + +async function buildMiniprogram() { + await rimraf("dist/miniprogram"); + await build({ + ...viteConfig, + mode: "miniprogram", + build: { + ...viteConfig.build, + target: ["es2018"], + lib: { + ...(viteConfig.build?.lib as LibraryOptions), + entry: { + index: "./src/full/index.ts", + }, + formats: ["cjs"], + }, + outDir: "dist/miniprogram", + rollupOptions: { + ...viteConfig.build?.rollupOptions, + output: { + ...viteConfig.build?.rollupOptions?.output, + manualChunks: {}, + }, + }, + }, + configFile: false, + define: { + ...viteConfig.define, + WebAssembly: "WXWebAssembly", + }, + }); + await writeFile( + "dist/miniprogram/package.json", + `${JSON.stringify({ type: "commonjs" }, undefined, 2)}\n`, + ); +} + +buildMiniprogram(); diff --git a/src/full/index.ts b/src/full/index.ts index 7b756145..5443c4b3 100644 --- a/src/full/index.ts +++ b/src/full/index.ts @@ -74,7 +74,7 @@ export function setZXingModuleOverrides( } export async function readBarcodes( - input: Blob | ImageData, + input: Blob | ArrayBuffer | Uint8Array | ImageData, readerOptions?: ReaderOptions, ) { return readBarcodesWithFactory(zxingModuleFactory, input, readerOptions); diff --git a/src/reader/index.ts b/src/reader/index.ts index f5faaf27..b4d01399 100644 --- a/src/reader/index.ts +++ b/src/reader/index.ts @@ -73,7 +73,7 @@ export function setZXingModuleOverrides( } export async function readBarcodes( - input: Blob | ImageData, + input: Blob | ArrayBuffer | Uint8Array | ImageData, readerOptions?: ReaderOptions, ) { return readBarcodesWithFactory(zxingModuleFactory, input, readerOptions); diff --git a/src/share.ts b/src/share.ts index 2d0b530d..e7e58ca8 100644 --- a/src/share.ts +++ b/src/share.ts @@ -83,25 +83,51 @@ export const ZXING_WASM_VERSION = NPM_PACKAGE_VERSION; export const ZXING_CPP_COMMIT = SUBMODULE_COMMIT; -const DEFAULT_MODULE_OVERRIDES: ZXingModuleOverrides = import.meta.env.PROD - ? { - locateFile: (path, prefix) => { - const match = path.match(/_(.+?)\.wasm$/); - if (match) { - return `https://fastly.jsdelivr.net/npm/zxing-wasm@${NPM_PACKAGE_VERSION}/dist/${match[1]}/${path}`; - } - return prefix + path; - }, - } - : { - locateFile: (path, prefix) => { - const match = path.match(/_(.+?)\.wasm$/); - if (match) { - return `/src/${match[1]}/${path}`; +const DEFAULT_MODULE_OVERRIDES: ZXingModuleOverrides = + import.meta.env.MODE === "miniprogram" + ? { + instantiateWasm() { + throw Error( + `To use zxing-wasm in a WeChat Mini Program, you must provide a custom "instantiateWasm" function, e.g.: + +prepareZXingModule({ + overrides: { + instantiateWasm(imports, successCallback) { + WXWebAssembly.instantiate("path/to/zxing_full.wasm", imports).then(({ instance }) => + successCallback(instance), + ); + return {}; + }, + } +}); + +Learn more: +- https://developers.weixin.qq.com/miniprogram/dev/framework/performance/wasm.html +- https://emscripten.org/docs/api_reference/module.html#Module.instantiateWasm +- https://github.com/Sec-ant/zxing-wasm#integrating-in-non-web-runtimes +`, + ); + }, + } + : import.meta.env.PROD + ? { + locateFile: (path, prefix) => { + const match = path.match(/_(.+?)\.wasm$/); + if (match) { + return `https://fastly.jsdelivr.net/npm/zxing-wasm@${NPM_PACKAGE_VERSION}/dist/${match[1]}/${path}`; + } + return prefix + path; + }, } - return prefix + path; - }, - }; + : { + locateFile: (path, prefix) => { + const match = path.match(/_(.+?)\.wasm$/); + if (match) { + return `/src/${match[1]}/${path}`; + } + return prefix + path; + }, + }; type CachedValue = | [ZXingModuleOverrides] @@ -261,7 +287,7 @@ export function purgeZXingModuleWithFactory( * Reads barcodes from an image using a ZXing module factory. * * @param zxingModuleFactory - Factory function to create a ZXing module instance - * @param input - Source image data as either a Blob or ImageData object + * @param input - Source image data as a Blob, ArrayBuffer, Uint8Array, or ImageData * @param readerOptions - Optional configuration options for barcode reading (defaults to defaultReaderOptions) * @returns An array of ReadResult objects containing decoded barcode information * @@ -271,7 +297,7 @@ export function purgeZXingModuleWithFactory( */ export async function readBarcodesWithFactory( zxingModuleFactory: ZXingModuleFactory, - input: Blob | ImageData, + input: Blob | ArrayBuffer | Uint8Array | ImageData, readerOptions: ReaderOptions = defaultReaderOptions, ) { const requiredReaderOptions: Required = { @@ -283,18 +309,8 @@ export async function readBarcodesWithFactory( }); let zxingReadResultVector: ZXingVector; let bufferPtr: number; - if ("size" in input) { - /* Blob */ - const { size } = input; - const buffer = new Uint8Array(await input.arrayBuffer()); - bufferPtr = zxingModule._malloc(size); - zxingModule.HEAPU8.set(buffer, bufferPtr); - zxingReadResultVector = zxingModule.readBarcodesFromImage( - bufferPtr, - size, - readerOptionsToZXingReaderOptions(requiredReaderOptions), - ); - } else { + + if ("width" in input && "height" in input && "data" in input) { /* ImageData */ const { data: buffer, @@ -310,6 +326,28 @@ export async function readBarcodesWithFactory( height, readerOptionsToZXingReaderOptions(requiredReaderOptions), ); + } else { + let size: number; + let buffer: Uint8Array; + if ("buffer" in input) { + /* Uint8Array */ + [size, buffer] = [input.byteLength, input]; + } else if ("byteLength" in input) { + /* ArrayBuffer */ + [size, buffer] = [input.byteLength, new Uint8Array(input)]; + } else if ("size" in input) { + /* Blob */ + [size, buffer] = [input.size, new Uint8Array(await input.arrayBuffer())]; + } else { + throw new TypeError("Invalid input type"); + } + bufferPtr = zxingModule._malloc(size); + zxingModule.HEAPU8.set(buffer, bufferPtr); + zxingReadResultVector = zxingModule.readBarcodesFromImage( + bufferPtr, + size, + readerOptionsToZXingReaderOptions(requiredReaderOptions), + ); } zxingModule._free(bufferPtr); const readResults: ReadResult[] = []; @@ -364,3 +402,46 @@ export async function writeBarcodeWithFactory( zxingModule._free(bufferPtr); return zxingWriteResultToWriteResult(zxingWriteResult); } + +if (import.meta.env.MODE === "miniprogram") { + /* A bare minimum Blob polyfill */ + globalThis.Blob ??= class { + #blobParts; + #options; + #zeroUint8Array = new Uint8Array(); + constructor(blobParts?: BlobPart[], options?: BlobPropertyBag) { + console.error( + "For the sake of robustness, a properly implemented Blob polyfill is required.", + ); + this.#blobParts = blobParts as Uint8Array[]; + this.#options = options; + } + get size() { + return this.#blobParts?.[0]?.byteLength ?? 0; + } + get type() { + return this.#options?.type ?? ""; + } + async arrayBuffer() { + return ( + (this.#blobParts?.[0]?.buffer as ArrayBuffer) ?? + this.#zeroUint8Array.buffer + ); + } + async bytes() { + return this.#blobParts?.[0] ?? this.#zeroUint8Array; + } + slice(): ReturnType { + throw new Error("Not implemented"); + } + stream(): ReturnType { + throw new Error("Not implemented"); + } + text(): ReturnType { + throw new Error("Not implemented"); + } + get [Symbol.toStringTag]() { + return "Blob"; + } + }; +} diff --git a/tests/samples/qrcode/wikipedia.png b/tests/samples/qrcode/wikipedia.png new file mode 100644 index 00000000..6b3a5193 Binary files /dev/null and b/tests/samples/qrcode/wikipedia.png differ diff --git a/tests/unit.test.ts b/tests/unit.test.ts index 3d622af9..4cdb7738 100644 --- a/tests/unit.test.ts +++ b/tests/unit.test.ts @@ -1,9 +1,16 @@ -import { afterAll, describe, expect, test, vi } from "vitest"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createCanvas, loadImage } from "@napi-rs/canvas"; +import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; import { prepareZXingModule as prepareZXingFullModule, purgeZXingModule as purgeZXingFullModule, } from "../src/full/index.js"; -import { prepareZXingModule as prepareZXingReaderModule } from "../src/reader/index.js"; +import { + prepareZXingModule as prepareZXingReaderModule, + readBarcodes, +} from "../src/reader/index.js"; describe("prepare zxing module", () => { const consoleMock = vi.spyOn(console, "error").mockImplementation(() => {}); @@ -178,3 +185,62 @@ describe("prepare zxing module", () => { expect(modulePromise2).toBe(modulePromise3); }); }); + +describe("readBarcodes input", async () => { + const arrayBuffer = await readFile( + fileURLToPath(new URL("./samples/qrcode/wikipedia.png", import.meta.url)), + ); + + beforeAll(async () => { + await prepareZXingReaderModule({ + overrides: { + wasmBinary: ( + await readFile( + resolve(import.meta.dirname, "../src/reader/zxing_reader.wasm"), + ) + ).buffer as ArrayBuffer, + }, + fireImmediately: true, + }); + }); + + test("readBarcodes accepts ArrayBuffer as input", async () => { + const readResult = await readBarcodes(arrayBuffer); + expect(readResult).length(1); + expect(readResult[0].text).toBe("http://en.m.wikipedia.org"); + }); + + test("readBarcodes accepts Blob as input", async () => { + const blob = new Blob([arrayBuffer], { type: "image/png" }); + const readResult = await readBarcodes(blob); + expect(readResult).length(1); + expect(readResult[0].text).toBe("http://en.m.wikipedia.org"); + }); + + test("readBarcodes accepts File as input", async () => { + const file = new File([arrayBuffer], "wikipedia.png", { + type: "image/png", + }); + const readResult = await readBarcodes(file); + expect(readResult).length(1); + expect(readResult[0].text).toBe("http://en.m.wikipedia.org"); + }); + + test("readBarcodes accepts Uint8Array as input", async () => { + const uint8Array = new Uint8Array(arrayBuffer); + const readResult = await readBarcodes(uint8Array); + expect(readResult).length(1); + expect(readResult[0].text).toBe("http://en.m.wikipedia.org"); + }); + + test("readBarcodes accepts ImageData as input", async () => { + const image = await loadImage(arrayBuffer); + const canvas = createCanvas(image.width, image.height); + const context = canvas.getContext("2d"); + context.drawImage(image, 0, 0, image.width, image.height); + const imageData = context.getImageData(0, 0, image.width, image.height); + const readResult = await readBarcodes(imageData); + expect(readResult).length(1); + expect(readResult[0].text).toBe("http://en.m.wikipedia.org"); + }); +});