@hxa-rn/rn-qr-generator 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/commonjs/index.js +81 -0
- package/dist/commonjs/index.js.map +1 -0
- package/dist/commonjs/specs/v1/NativeRNQrGenerator.js +9 -0
- package/dist/commonjs/specs/v1/NativeRNQrGenerator.js.map +1 -0
- package/dist/module/index.js +75 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/specs/v1/NativeRNQrGenerator.js +3 -0
- package/dist/module/specs/v1/NativeRNQrGenerator.js.map +1 -0
- package/dist/typescript/index.d.ts +39 -0
- package/dist/typescript/specs/v1/NativeRNQrGenerator.d.ts +40 -0
- package/harmony/qr_generator/LICENSE +21 -0
- package/harmony/qr_generator/NOTICE +33 -0
- package/harmony/qr_generator/OAT.xml +38 -0
- package/harmony/qr_generator/build-profile.json5 +16 -0
- package/harmony/qr_generator/hvigorfile.ts +2 -0
- package/harmony/qr_generator/index.ets +1 -0
- package/harmony/qr_generator/oh-package.json5 +15 -0
- package/harmony/qr_generator/src/main/cpp/CMakeLists.txt +15 -0
- package/harmony/qr_generator/src/main/cpp/QrGeneratorPackage.h +13 -0
- package/harmony/qr_generator/src/main/cpp/generated/RNOH/generated/BaseQrGeneratorPackage.h +72 -0
- package/harmony/qr_generator/src/main/cpp/generated/RNOH/generated/dummy.cpp +2 -0
- package/harmony/qr_generator/src/main/cpp/generated/RNOH/generated/turbo_modules/RNQrGenerator.cpp +17 -0
- package/harmony/qr_generator/src/main/cpp/generated/RNOH/generated/turbo_modules/RNQrGenerator.h +16 -0
- package/harmony/qr_generator/src/main/cpp/generated/react/renderer/components/qr_generator/ComponentDescriptors.h +24 -0
- package/harmony/qr_generator/src/main/cpp/generated/react/renderer/components/qr_generator/EventEmitters.cpp +16 -0
- package/harmony/qr_generator/src/main/cpp/generated/react/renderer/components/qr_generator/EventEmitters.h +17 -0
- package/harmony/qr_generator/src/main/cpp/generated/react/renderer/components/qr_generator/Props.cpp +19 -0
- package/harmony/qr_generator/src/main/cpp/generated/react/renderer/components/qr_generator/Props.h +18 -0
- package/harmony/qr_generator/src/main/cpp/generated/react/renderer/components/qr_generator/ShadowNodes.cpp +17 -0
- package/harmony/qr_generator/src/main/cpp/generated/react/renderer/components/qr_generator/ShadowNodes.h +23 -0
- package/harmony/qr_generator/src/main/cpp/generated/react/renderer/components/qr_generator/States.cpp +16 -0
- package/harmony/qr_generator/src/main/cpp/generated/react/renderer/components/qr_generator/States.h +19 -0
- package/harmony/qr_generator/src/main/ets/DetectScanWorker.ets +6 -0
- package/harmony/qr_generator/src/main/ets/QrGeneratorTurboModulesFactory.ets +18 -0
- package/harmony/qr_generator/src/main/ets/RNQrGeneratorTurboModule.ets +75 -0
- package/harmony/qr_generator/src/main/ets/ZxingImageHelper.ets +1449 -0
- package/harmony/qr_generator/src/main/ets/generated/index.ets +5 -0
- package/harmony/qr_generator/src/main/ets/generated/ts.ts +5 -0
- package/harmony/qr_generator/src/main/ets/generated/turboModules/RNQrGenerator.ts +26 -0
- package/harmony/qr_generator/src/main/ets/generated/turboModules/ts.ts +5 -0
- package/harmony/qr_generator/src/main/module.json5 +13 -0
- package/harmony/qr_generator/src/main/resources/base/element/string.json +8 -0
- package/harmony/qr_generator/src/main/resources/en_US/element/string.json +8 -0
- package/harmony/qr_generator/src/main/resources/zh_CN/element/string.json +8 -0
- package/harmony/qr_generator.har +0 -0
- package/package.json +72 -0
- package/src/index.ts +151 -0
- package/src/specs/v1/.gitkeep +1 -0
- package/src/specs/v1/NativeRNQrGenerator.ts +55 -0
- package/src/specs/v2/.gitkeep +1 -0
|
@@ -0,0 +1,1449 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* RNQrGenerator image / ZXing helper for HarmonyOS.
|
|
3
|
+
*
|
|
4
|
+
* Implements the same behavior as the Android RNQrGeneratorModule:
|
|
5
|
+
* - generate: MultiFormatWriter.encode (ERROR_CORRECTION / CHARACTER_SET / MARGIN hints)
|
|
6
|
+
* -> BitMatrix -> RGBA_8888 buffer -> PixelMap -> PNG bytes -> cacheDir file -> sandbox URI
|
|
7
|
+
* - detect: uri/base64 -> PixelMap -> RGBA -> luminance ((R+2G+B)/4)
|
|
8
|
+
* -> RGBLuminanceSource + HybridBinarizer + MultiFormatReader decode,
|
|
9
|
+
* with Android-aligned multi-round retry:
|
|
10
|
+
* original -> inverted -> 3 relative crops -> 1.2/1.5/2.0 zoom.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { MultiFormatWriter, BarcodeFormat, EncodeHintType, QRCodeDecoderErrorCorrectionLevel,
|
|
14
|
+
MultiFormatReader, DecodeHintType, BinaryBitmap, HybridBinarizer, GlobalHistogramBinarizer,
|
|
15
|
+
RGBLuminanceSource, InvertedLuminanceSource, Result, ResultMetadataType, ZXingStringEncoding,
|
|
16
|
+
LuminanceSource, DataMatrixReader, PDF417Reader, Reader } from '@ohos/zxing';
|
|
17
|
+
import { image } from '@kit.ImageKit';
|
|
18
|
+
import { fileIo as fs, fileUri } from '@kit.CoreFileKit';
|
|
19
|
+
import { util } from '@kit.ArkTS';
|
|
20
|
+
import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
21
|
+
import { TM } from './generated/ts';
|
|
22
|
+
|
|
23
|
+
const TAG: string = 'RNQrGenerator';
|
|
24
|
+
const DOMAIN: number = 0x0000;
|
|
25
|
+
const MAX_SCAN_DIMENSION: number = 1280;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 16 formats for general multi-format scan. MaxiCode is excluded because its reader
|
|
29
|
+
* assumes a pure barcode image and can abort the process when invoked from crop/zoom
|
|
30
|
+
* retries or inverted passes on album photos.
|
|
31
|
+
*/
|
|
32
|
+
const STANDARD_DECODE_FORMATS: BarcodeFormat[] = [
|
|
33
|
+
BarcodeFormat.AZTEC,
|
|
34
|
+
BarcodeFormat.CODABAR,
|
|
35
|
+
BarcodeFormat.CODE_39,
|
|
36
|
+
BarcodeFormat.CODE_93,
|
|
37
|
+
BarcodeFormat.CODE_128,
|
|
38
|
+
BarcodeFormat.DATA_MATRIX,
|
|
39
|
+
BarcodeFormat.EAN_8,
|
|
40
|
+
BarcodeFormat.EAN_13,
|
|
41
|
+
BarcodeFormat.ITF,
|
|
42
|
+
BarcodeFormat.PDF_417,
|
|
43
|
+
BarcodeFormat.QR_CODE,
|
|
44
|
+
BarcodeFormat.RSS_14,
|
|
45
|
+
BarcodeFormat.RSS_EXPANDED,
|
|
46
|
+
BarcodeFormat.UPC_A,
|
|
47
|
+
BarcodeFormat.UPC_E,
|
|
48
|
+
BarcodeFormat.UPC_EAN_EXTENSION,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const MAXICODE_DECODE_FORMATS: BarcodeFormat[] = [BarcodeFormat.MAXICODE];
|
|
52
|
+
|
|
53
|
+
/** 2D matrix symbologies that MultiFormatReader may miss on @ohos/zxing; retried with dedicated readers. */
|
|
54
|
+
const MATRIX_SYMBOLOGY_READERS: Reader[] = [
|
|
55
|
+
new DataMatrixReader(),
|
|
56
|
+
new PDF417Reader(),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** Result of generate(), mirrors JS GenerateResult. */
|
|
60
|
+
export interface GenerateResultData {
|
|
61
|
+
uri: string;
|
|
62
|
+
width: number;
|
|
63
|
+
height: number;
|
|
64
|
+
base64?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Result of detect(), mirrors JS DetectResult. */
|
|
68
|
+
export interface DetectResultData {
|
|
69
|
+
values: string[];
|
|
70
|
+
type: string;
|
|
71
|
+
types?: string[];
|
|
72
|
+
correctionLevel?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Plain scan payload returned from the taskpool worker. */
|
|
76
|
+
export interface DetectScanPayload {
|
|
77
|
+
values: string[];
|
|
78
|
+
type: string;
|
|
79
|
+
types?: string[];
|
|
80
|
+
correctionLevel?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface DetectScanInput {
|
|
84
|
+
lum: Uint8ClampedArray;
|
|
85
|
+
w: number;
|
|
86
|
+
h: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface LuminanceData {
|
|
90
|
+
lum: Uint8ClampedArray;
|
|
91
|
+
w: number;
|
|
92
|
+
h: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface ImageSize {
|
|
96
|
+
width: number;
|
|
97
|
+
height: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface PaddingInsets {
|
|
101
|
+
top: number;
|
|
102
|
+
left: number;
|
|
103
|
+
bottom: number;
|
|
104
|
+
right: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface ScanRegion {
|
|
108
|
+
x: number;
|
|
109
|
+
y: number;
|
|
110
|
+
width: number;
|
|
111
|
+
height: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface ScanRetryOptions {
|
|
115
|
+
/** Full-image scanBasic already ran in scanAllResults. */
|
|
116
|
+
skipInitialBasic?: boolean;
|
|
117
|
+
/** Use DataMatrix/PDF417 dedicated readers during crop/zoom retries. */
|
|
118
|
+
dedicatedMatrixReaders?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Pixel budget above which zoom retries are skipped to avoid OOM on album photos. */
|
|
122
|
+
const MAX_RETRY_ZOOM_PIXELS: number = 512 * 512;
|
|
123
|
+
/** Pixel budget above which crop retries are reduced to a single pass. */
|
|
124
|
+
const MAX_RETRY_CROP_PIXELS: number = 1024 * 1024;
|
|
125
|
+
|
|
126
|
+
export class ZxingImageHelper {
|
|
127
|
+
/**
|
|
128
|
+
* Generate a QR code PNG and write it to the cache directory.
|
|
129
|
+
* Resolves with { uri, width, height, base64? }; rejects with error message string.
|
|
130
|
+
*/
|
|
131
|
+
static generate(options: TM.RNQrGenerator.GenerateOptions, cacheDir: string): Promise<GenerateResultData> {
|
|
132
|
+
return new Promise<GenerateResultData>((resolve, reject) => {
|
|
133
|
+
try {
|
|
134
|
+
ZxingImageHelper.ensureSafeStringCodec();
|
|
135
|
+
const value: string = options.value ?? '';
|
|
136
|
+
const fileName: string = options.fileName ?? '';
|
|
137
|
+
const correctionLevel: string = options.correctionLevel ?? 'H';
|
|
138
|
+
const width: number = options.width ?? 100;
|
|
139
|
+
const height: number = options.height ?? 100;
|
|
140
|
+
const backgroundColor: number = options.backgroundColor ?? 0xFFFFFFFF;
|
|
141
|
+
const color: number = options.color ?? 0xFF000000;
|
|
142
|
+
const base64: boolean = options.base64 ?? false;
|
|
143
|
+
|
|
144
|
+
if (value.length === 0) {
|
|
145
|
+
reject('Property "value" is missing');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const insets = ZxingImageHelper.parsePaddingFromOptions(options);
|
|
150
|
+
const top: number = insets.top;
|
|
151
|
+
const left: number = insets.left;
|
|
152
|
+
const bottom: number = insets.bottom;
|
|
153
|
+
const right: number = insets.right;
|
|
154
|
+
|
|
155
|
+
const hasPadding: boolean = top !== 0 || left !== 0 || bottom !== 0 || right !== 0;
|
|
156
|
+
// Android-aligned: subtract padding from encode dimensions first, then expand the
|
|
157
|
+
// output canvas to matrixSize + padding (not a fixed width×height clip rect).
|
|
158
|
+
const encodeW: number = width - left - right;
|
|
159
|
+
const encodeH: number = height - top - bottom;
|
|
160
|
+
if (hasPadding && (encodeW < 1 || encodeH < 1)) {
|
|
161
|
+
reject('padding exceeds image dimensions');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const qrWidth: number = hasPadding ? encodeW : width;
|
|
166
|
+
const qrHeight: number = hasPadding ? encodeH : height;
|
|
167
|
+
|
|
168
|
+
// Build encode hints, aligned with Android generateQrCode (MARGIN=0, padding handled separately).
|
|
169
|
+
const hints = new Map<EncodeHintType, Object>();
|
|
170
|
+
hints.set(EncodeHintType.ERROR_CORRECTION, ZxingImageHelper.parseCorrectionLevel(correctionLevel));
|
|
171
|
+
hints.set(EncodeHintType.CHARACTER_SET, 'UTF-8');
|
|
172
|
+
hints.set(EncodeHintType.MARGIN, 0);
|
|
173
|
+
|
|
174
|
+
const writer: MultiFormatWriter = new MultiFormatWriter();
|
|
175
|
+
const matrix = writer.encode(value, BarcodeFormat.QR_CODE, qrWidth, qrHeight, hints);
|
|
176
|
+
const matrixW: number = matrix.getWidth();
|
|
177
|
+
const matrixH: number = matrix.getHeight();
|
|
178
|
+
|
|
179
|
+
// Fill the QR pixel buffer (RGBA_8888 byte order R,G,B,A) from the BitMatrix.
|
|
180
|
+
const qrBuf: ArrayBuffer = new ArrayBuffer(matrixW * matrixH * 4);
|
|
181
|
+
const qrArr: Uint8ClampedArray = new Uint8ClampedArray(qrBuf);
|
|
182
|
+
for (let y = 0; y < matrixH; y++) {
|
|
183
|
+
for (let x = 0; x < matrixW; x++) {
|
|
184
|
+
const on: boolean = matrix.get(x, y);
|
|
185
|
+
const c: number = on ? color : backgroundColor;
|
|
186
|
+
const idx: number = (y * matrixW + x) * 4;
|
|
187
|
+
qrArr[idx] = (c >> 16) & 0xFF;
|
|
188
|
+
qrArr[idx + 1] = (c >> 8) & 0xFF;
|
|
189
|
+
qrArr[idx + 2] = c & 0xFF;
|
|
190
|
+
const a: number = (c >> 24) & 0xFF;
|
|
191
|
+
qrArr[idx + 3] = a === 0 ? 0xFF : a;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// When padding is non-zero, expand the canvas to fit the encoded QR plus insets,
|
|
196
|
+
// fill with background, and draw the QR bitmap at (left, top). Matches Android.
|
|
197
|
+
let finalW: number = matrixW;
|
|
198
|
+
let finalH: number = matrixH;
|
|
199
|
+
let finalBuffer: ArrayBuffer = qrBuf;
|
|
200
|
+
if (hasPadding) {
|
|
201
|
+
finalW = matrixW + left + right;
|
|
202
|
+
finalH = matrixH + top + bottom;
|
|
203
|
+
const outBuf: ArrayBuffer = new ArrayBuffer(finalW * finalH * 4);
|
|
204
|
+
const outArr: Uint8ClampedArray = new Uint8ClampedArray(outBuf);
|
|
205
|
+
for (let i = 0; i < finalW * finalH; i++) {
|
|
206
|
+
const idx: number = i * 4;
|
|
207
|
+
outArr[idx] = (backgroundColor >> 16) & 0xFF;
|
|
208
|
+
outArr[idx + 1] = (backgroundColor >> 8) & 0xFF;
|
|
209
|
+
outArr[idx + 2] = backgroundColor & 0xFF;
|
|
210
|
+
const a: number = (backgroundColor >> 24) & 0xFF;
|
|
211
|
+
outArr[idx + 3] = a === 0 ? 0xFF : a;
|
|
212
|
+
}
|
|
213
|
+
for (let y = 0; y < matrixH; y++) {
|
|
214
|
+
for (let x = 0; x < matrixW; x++) {
|
|
215
|
+
const srcIdx: number = (y * matrixW + x) * 4;
|
|
216
|
+
const dstIdx: number = ((y + top) * finalW + (x + left)) * 4;
|
|
217
|
+
outArr[dstIdx] = qrArr[srcIdx];
|
|
218
|
+
outArr[dstIdx + 1] = qrArr[srcIdx + 1];
|
|
219
|
+
outArr[dstIdx + 2] = qrArr[srcIdx + 2];
|
|
220
|
+
outArr[dstIdx + 3] = qrArr[srcIdx + 3];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
finalBuffer = outBuf;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const initOptions: image.InitializationOptions = {
|
|
227
|
+
size: { width: finalW, height: finalH },
|
|
228
|
+
srcPixelFormat: image.PixelMapFormat.RGBA_8888,
|
|
229
|
+
pixelFormat: image.PixelMapFormat.RGBA_8888,
|
|
230
|
+
editable: true,
|
|
231
|
+
};
|
|
232
|
+
const pixelMap: image.PixelMap = image.createPixelMapSync(finalBuffer, initOptions);
|
|
233
|
+
|
|
234
|
+
const packer: image.ImagePacker = image.createImagePacker();
|
|
235
|
+
const packOptions: image.PackingOption = { format: 'image/png', quality: 100 };
|
|
236
|
+
packer.packToData(pixelMap, packOptions).then((pngBytes: ArrayBuffer) => {
|
|
237
|
+
try {
|
|
238
|
+
const name: string = fileName.length > 0 ? fileName : util.generateRandomUUID();
|
|
239
|
+
const nameWithExt: string = name.endsWith('.png') ? name : name + '.png';
|
|
240
|
+
const filePath: string = cacheDir + '/' + nameWithExt;
|
|
241
|
+
let file: fs.File | undefined;
|
|
242
|
+
try {
|
|
243
|
+
const openMode =
|
|
244
|
+
fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE | fs.OpenMode.TRUNC;
|
|
245
|
+
file = fs.openSync(filePath, openMode);
|
|
246
|
+
fs.writeSync(file.fd, pngBytes);
|
|
247
|
+
} finally {
|
|
248
|
+
if (file !== undefined) {
|
|
249
|
+
fs.closeSync(file);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const result: GenerateResultData = {
|
|
254
|
+
uri: fileUri.getUriFromPath(filePath),
|
|
255
|
+
width: finalW,
|
|
256
|
+
height: finalH,
|
|
257
|
+
};
|
|
258
|
+
if (base64) {
|
|
259
|
+
const b64Helper: util.Base64Helper = new util.Base64Helper();
|
|
260
|
+
result.base64 = b64Helper.encodeToStringSync(new Uint8Array(pngBytes));
|
|
261
|
+
}
|
|
262
|
+
resolve(result);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
const err: Error = e as Error;
|
|
265
|
+
hilog.error(DOMAIN, TAG, 'generate write failed: %{public}s', ZxingImageHelper.errMsg(err));
|
|
266
|
+
reject(err.message);
|
|
267
|
+
}
|
|
268
|
+
}).catch((err: Object) => {
|
|
269
|
+
const e: Error = err as Error;
|
|
270
|
+
hilog.error(DOMAIN, TAG, 'generate packToData failed: %{public}s', ZxingImageHelper.errMsg(e));
|
|
271
|
+
reject(e.message);
|
|
272
|
+
});
|
|
273
|
+
} catch (e) {
|
|
274
|
+
const err: Error = e as Error;
|
|
275
|
+
hilog.error(DOMAIN, TAG, 'generate failed: %{public}s', ZxingImageHelper.errMsg(err));
|
|
276
|
+
reject(err.message);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Detect/decode a barcode from a local image uri or base64 string.
|
|
283
|
+
* Resolves with { values: string[], type: string }; on image load failure rejects 'IMAGE_NOT_FOUND'.
|
|
284
|
+
*/
|
|
285
|
+
static detect(options: TM.RNQrGenerator.DetectOptions): Promise<DetectResultData> {
|
|
286
|
+
return ZxingImageHelper.prepareDetectInput(options).then((input: DetectScanInput | null) => {
|
|
287
|
+
if (input === null) {
|
|
288
|
+
const emptyResult: DetectResultData = { values: [], type: '' };
|
|
289
|
+
return emptyResult;
|
|
290
|
+
}
|
|
291
|
+
return ZxingImageHelper.buildDetectPayload(
|
|
292
|
+
ZxingImageHelper.scanAllResultsInternal(input.lum, input.w, input.h));
|
|
293
|
+
}).catch((err: Object) => {
|
|
294
|
+
const msg: string = ZxingImageHelper.errMsg(err as Error);
|
|
295
|
+
if (msg.includes('IMAGE_NOT_FOUND') || msg.includes('No such file') || msg.includes('not found')) {
|
|
296
|
+
throw new Error(msg);
|
|
297
|
+
}
|
|
298
|
+
hilog.error(DOMAIN, TAG, 'detect failed: %{public}s', msg);
|
|
299
|
+
const emptyResult: DetectResultData = { values: [], type: '' };
|
|
300
|
+
return emptyResult;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
static prepareDetectInput(options: TM.RNQrGenerator.DetectOptions): Promise<DetectScanInput | null> {
|
|
305
|
+
return new Promise<DetectScanInput | null>((resolve, reject) => {
|
|
306
|
+
const uri: string = options.uri ?? '';
|
|
307
|
+
const base64: string = options.base64 ?? '';
|
|
308
|
+
if (uri.length === 0 && base64.length === 0) {
|
|
309
|
+
reject('IMAGE_NOT_FOUND');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let pixelMap: image.PixelMap | undefined = undefined;
|
|
314
|
+
try {
|
|
315
|
+
pixelMap = ZxingImageHelper.loadPixelMap(uri, base64);
|
|
316
|
+
pixelMap = ZxingImageHelper.preparePixelMapForScan(pixelMap);
|
|
317
|
+
const extracted: LuminanceData = ZxingImageHelper.extractLuminance(pixelMap);
|
|
318
|
+
resolve({
|
|
319
|
+
lum: new Uint8ClampedArray(extracted.lum),
|
|
320
|
+
w: extracted.w,
|
|
321
|
+
h: extracted.h,
|
|
322
|
+
});
|
|
323
|
+
} catch (e) {
|
|
324
|
+
if (uri.length > 0) {
|
|
325
|
+
reject('IMAGE_NOT_FOUND');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
reject(e as Error);
|
|
329
|
+
} finally {
|
|
330
|
+
if (pixelMap !== undefined) {
|
|
331
|
+
try {
|
|
332
|
+
pixelMap.release();
|
|
333
|
+
} catch (releaseErr) {
|
|
334
|
+
hilog.warn(
|
|
335
|
+
DOMAIN,
|
|
336
|
+
TAG,
|
|
337
|
+
'detect release pixelMap failed: %{public}s',
|
|
338
|
+
ZxingImageHelper.errMsg(releaseErr as Error)
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Entry for taskpool worker; keeps heavy ZXing work off the UI thread. */
|
|
347
|
+
static scanForDetectWorker(lum: Uint8ClampedArray, w: number, h: number): DetectScanPayload {
|
|
348
|
+
ZxingImageHelper.ensureSafeStringCodec();
|
|
349
|
+
return ZxingImageHelper.buildDetectPayload(ZxingImageHelper.scanAllResultsInternal(lum, w, h));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private static scanAllResultsInternal(lum: Uint8ClampedArray, w: number, h: number): Result[] {
|
|
353
|
+
try {
|
|
354
|
+
return ZxingImageHelper.scanAllResults(lum, w, h);
|
|
355
|
+
} catch (scanErr) {
|
|
356
|
+
hilog.error(DOMAIN, TAG, 'scanAllResults failed: %{public}s', ZxingImageHelper.errMsg(scanErr as Error));
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private static buildDetectPayload(results: Result[]): DetectScanPayload {
|
|
362
|
+
if (results.length === 0) {
|
|
363
|
+
return { values: [], type: '' };
|
|
364
|
+
}
|
|
365
|
+
const values: string[] = [];
|
|
366
|
+
const types: string[] = [];
|
|
367
|
+
for (let i = 0; i < results.length; i++) {
|
|
368
|
+
try {
|
|
369
|
+
values.push(ZxingImageHelper.copyDecodedText(results[i].getText()));
|
|
370
|
+
types.push(ZxingImageHelper.getCodeType(results[i].getBarcodeFormat()));
|
|
371
|
+
} catch (textErr) {
|
|
372
|
+
hilog.warn(DOMAIN, TAG, 'getText failed: %{public}s', ZxingImageHelper.errMsg(textErr as Error));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (values.length === 0) {
|
|
376
|
+
return { values: [], type: '' };
|
|
377
|
+
}
|
|
378
|
+
const payload: DetectScanPayload = {
|
|
379
|
+
values: values,
|
|
380
|
+
type: types[0],
|
|
381
|
+
types: types,
|
|
382
|
+
};
|
|
383
|
+
const correctionLevel: string | undefined = ZxingImageHelper.extractCorrectionLevel(results[0]);
|
|
384
|
+
if (correctionLevel !== undefined) {
|
|
385
|
+
payload.correctionLevel = correctionLevel;
|
|
386
|
+
}
|
|
387
|
+
return payload;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Read RGBA from PixelMap and compute luminance; honors row stride when present. */
|
|
391
|
+
private static extractLuminance(pixelMap: image.PixelMap): LuminanceData {
|
|
392
|
+
const info: image.ImageInfo = pixelMap.getImageInfoSync();
|
|
393
|
+
const w: number = info.size.width;
|
|
394
|
+
const h: number = info.size.height;
|
|
395
|
+
const bufferSize: number = pixelMap.getPixelBytesNumber();
|
|
396
|
+
const rowBytes: number = pixelMap.getBytesNumberPerRow();
|
|
397
|
+
const buf: ArrayBuffer = new ArrayBuffer(bufferSize);
|
|
398
|
+
pixelMap.readPixelsToBufferSync(buf);
|
|
399
|
+
const rgba: Uint8ClampedArray = new Uint8ClampedArray(buf);
|
|
400
|
+
const lum: Uint8ClampedArray = new Uint8ClampedArray(w * h);
|
|
401
|
+
for (let y = 0; y < h; y++) {
|
|
402
|
+
const rowBase: number = y * rowBytes;
|
|
403
|
+
for (let x = 0; x < w; x++) {
|
|
404
|
+
const j: number = rowBase + x * 4;
|
|
405
|
+
const alpha: number = rgba[j + 3];
|
|
406
|
+
if (alpha < 16) {
|
|
407
|
+
// PNG transparency must not become black bars that break MaxiCode pure-bit extraction.
|
|
408
|
+
lum[y * w + x] = 255;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (alpha < 255) {
|
|
412
|
+
const scale: number = alpha / 255;
|
|
413
|
+
const r: number = 255 * (1 - scale) + rgba[j] * scale;
|
|
414
|
+
const g: number = 255 * (1 - scale) + rgba[j + 1] * scale;
|
|
415
|
+
const b: number = 255 * (1 - scale) + rgba[j + 2] * scale;
|
|
416
|
+
lum[y * w + x] = (r + g * 2 + b) / 4;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
lum[y * w + x] = (rgba[j] + rgba[j + 1] * 2 + rgba[j + 2]) / 4;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { lum: lum, w: w, h: h };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Downscale very large images before decode to avoid UI freeze/OOM on album photos. */
|
|
426
|
+
private static preparePixelMapForScan(pixelMap: image.PixelMap): image.PixelMap {
|
|
427
|
+
const info: image.ImageInfo = pixelMap.getImageInfoSync();
|
|
428
|
+
const w: number = info.size.width;
|
|
429
|
+
const h: number = info.size.height;
|
|
430
|
+
const maxSide: number = Math.max(w, h);
|
|
431
|
+
if (maxSide <= MAX_SCAN_DIMENSION) {
|
|
432
|
+
return pixelMap;
|
|
433
|
+
}
|
|
434
|
+
const scale: number = MAX_SCAN_DIMENSION / maxSide;
|
|
435
|
+
pixelMap.scaleSync(scale, scale);
|
|
436
|
+
return pixelMap;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private static computeDesiredSize(width: number, height: number): ImageSize {
|
|
440
|
+
const maxSide: number = Math.max(width, height);
|
|
441
|
+
if (maxSide <= MAX_SCAN_DIMENSION) {
|
|
442
|
+
return { width: width, height: height };
|
|
443
|
+
}
|
|
444
|
+
const scale: number = MAX_SCAN_DIMENSION / maxSide;
|
|
445
|
+
return {
|
|
446
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
447
|
+
height: Math.max(1, Math.round(height * scale)),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private static buildDecodingOptions(sourceSize?: ImageSize): image.DecodingOptions {
|
|
452
|
+
const options: image.DecodingOptions = {
|
|
453
|
+
editable: true,
|
|
454
|
+
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
|
|
455
|
+
};
|
|
456
|
+
if (sourceSize !== undefined) {
|
|
457
|
+
options.desiredSize = ZxingImageHelper.computeDesiredSize(sourceSize.width, sourceSize.height);
|
|
458
|
+
}
|
|
459
|
+
return options;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private static readSourceSize(source: image.ImageSource): ImageSize | undefined {
|
|
463
|
+
try {
|
|
464
|
+
const info: image.ImageInfo = source.getImageInfoSync();
|
|
465
|
+
return { width: info.size.width, height: info.size.height };
|
|
466
|
+
} catch (e) {
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private static loadPixelMap(uri: string, base64: string): image.PixelMap {
|
|
472
|
+
if (uri.length > 0) {
|
|
473
|
+
const path: string = ZxingImageHelper.normalizeSourcePath(uri);
|
|
474
|
+
return ZxingImageHelper.loadPixelMapFromPath(path);
|
|
475
|
+
}
|
|
476
|
+
const b64: string = base64.includes(',') ? base64.substring(base64.indexOf(',') + 1) : base64;
|
|
477
|
+
try {
|
|
478
|
+
const b64Helper: util.Base64Helper = new util.Base64Helper();
|
|
479
|
+
const bytes: Uint8Array = b64Helper.decodeSync(b64);
|
|
480
|
+
const copyBuf: ArrayBuffer = new ArrayBuffer(bytes.byteLength);
|
|
481
|
+
new Uint8Array(copyBuf).set(bytes);
|
|
482
|
+
const source: image.ImageSource = image.createImageSource(copyBuf);
|
|
483
|
+
try {
|
|
484
|
+
const sourceSize = ZxingImageHelper.readSourceSize(source);
|
|
485
|
+
return source.createPixelMapSync(ZxingImageHelper.buildDecodingOptions(sourceSize));
|
|
486
|
+
} finally {
|
|
487
|
+
try {
|
|
488
|
+
source.release();
|
|
489
|
+
} catch (releaseErr) {
|
|
490
|
+
hilog.warn(
|
|
491
|
+
DOMAIN,
|
|
492
|
+
TAG,
|
|
493
|
+
'loadPixelMap base64 release failed: %{public}s',
|
|
494
|
+
ZxingImageHelper.errMsg(releaseErr as Error)
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} catch (e) {
|
|
499
|
+
const err3: Error = e as Error;
|
|
500
|
+
hilog.error(DOMAIN, TAG, 'loadPixelMap base64 decode failed: %{public}s', ZxingImageHelper.errMsg(err3));
|
|
501
|
+
throw err3;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Multi-round scan, aligned with Android scanBitmap:
|
|
507
|
+
* original -> inverted -> 3 relative crop configs -> 1.2/1.5/2.0 zoom on each crop.
|
|
508
|
+
* Returns the first successful Result, or null when nothing decodes.
|
|
509
|
+
*/
|
|
510
|
+
private static scanWithRetries(lum: Uint8ClampedArray, w: number, h: number,
|
|
511
|
+
options?: ScanRetryOptions): Result | null {
|
|
512
|
+
try {
|
|
513
|
+
const pixelCount: number = w * h;
|
|
514
|
+
const hints: Map<DecodeHintType, Object> =
|
|
515
|
+
ZxingImageHelper.createDecodeHints(pixelCount <= MAX_RETRY_ZOOM_PIXELS);
|
|
516
|
+
if (options?.skipInitialBasic !== true) {
|
|
517
|
+
const basicResult: Result | null = ZxingImageHelper.scanBasic(lum, w, h, hints);
|
|
518
|
+
if (basicResult !== null) {
|
|
519
|
+
return basicResult;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const cropConfigs: number[][] = pixelCount > MAX_RETRY_CROP_PIXELS
|
|
524
|
+
? [[0.15, 0.05, 0.35, 0.05]]
|
|
525
|
+
: [
|
|
526
|
+
[0.15, 0.05, 0.35, 0.05],
|
|
527
|
+
[0.25, 0.05, 0.25, 0.05],
|
|
528
|
+
[0.35, 0.05, 0.15, 0.05],
|
|
529
|
+
];
|
|
530
|
+
const zoomFactors: number[] = pixelCount > MAX_RETRY_ZOOM_PIXELS
|
|
531
|
+
? []
|
|
532
|
+
: pixelCount > MAX_RETRY_ZOOM_PIXELS / 2
|
|
533
|
+
? [1.2]
|
|
534
|
+
: [1.2, 1.5, 2.0];
|
|
535
|
+
|
|
536
|
+
for (let c = 0; c < cropConfigs.length; c++) {
|
|
537
|
+
const config: number[] = cropConfigs[c];
|
|
538
|
+
const topInset: number = config[0];
|
|
539
|
+
const leftInset: number = config[1];
|
|
540
|
+
const bottomInset: number = config[2];
|
|
541
|
+
const rightInset: number = config[3];
|
|
542
|
+
|
|
543
|
+
if (topInset + bottomInset >= 1.0 || leftInset + rightInset >= 1.0) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const left: number = Math.max(0, Math.round(leftInset * w));
|
|
547
|
+
const top: number = Math.max(0, Math.round(topInset * h));
|
|
548
|
+
const right: number = Math.max(0, Math.round(rightInset * w));
|
|
549
|
+
const bottom: number = Math.max(0, Math.round(bottomInset * h));
|
|
550
|
+
const cropW: number = w - left - right;
|
|
551
|
+
const cropH: number = h - top - bottom;
|
|
552
|
+
if (cropW <= 0 || cropH <= 0) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Crop via RGBLuminanceSource sub-region (no second image decode needed).
|
|
557
|
+
const croppedSource: RGBLuminanceSource = new RGBLuminanceSource(lum, cropW, cropH, w, h, left, top);
|
|
558
|
+
const croppedResult: Result | null = options?.dedicatedMatrixReaders === true
|
|
559
|
+
? ZxingImageHelper.decodeWithDedicatedMatrixReaders(croppedSource, hints)
|
|
560
|
+
: ZxingImageHelper.decodeOnSource(croppedSource, hints);
|
|
561
|
+
if (croppedResult !== null) {
|
|
562
|
+
return croppedResult;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
for (let z = 0; z < zoomFactors.length; z++) {
|
|
566
|
+
const zoom: number = zoomFactors[z];
|
|
567
|
+
if (zoom <= 1.0) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const zoomedSource: RGBLuminanceSource =
|
|
572
|
+
ZxingImageHelper.scaleLuminance(croppedSource.getMatrix(), cropW, cropH, zoom);
|
|
573
|
+
const zoomedResult: Result | null = options?.dedicatedMatrixReaders === true
|
|
574
|
+
? ZxingImageHelper.decodeWithDedicatedMatrixReaders(zoomedSource, hints)
|
|
575
|
+
: ZxingImageHelper.decodeOnSource(zoomedSource, hints);
|
|
576
|
+
if (zoomedResult !== null) {
|
|
577
|
+
return zoomedResult;
|
|
578
|
+
}
|
|
579
|
+
} catch (e) {
|
|
580
|
+
// continue with next zoom / crop
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return null;
|
|
586
|
+
} catch (retryErr) {
|
|
587
|
+
hilog.warn(DOMAIN, TAG, 'scanWithRetries failed: %{public}s', ZxingImageHelper.errMsg(retryErr as Error));
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Collect multiple barcodes with a bounded scan plan.
|
|
594
|
+
*
|
|
595
|
+
* A full-image scan runs first so normal single-code images return promptly.
|
|
596
|
+
* Lightweight QR-only region probes then collect additional codes for F02-005.
|
|
597
|
+
* Expensive crop/zoom retries run once on the full image only when neither
|
|
598
|
+
* basic path succeeds, preserving difficult single-code behavior without
|
|
599
|
+
* multiplying the heavy path across regions.
|
|
600
|
+
*/
|
|
601
|
+
private static scanAllResults(lum: Uint8ClampedArray, w: number, h: number): Result[] {
|
|
602
|
+
let maxiResult: Result | null = null;
|
|
603
|
+
try {
|
|
604
|
+
maxiResult = ZxingImageHelper.scanMaxiCode(lum, w, h);
|
|
605
|
+
} catch (maxiErr) {
|
|
606
|
+
hilog.warn(DOMAIN, TAG, 'scanMaxiCode failed: %{public}s', ZxingImageHelper.errMsg(maxiErr as Error));
|
|
607
|
+
}
|
|
608
|
+
if (maxiResult !== null) {
|
|
609
|
+
return [maxiResult];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const pixelCount: number = w * h;
|
|
613
|
+
const isWideAlbumLayout: boolean = w > h * 1.25;
|
|
614
|
+
const tryHarder: boolean = !isWideAlbumLayout && pixelCount <= MAX_RETRY_ZOOM_PIXELS;
|
|
615
|
+
const hints: Map<DecodeHintType, Object> = ZxingImageHelper.createDecodeHints(tryHarder);
|
|
616
|
+
const fullResult: Result | null =
|
|
617
|
+
ZxingImageHelper.scanBasic(lum, w, h, hints, !isWideAlbumLayout);
|
|
618
|
+
|
|
619
|
+
if (fullResult !== null && !ZxingImageHelper.shouldProbeForAdditionalCodes(w, h)) {
|
|
620
|
+
return [fullResult];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const regionHints: Map<DecodeHintType, Object> = ZxingImageHelper.createMultiRegionDecodeHints();
|
|
624
|
+
const multiRegions: ScanRegion[] = ZxingImageHelper.buildMultiQrRegions(w, h);
|
|
625
|
+
const regionalResults: Result[] =
|
|
626
|
+
ZxingImageHelper.scanRegions(lum, w, h, multiRegions, regionHints);
|
|
627
|
+
const mergedBasic: Result[] = ZxingImageHelper.mergeUniqueResults(
|
|
628
|
+
fullResult !== null ? [fullResult] : [],
|
|
629
|
+
regionalResults,
|
|
630
|
+
);
|
|
631
|
+
if (mergedBasic.length > 0) {
|
|
632
|
+
return mergedBasic;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (fullResult !== null) {
|
|
636
|
+
return [fullResult];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (isWideAlbumLayout) {
|
|
640
|
+
let wideFallback: Result | null = null;
|
|
641
|
+
try {
|
|
642
|
+
wideFallback = ZxingImageHelper.scanWideAlbumFallback(lum, w, h);
|
|
643
|
+
} catch (wideErr) {
|
|
644
|
+
hilog.warn(
|
|
645
|
+
DOMAIN,
|
|
646
|
+
TAG,
|
|
647
|
+
'scanWideAlbumFallback failed: %{public}s',
|
|
648
|
+
ZxingImageHelper.errMsg(wideErr as Error)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (wideFallback !== null) {
|
|
652
|
+
return [wideFallback];
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const enhancedResult: Result | null =
|
|
657
|
+
ZxingImageHelper.scanWithRetries(lum, w, h, { skipInitialBasic: true });
|
|
658
|
+
if (enhancedResult !== null) {
|
|
659
|
+
return [enhancedResult];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const matrixFallback: Result | null = ZxingImageHelper.scanMatrixSymbologyFallback(lum, w, h);
|
|
663
|
+
return matrixFallback === null ? [] : [matrixFallback];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Aspect ratio suggests side-by-side or stacked multiple codes (F02-005). */
|
|
667
|
+
private static shouldProbeForAdditionalCodes(w: number, h: number): boolean {
|
|
668
|
+
return w > h * 1.35 || h > w * 1.35;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Build a bounded, aspect-aware regional probe set for multi-QR images (F02-005). */
|
|
672
|
+
private static buildMultiQrRegions(w: number, h: number): ScanRegion[] {
|
|
673
|
+
const regions: ScanRegion[] = [];
|
|
674
|
+
const isWide: boolean = w > h * 1.25;
|
|
675
|
+
const isTall: boolean = h > w * 1.25;
|
|
676
|
+
|
|
677
|
+
if (isWide) {
|
|
678
|
+
ZxingImageHelper.appendThirdColumnBands(regions, w, h);
|
|
679
|
+
ZxingImageHelper.appendCardStyleHorizontalProbes(regions, w, h);
|
|
680
|
+
ZxingImageHelper.appendCenteredColumnProbes(regions, w, h, 3);
|
|
681
|
+
ZxingImageHelper.appendOverlappingHorizontalProbes(regions, w, h);
|
|
682
|
+
} else if (isTall) {
|
|
683
|
+
ZxingImageHelper.appendThirdRowBands(regions, w, h);
|
|
684
|
+
ZxingImageHelper.appendCenteredRowProbes(regions, w, h, 3);
|
|
685
|
+
ZxingImageHelper.appendOverlappingVerticalProbes(regions, w, h);
|
|
686
|
+
} else {
|
|
687
|
+
ZxingImageHelper.appendThirdColumnBands(regions, w, h);
|
|
688
|
+
ZxingImageHelper.appendThirdRowBands(regions, w, h);
|
|
689
|
+
ZxingImageHelper.appendCenteredColumnProbes(regions, w, h, 3);
|
|
690
|
+
ZxingImageHelper.appendCenteredRowProbes(regions, w, h, 3);
|
|
691
|
+
const halfW: number = Math.floor(w / 2);
|
|
692
|
+
const halfH: number = Math.floor(h / 2);
|
|
693
|
+
if (halfW > 0 && halfH > 0) {
|
|
694
|
+
regions.push({ x: 0, y: 0, width: halfW, height: halfH });
|
|
695
|
+
regions.push({ x: halfW, y: 0, width: w - halfW, height: halfH });
|
|
696
|
+
regions.push({ x: 0, y: halfH, width: halfW, height: h - halfH });
|
|
697
|
+
regions.push({ x: halfW, y: halfH, width: w - halfW, height: h - halfH });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return regions;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private static appendThirdColumnBands(regions: ScanRegion[], w: number, h: number): void {
|
|
704
|
+
const bandW: number = Math.floor(w / 3);
|
|
705
|
+
if (bandW <= 0) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
regions.push({ x: 0, y: 0, width: bandW, height: h });
|
|
709
|
+
regions.push({ x: bandW, y: 0, width: bandW, height: h });
|
|
710
|
+
regions.push({ x: bandW * 2, y: 0, width: w - bandW * 2, height: h });
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private static appendThirdRowBands(regions: ScanRegion[], w: number, h: number): void {
|
|
714
|
+
const bandH: number = Math.floor(h / 3);
|
|
715
|
+
if (bandH <= 0) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
regions.push({ x: 0, y: 0, width: w, height: bandH });
|
|
719
|
+
regions.push({ x: 0, y: bandH, width: w, height: bandH });
|
|
720
|
+
regions.push({ x: 0, y: bandH * 2, width: w, height: h - bandH * 2 });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* 50%-height sliding windows (25% step) so 3+ stacked QRs each get a window
|
|
725
|
+
* where the code is fully contained; tight 1/3 bands alone often miss codes.
|
|
726
|
+
*/
|
|
727
|
+
private static appendOverlappingVerticalProbes(regions: ScanRegion[], w: number, h: number): void {
|
|
728
|
+
const probeH: number = Math.max(1, Math.floor(h / 2));
|
|
729
|
+
const stepH: number = Math.max(1, Math.floor(h / 4));
|
|
730
|
+
if (probeH >= h) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
let y: number = 0;
|
|
734
|
+
while (y + probeH <= h) {
|
|
735
|
+
regions.push({ x: 0, y: y, width: w, height: probeH });
|
|
736
|
+
y += stepH;
|
|
737
|
+
}
|
|
738
|
+
const lastY: number = h - probeH;
|
|
739
|
+
if (lastY > 0) {
|
|
740
|
+
regions.push({ x: 0, y: lastY, width: w, height: probeH });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** 50%-width sliding windows (33% step) for side-by-side multi-QR layouts. */
|
|
745
|
+
private static appendOverlappingHorizontalProbes(regions: ScanRegion[], w: number, h: number): void {
|
|
746
|
+
const probeW: number = Math.max(1, Math.floor(w / 2));
|
|
747
|
+
const stepW: number = Math.max(1, Math.floor(w / 3));
|
|
748
|
+
if (probeW >= w) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
let x: number = 0;
|
|
752
|
+
while (x + probeW <= w) {
|
|
753
|
+
regions.push({ x: x, y: 0, width: probeW, height: h });
|
|
754
|
+
x += stepW;
|
|
755
|
+
}
|
|
756
|
+
const lastX: number = w - probeW;
|
|
757
|
+
if (lastX > 0) {
|
|
758
|
+
regions.push({ x: lastX, y: 0, width: probeW, height: h });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Column-centered windows (~45% image width) aligned to N-column grids so the
|
|
764
|
+
* rightmost of 3 side-by-side QRs is fully enclosed with quiet zone margin.
|
|
765
|
+
*/
|
|
766
|
+
private static appendCenteredColumnProbes(regions: ScanRegion[], w: number, h: number,
|
|
767
|
+
columns: number): void {
|
|
768
|
+
if (columns <= 0 || w <= 0 || h <= 0) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const cellW: number = w / columns;
|
|
772
|
+
const probeW: number = Math.max(1, Math.min(w, Math.floor(Math.max(cellW, w * 0.45))));
|
|
773
|
+
for (let i = 0; i < columns; i++) {
|
|
774
|
+
const centerX: number = cellW * (i + 0.5);
|
|
775
|
+
let x: number = Math.floor(centerX - probeW / 2);
|
|
776
|
+
if (x < 0) {
|
|
777
|
+
x = 0;
|
|
778
|
+
}
|
|
779
|
+
if (x + probeW > w) {
|
|
780
|
+
x = w - probeW;
|
|
781
|
+
}
|
|
782
|
+
if (x < 0) {
|
|
783
|
+
x = 0;
|
|
784
|
+
}
|
|
785
|
+
const width: number = Math.min(probeW, w - x);
|
|
786
|
+
if (width > 0) {
|
|
787
|
+
regions.push({ x: x, y: 0, width: width, height: h });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Wide card-style rows (labels above each code): square crops per column, skipping
|
|
794
|
+
* the top caption band so ZXing sees one QR per window on album screenshots.
|
|
795
|
+
*/
|
|
796
|
+
private static appendCardStyleHorizontalProbes(regions: ScanRegion[], w: number, h: number): void {
|
|
797
|
+
if (w < h * 1.5 || w <= 0 || h <= 0) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
ZxingImageHelper.appendCardStyleHorizontalProbesForColumns(regions, w, h, 3);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private static appendCardStyleHorizontalProbesForColumns(regions: ScanRegion[], w: number, h: number,
|
|
804
|
+
columns: number): void {
|
|
805
|
+
if (columns <= 0) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const cellW: number = w / columns;
|
|
809
|
+
const topSkip: number = Math.max(0, Math.floor(h * 0.12));
|
|
810
|
+
const availH: number = h - topSkip;
|
|
811
|
+
let squareSize: number = Math.floor(Math.min(cellW * 0.92, availH * 0.92));
|
|
812
|
+
if (squareSize < 32) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
for (let i = 0; i < columns; i++) {
|
|
816
|
+
const centerX: number = cellW * (i + 0.5);
|
|
817
|
+
let x: number = Math.floor(centerX - squareSize / 2);
|
|
818
|
+
if (x < 0) {
|
|
819
|
+
x = 0;
|
|
820
|
+
}
|
|
821
|
+
if (x + squareSize > w) {
|
|
822
|
+
x = w - squareSize;
|
|
823
|
+
}
|
|
824
|
+
if (x < 0) {
|
|
825
|
+
x = 0;
|
|
826
|
+
}
|
|
827
|
+
const width: number = Math.min(squareSize, w - x);
|
|
828
|
+
const height: number = Math.min(squareSize, availH);
|
|
829
|
+
if (width > 0 && height > 0) {
|
|
830
|
+
regions.push({ x: x, y: topSkip, width: width, height: height });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** Row-centered windows for 3+ vertically stacked QRs (symmetric to column probes). */
|
|
836
|
+
private static appendCenteredRowProbes(regions: ScanRegion[], w: number, h: number,
|
|
837
|
+
rows: number): void {
|
|
838
|
+
if (rows <= 0 || w <= 0 || h <= 0) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const cellH: number = h / rows;
|
|
842
|
+
const probeH: number = Math.max(1, Math.min(h, Math.floor(Math.max(cellH, h * 0.45))));
|
|
843
|
+
for (let i = 0; i < rows; i++) {
|
|
844
|
+
const centerY: number = cellH * (i + 0.5);
|
|
845
|
+
let y: number = Math.floor(centerY - probeH / 2);
|
|
846
|
+
if (y < 0) {
|
|
847
|
+
y = 0;
|
|
848
|
+
}
|
|
849
|
+
if (y + probeH > h) {
|
|
850
|
+
y = h - probeH;
|
|
851
|
+
}
|
|
852
|
+
if (y < 0) {
|
|
853
|
+
y = 0;
|
|
854
|
+
}
|
|
855
|
+
const height: number = Math.min(probeH, h - y);
|
|
856
|
+
if (height > 0) {
|
|
857
|
+
regions.push({ x: 0, y: y, width: w, height: height });
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private static mergeUniqueResults(first: Result[], second: Result[]): Result[] {
|
|
863
|
+
const merged: Result[] = [];
|
|
864
|
+
const seenTexts: string[] = [];
|
|
865
|
+
const lists: Result[][] = [first, second];
|
|
866
|
+
for (let i = 0; i < lists.length; i++) {
|
|
867
|
+
const list: Result[] = lists[i];
|
|
868
|
+
for (let j = 0; j < list.length; j++) {
|
|
869
|
+
ZxingImageHelper.appendUniqueResult(merged, seenTexts, list[j]);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return merged;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private static appendUniqueResult(merged: Result[], seenTexts: string[], result: Result): void {
|
|
876
|
+
let text: string = '';
|
|
877
|
+
try {
|
|
878
|
+
text = ZxingImageHelper.copyDecodedText(result.getText());
|
|
879
|
+
} catch (textErr) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (text.length === 0) {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
for (let i = 0; i < seenTexts.length; i++) {
|
|
886
|
+
if (seenTexts[i] === text) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
seenTexts.push(text);
|
|
891
|
+
merged.push(result);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private static scanRegions(lum: Uint8ClampedArray, srcW: number, srcH: number,
|
|
895
|
+
regions: ScanRegion[], hints: Map<DecodeHintType, Object>): Result[] {
|
|
896
|
+
const results: Result[] = [];
|
|
897
|
+
const tryHarderHints: Map<DecodeHintType, Object> =
|
|
898
|
+
ZxingImageHelper.createSmallRegionDecodeHints();
|
|
899
|
+
for (let i = 0; i < regions.length; i++) {
|
|
900
|
+
const region: ScanRegion = regions[i];
|
|
901
|
+
const regionLum: Uint8ClampedArray = ZxingImageHelper.copyLuminanceRegion(
|
|
902
|
+
lum, srcW, srcH, region.x, region.y, region.width, region.height);
|
|
903
|
+
const useTryHarder: boolean =
|
|
904
|
+
ZxingImageHelper.shouldUseTryHarderForRegion(region, srcW, srcH);
|
|
905
|
+
const regionHints: Map<DecodeHintType, Object> = useTryHarder ? tryHarderHints : hints;
|
|
906
|
+
const result: Result | null =
|
|
907
|
+
ZxingImageHelper.scanBasic(regionLum, region.width, region.height, regionHints);
|
|
908
|
+
if (result !== null && result.getText().length > 0) {
|
|
909
|
+
results.push(result);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return results;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** TRY_HARDER only on narrow bands and card-style square crops; avoids 2x cost on large windows. */
|
|
916
|
+
private static shouldUseTryHarderForRegion(region: ScanRegion, srcW: number, srcH: number): boolean {
|
|
917
|
+
const narrowW: boolean = region.width * 10 <= srcW * 4;
|
|
918
|
+
const narrowH: boolean = region.height * 10 <= srcH * 4;
|
|
919
|
+
const topSkip: number = Math.max(0, Math.floor(srcH * 0.12));
|
|
920
|
+
const isCardCrop: boolean = region.y >= topSkip && region.height <= srcH * 0.88;
|
|
921
|
+
return narrowW || narrowH || isCardCrop;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private static createSmallRegionDecodeHints(): Map<DecodeHintType, Object> {
|
|
925
|
+
const hints = new Map<DecodeHintType, Object>();
|
|
926
|
+
hints.set(DecodeHintType.TRY_HARDER, true);
|
|
927
|
+
hints.set(DecodeHintType.POSSIBLE_FORMATS, STANDARD_DECODE_FORMATS);
|
|
928
|
+
hints.set(DecodeHintType.CHARACTER_SET, 'UTF-8');
|
|
929
|
+
return hints;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private static createDecodeHints(tryHarder: boolean = true): Map<DecodeHintType, Object> {
|
|
933
|
+
const hints = new Map<DecodeHintType, Object>();
|
|
934
|
+
if (tryHarder) {
|
|
935
|
+
hints.set(DecodeHintType.TRY_HARDER, true);
|
|
936
|
+
}
|
|
937
|
+
hints.set(DecodeHintType.POSSIBLE_FORMATS, STANDARD_DECODE_FORMATS);
|
|
938
|
+
hints.set(DecodeHintType.CHARACTER_SET, 'UTF-8');
|
|
939
|
+
return hints;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** MaxiCode only: no TRY_HARDER, no inverted pass, bounded to full image (+ one center crop). */
|
|
943
|
+
private static createMaxiCodeDecodeHints(): Map<DecodeHintType, Object> {
|
|
944
|
+
const hints = new Map<DecodeHintType, Object>();
|
|
945
|
+
hints.set(DecodeHintType.POSSIBLE_FORMATS, MAXICODE_DECODE_FORMATS);
|
|
946
|
+
hints.set(DecodeHintType.CHARACTER_SET, 'UTF-8');
|
|
947
|
+
return hints;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Wide album photos often place the barcode on the left and descriptive text on the right.
|
|
952
|
+
* Probe those left-biased crops before giving up on a landscape layout.
|
|
953
|
+
*/
|
|
954
|
+
private static buildWideAlbumLeftRegions(w: number, h: number): ScanRegion[] {
|
|
955
|
+
const regions: ScanRegion[] = [];
|
|
956
|
+
if (w <= h * 1.25) {
|
|
957
|
+
return regions;
|
|
958
|
+
}
|
|
959
|
+
const squareW: number = Math.min(h, w);
|
|
960
|
+
regions.push({ x: 0, y: 0, width: squareW, height: h });
|
|
961
|
+
const left45: number = Math.max(1, Math.round(w * 0.45));
|
|
962
|
+
if (left45 !== squareW) {
|
|
963
|
+
regions.push({ x: 0, y: 0, width: left45, height: h });
|
|
964
|
+
}
|
|
965
|
+
const leftW: number = Math.max(1, Math.round(w * 0.55));
|
|
966
|
+
if (leftW !== squareW && leftW !== left45) {
|
|
967
|
+
regions.push({ x: 0, y: 0, width: leftW, height: h });
|
|
968
|
+
}
|
|
969
|
+
return regions;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
private static scanWideAlbumFallback(lum: Uint8ClampedArray, w: number, h: number): Result | null {
|
|
973
|
+
const regions: ScanRegion[] = ZxingImageHelper.buildWideAlbumLeftRegions(w, h);
|
|
974
|
+
if (regions.length === 0) {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
const hints: Map<DecodeHintType, Object> = ZxingImageHelper.createDecodeHints(true);
|
|
978
|
+
for (let i = 0; i < regions.length; i++) {
|
|
979
|
+
const region: ScanRegion = regions[i];
|
|
980
|
+
const regionLum: Uint8ClampedArray = ZxingImageHelper.copyLuminanceRegion(
|
|
981
|
+
lum, w, h, region.x, region.y, region.width, region.height);
|
|
982
|
+
const result: Result | null =
|
|
983
|
+
ZxingImageHelper.scanBasic(regionLum, region.width, region.height, hints, true);
|
|
984
|
+
if (result !== null) {
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
private static scanMaxiCode(lum: Uint8ClampedArray, w: number, h: number): Result | null {
|
|
992
|
+
const hints: Map<DecodeHintType, Object> = ZxingImageHelper.createMaxiCodeDecodeHints();
|
|
993
|
+
const regions: ScanRegion[] = ZxingImageHelper.buildWideAlbumLeftRegions(w, h);
|
|
994
|
+
regions.push({ x: 0, y: 0, width: w, height: h });
|
|
995
|
+
if (w > 32 && h > 32) {
|
|
996
|
+
const marginX: number = Math.max(1, Math.round(w * 0.05));
|
|
997
|
+
const marginY: number = Math.max(1, Math.round(h * 0.05));
|
|
998
|
+
const cropW: number = w - marginX * 2;
|
|
999
|
+
const cropH: number = h - marginY * 2;
|
|
1000
|
+
if (cropW > 0 && cropH > 0) {
|
|
1001
|
+
regions.push({ x: marginX, y: marginY, width: cropW, height: cropH });
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
for (let i = 0; i < regions.length; i++) {
|
|
1005
|
+
const region: ScanRegion = regions[i];
|
|
1006
|
+
const regionLum: Uint8ClampedArray = region.x === 0 && region.y === 0 &&
|
|
1007
|
+
region.width === w && region.height === h
|
|
1008
|
+
? lum
|
|
1009
|
+
: ZxingImageHelper.copyLuminanceRegion(
|
|
1010
|
+
lum, w, h, region.x, region.y, region.width, region.height);
|
|
1011
|
+
const result: Result | null =
|
|
1012
|
+
ZxingImageHelper.tryMaxiCodeDecode(regionLum, region.width, region.height, hints);
|
|
1013
|
+
if (result !== null) {
|
|
1014
|
+
return result;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* MaxiCodeReader assumes a nearly-square pure barcode. Skip decode when the binarized
|
|
1022
|
+
* enclosing rectangle is too wide/tall, which otherwise crashes on album composite images.
|
|
1023
|
+
*/
|
|
1024
|
+
private static tryMaxiCodeDecode(lum: Uint8ClampedArray, w: number, h: number,
|
|
1025
|
+
hints: Map<DecodeHintType, Object>): Result | null {
|
|
1026
|
+
const source: RGBLuminanceSource = new RGBLuminanceSource(lum, w, h);
|
|
1027
|
+
const hybridResult: Result | null =
|
|
1028
|
+
ZxingImageHelper.tryMaxiCodeOnSource(source, hints, true);
|
|
1029
|
+
if (hybridResult !== null) {
|
|
1030
|
+
return hybridResult;
|
|
1031
|
+
}
|
|
1032
|
+
const globalResult: Result | null =
|
|
1033
|
+
ZxingImageHelper.tryMaxiCodeOnSource(source, hints, false);
|
|
1034
|
+
if (globalResult !== null) {
|
|
1035
|
+
return globalResult;
|
|
1036
|
+
}
|
|
1037
|
+
try {
|
|
1038
|
+
const inverted: InvertedLuminanceSource = new InvertedLuminanceSource(source);
|
|
1039
|
+
return ZxingImageHelper.tryMaxiCodeOnSource(inverted, hints, true);
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private static tryMaxiCodeOnSource(source: LuminanceSource, hints: Map<DecodeHintType, Object>,
|
|
1046
|
+
useHybrid: boolean): Result | null {
|
|
1047
|
+
try {
|
|
1048
|
+
const bitmap: BinaryBitmap = useHybrid
|
|
1049
|
+
? new BinaryBitmap(new HybridBinarizer(source))
|
|
1050
|
+
: new BinaryBitmap(new GlobalHistogramBinarizer(source));
|
|
1051
|
+
const matrix = bitmap.getBlackMatrix();
|
|
1052
|
+
const enclosing: Int32Array | null = matrix.getEnclosingRectangle();
|
|
1053
|
+
if (enclosing === null || enclosing === undefined) {
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
const rectW: number = enclosing[2];
|
|
1057
|
+
const rectH: number = enclosing[3];
|
|
1058
|
+
if (rectW <= 0 || rectH <= 0) {
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
const aspect: number = rectW / rectH;
|
|
1062
|
+
if (aspect > 1.8 || aspect < 0.55) {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
return ZxingImageHelper.decodeBinaryBitmap(bitmap, hints);
|
|
1066
|
+
} catch (e) {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/** F02-005 region probes support mixed barcode formats, bounded without TRY_HARDER. */
|
|
1072
|
+
private static createMultiRegionDecodeHints(): Map<DecodeHintType, Object> {
|
|
1073
|
+
const hints = new Map<DecodeHintType, Object>();
|
|
1074
|
+
hints.set(DecodeHintType.POSSIBLE_FORMATS, STANDARD_DECODE_FORMATS);
|
|
1075
|
+
hints.set(DecodeHintType.CHARACTER_SET, 'UTF-8');
|
|
1076
|
+
return hints;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Bypass @zxing/text-encoding's huge GB18030/Shift_JIS tables, which can crash
|
|
1081
|
+
* ArkTS when a Chinese QR (Hanzi/GB2312 or UTF-8 byte mode) is decoded.
|
|
1082
|
+
*/
|
|
1083
|
+
private static stringCodecReady: boolean = false;
|
|
1084
|
+
|
|
1085
|
+
private static ensureSafeStringCodec(): void {
|
|
1086
|
+
if (ZxingImageHelper.stringCodecReady) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
ZXingStringEncoding.customDecoder = (bytes: Uint8Array, encodingName: string): string => {
|
|
1090
|
+
return ZxingImageHelper.decodeBytesToString(bytes, encodingName);
|
|
1091
|
+
};
|
|
1092
|
+
ZxingImageHelper.stringCodecReady = true;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
private static copyDecodedText(text: string): string {
|
|
1096
|
+
return text === undefined || text === null ? '' : String(text);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private static normalizeCharsetLabel(encodingName: string): string {
|
|
1100
|
+
const raw: string = encodingName === undefined || encodingName === null ? '' : encodingName;
|
|
1101
|
+
const lowered: string = raw.trim().toLowerCase();
|
|
1102
|
+
const n: string = lowered.split('_').join('-').split(' ').join('-');
|
|
1103
|
+
if (n === 'utf8' || n === 'utf-8' || n === 'unicode') {
|
|
1104
|
+
return 'utf-8';
|
|
1105
|
+
}
|
|
1106
|
+
if (n === 'gb2312' || n === 'gbk' || n === 'gb18030') {
|
|
1107
|
+
return 'gb18030';
|
|
1108
|
+
}
|
|
1109
|
+
if (n === 'shift-jis' || n === 'sjis' || n === 'shiftjis') {
|
|
1110
|
+
return 'shift_jis';
|
|
1111
|
+
}
|
|
1112
|
+
if (n === 'iso8859-1' || n === 'iso-8859-1' || n === 'iso88591') {
|
|
1113
|
+
return 'iso-8859-1';
|
|
1114
|
+
}
|
|
1115
|
+
return n.length > 0 ? n : 'utf-8';
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private static decodeBytesToString(bytes: Uint8Array, encodingName: string): string {
|
|
1119
|
+
const label: string = ZxingImageHelper.normalizeCharsetLabel(encodingName);
|
|
1120
|
+
if (label === 'iso-8859-1') {
|
|
1121
|
+
let latin1: string = '';
|
|
1122
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1123
|
+
latin1 += String.fromCharCode(bytes[i] & 0xFF);
|
|
1124
|
+
}
|
|
1125
|
+
return latin1;
|
|
1126
|
+
}
|
|
1127
|
+
try {
|
|
1128
|
+
const decoder = util.TextDecoder.create(label);
|
|
1129
|
+
return decoder.decodeWithStream(bytes, { stream: false });
|
|
1130
|
+
} catch (e) {
|
|
1131
|
+
if (label !== 'utf-8') {
|
|
1132
|
+
const utf8Decoder = util.TextDecoder.create('utf-8');
|
|
1133
|
+
return utf8Decoder.decodeWithStream(bytes, { stream: false });
|
|
1134
|
+
}
|
|
1135
|
+
const err: Error = e as Error;
|
|
1136
|
+
throw err;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/** Original and inverted decode only; does not allocate crop/zoom retry buffers. */
|
|
1141
|
+
private static scanBasic(lum: Uint8ClampedArray, w: number, h: number,
|
|
1142
|
+
hints: Map<DecodeHintType, Object>, allowInverted: boolean = true): Result | null {
|
|
1143
|
+
const source: RGBLuminanceSource = new RGBLuminanceSource(lum, w, h);
|
|
1144
|
+
const original: Result | null = ZxingImageHelper.decodeOnSource(source, hints);
|
|
1145
|
+
if (original !== null) {
|
|
1146
|
+
return original;
|
|
1147
|
+
}
|
|
1148
|
+
if (!allowInverted) {
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
const inverted: InvertedLuminanceSource = new InvertedLuminanceSource(source);
|
|
1153
|
+
return ZxingImageHelper.decodeOnSource(inverted, hints);
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Retry DataMatrix / PDF417 with dedicated readers after MultiFormatReader fails.
|
|
1161
|
+
* @ohos/zxing MultiFormatReader can miss matrix codes even when they are listed in POSSIBLE_FORMATS.
|
|
1162
|
+
*/
|
|
1163
|
+
private static scanMatrixSymbologyFallback(lum: Uint8ClampedArray, w: number, h: number): Result | null {
|
|
1164
|
+
const hints: Map<DecodeHintType, Object> = new Map<DecodeHintType, Object>();
|
|
1165
|
+
hints.set(DecodeHintType.TRY_HARDER, true);
|
|
1166
|
+
hints.set(DecodeHintType.CHARACTER_SET, 'UTF-8');
|
|
1167
|
+
const source: RGBLuminanceSource = new RGBLuminanceSource(lum, w, h);
|
|
1168
|
+
let result: Result | null = ZxingImageHelper.decodeWithDedicatedMatrixReaders(source, hints);
|
|
1169
|
+
if (result !== null) {
|
|
1170
|
+
return result;
|
|
1171
|
+
}
|
|
1172
|
+
try {
|
|
1173
|
+
const inverted: InvertedLuminanceSource = new InvertedLuminanceSource(source);
|
|
1174
|
+
result = ZxingImageHelper.decodeWithDedicatedMatrixReaders(inverted, hints);
|
|
1175
|
+
if (result !== null) {
|
|
1176
|
+
return result;
|
|
1177
|
+
}
|
|
1178
|
+
} catch (e) {
|
|
1179
|
+
// continue with crop retries
|
|
1180
|
+
}
|
|
1181
|
+
return ZxingImageHelper.scanWithRetries(lum, w, h, {
|
|
1182
|
+
skipInitialBasic: true,
|
|
1183
|
+
dedicatedMatrixReaders: true,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private static decodeWithDedicatedMatrixReaders(source: LuminanceSource,
|
|
1188
|
+
hints: Map<DecodeHintType, Object>): Result | null {
|
|
1189
|
+
const bitmaps: BinaryBitmap[] = [
|
|
1190
|
+
new BinaryBitmap(new HybridBinarizer(source)),
|
|
1191
|
+
new BinaryBitmap(new GlobalHistogramBinarizer(source)),
|
|
1192
|
+
];
|
|
1193
|
+
for (let b = 0; b < bitmaps.length; b++) {
|
|
1194
|
+
for (let r = 0; r < MATRIX_SYMBOLOGY_READERS.length; r++) {
|
|
1195
|
+
try {
|
|
1196
|
+
const result: Result = MATRIX_SYMBOLOGY_READERS[r].decode(bitmaps[b], hints);
|
|
1197
|
+
if (result !== null && result.getText().length > 0) {
|
|
1198
|
+
return result;
|
|
1199
|
+
}
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
// try next reader / binarizer
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
private static decodeOnSource(source: LuminanceSource,
|
|
1209
|
+
hints: Map<DecodeHintType, Object>): Result | null {
|
|
1210
|
+
const hybridResult: Result | null = ZxingImageHelper.decodeBinaryBitmap(
|
|
1211
|
+
new BinaryBitmap(new HybridBinarizer(source)), hints);
|
|
1212
|
+
if (hybridResult !== null) {
|
|
1213
|
+
return hybridResult;
|
|
1214
|
+
}
|
|
1215
|
+
return ZxingImageHelper.decodeBinaryBitmap(
|
|
1216
|
+
new BinaryBitmap(new GlobalHistogramBinarizer(source)), hints);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private static copyLuminanceRegion(
|
|
1220
|
+
lum: Uint8ClampedArray, srcW: number, srcH: number,
|
|
1221
|
+
x: number, y: number, regionW: number, regionH: number,
|
|
1222
|
+
): Uint8ClampedArray {
|
|
1223
|
+
const out: Uint8ClampedArray = new Uint8ClampedArray(regionW * regionH);
|
|
1224
|
+
for (let row = 0; row < regionH; row++) {
|
|
1225
|
+
const srcStart: number = (y + row) * srcW + x;
|
|
1226
|
+
const dstStart: number = row * regionW;
|
|
1227
|
+
for (let col = 0; col < regionW; col++) {
|
|
1228
|
+
out[dstStart + col] = lum[srcStart + col];
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return out;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
private static decodeBinaryBitmap(bitmap: BinaryBitmap, hints: Map<DecodeHintType, Object>): Result | null {
|
|
1235
|
+
try {
|
|
1236
|
+
const reader: MultiFormatReader = new MultiFormatReader();
|
|
1237
|
+
reader.setHints(hints);
|
|
1238
|
+
return reader.decode(bitmap, hints);
|
|
1239
|
+
} catch (e) {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/** Nearest-neighbor luminance upscaling, equivalent to Android Bitmap.createScaledBitmap. */
|
|
1245
|
+
private static scaleLuminance(
|
|
1246
|
+
src: Uint8ClampedArray,
|
|
1247
|
+
srcW: number,
|
|
1248
|
+
srcH: number,
|
|
1249
|
+
factor: number
|
|
1250
|
+
): RGBLuminanceSource {
|
|
1251
|
+
const dstW: number = Math.max(1, Math.round(srcW * factor));
|
|
1252
|
+
const dstH: number = Math.max(1, Math.round(srcH * factor));
|
|
1253
|
+
const dst: Uint8ClampedArray = new Uint8ClampedArray(dstW * dstH);
|
|
1254
|
+
for (let y = 0; y < dstH; y++) {
|
|
1255
|
+
const srcY: number = Math.min(srcH - 1, Math.floor(y / factor));
|
|
1256
|
+
for (let x = 0; x < dstW; x++) {
|
|
1257
|
+
const srcX: number = Math.min(srcW - 1, Math.floor(x / factor));
|
|
1258
|
+
dst[y * dstW + x] = src[srcY * srcW + srcX];
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return new RGBLuminanceSource(dst, dstW, dstH);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/** Read QR error correction level decoded from format information metadata. */
|
|
1265
|
+
private static extractCorrectionLevel(result: Result): string | undefined {
|
|
1266
|
+
const metadata: Map<ResultMetadataType, Object> | null = result.getResultMetadata();
|
|
1267
|
+
if (metadata === null || metadata === undefined) {
|
|
1268
|
+
return undefined;
|
|
1269
|
+
}
|
|
1270
|
+
const ecLevel: Object | undefined = metadata.get(ResultMetadataType.ERROR_CORRECTION_LEVEL);
|
|
1271
|
+
if (ecLevel === undefined || ecLevel === null) {
|
|
1272
|
+
return undefined;
|
|
1273
|
+
}
|
|
1274
|
+
const level: string = String(ecLevel);
|
|
1275
|
+
return level.length > 0 ? level : undefined;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/** Map BarcodeFormat to the JS CodeType string, aligned with Android getCodeType(). */
|
|
1279
|
+
private static getCodeType(format: BarcodeFormat): string {
|
|
1280
|
+
switch (format) {
|
|
1281
|
+
case BarcodeFormat.AZTEC:
|
|
1282
|
+
return 'Aztec';
|
|
1283
|
+
case BarcodeFormat.CODABAR:
|
|
1284
|
+
return 'Codabar';
|
|
1285
|
+
case BarcodeFormat.CODE_39:
|
|
1286
|
+
return 'Code39';
|
|
1287
|
+
case BarcodeFormat.CODE_93:
|
|
1288
|
+
return 'Code93';
|
|
1289
|
+
case BarcodeFormat.CODE_128:
|
|
1290
|
+
return 'Code128';
|
|
1291
|
+
case BarcodeFormat.DATA_MATRIX:
|
|
1292
|
+
return 'DataMatrix';
|
|
1293
|
+
case BarcodeFormat.EAN_8:
|
|
1294
|
+
return 'Ean8';
|
|
1295
|
+
case BarcodeFormat.EAN_13:
|
|
1296
|
+
return 'Ean13';
|
|
1297
|
+
case BarcodeFormat.ITF:
|
|
1298
|
+
return 'ITF';
|
|
1299
|
+
case BarcodeFormat.MAXICODE:
|
|
1300
|
+
return 'MaxiCode';
|
|
1301
|
+
case BarcodeFormat.PDF_417:
|
|
1302
|
+
return 'PDF417';
|
|
1303
|
+
case BarcodeFormat.QR_CODE:
|
|
1304
|
+
return 'QRCode';
|
|
1305
|
+
case BarcodeFormat.RSS_14:
|
|
1306
|
+
return 'RSS14';
|
|
1307
|
+
case BarcodeFormat.RSS_EXPANDED:
|
|
1308
|
+
return 'RSSExpanded';
|
|
1309
|
+
case BarcodeFormat.UPC_A:
|
|
1310
|
+
return 'UPCA';
|
|
1311
|
+
case BarcodeFormat.UPC_E:
|
|
1312
|
+
return 'UPCE';
|
|
1313
|
+
case BarcodeFormat.UPC_EAN_EXTENSION:
|
|
1314
|
+
return 'UPCEANExtension';
|
|
1315
|
+
default:
|
|
1316
|
+
return '';
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/** Convert a sandbox URI or legacy file:// path to an absolute path for file/image APIs. */
|
|
1321
|
+
private static normalizeSourcePath(source: string): string {
|
|
1322
|
+
if (source.startsWith('file://')) {
|
|
1323
|
+
try {
|
|
1324
|
+
return new fileUri.FileUri(source).path;
|
|
1325
|
+
} catch (e) {
|
|
1326
|
+
// Fall back for legacy file:///absolute/path URIs.
|
|
1327
|
+
}
|
|
1328
|
+
let path: string = source.substring(7);
|
|
1329
|
+
try {
|
|
1330
|
+
path = decodeURIComponent(path);
|
|
1331
|
+
} catch (decodeErr) {
|
|
1332
|
+
// keep original path when it is not a valid encoded uri
|
|
1333
|
+
}
|
|
1334
|
+
return path;
|
|
1335
|
+
}
|
|
1336
|
+
return source;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
private static loadPixelMapFromPath(path: string): image.PixelMap {
|
|
1340
|
+
if (!ZxingImageHelper.fileExists(path)) {
|
|
1341
|
+
throw new Error('IMAGE_NOT_FOUND');
|
|
1342
|
+
}
|
|
1343
|
+
let source: image.ImageSource | undefined = undefined;
|
|
1344
|
+
try {
|
|
1345
|
+
source = image.createImageSource(path);
|
|
1346
|
+
} catch (e) {
|
|
1347
|
+
throw new Error('IMAGE_NOT_FOUND');
|
|
1348
|
+
}
|
|
1349
|
+
if (source === undefined || source === null) {
|
|
1350
|
+
throw new Error('IMAGE_NOT_FOUND');
|
|
1351
|
+
}
|
|
1352
|
+
try {
|
|
1353
|
+
const sourceSize = ZxingImageHelper.readSourceSize(source);
|
|
1354
|
+
return source.createPixelMapSync(ZxingImageHelper.buildDecodingOptions(sourceSize));
|
|
1355
|
+
} catch (e) {
|
|
1356
|
+
const err: Error = e as Error;
|
|
1357
|
+
hilog.error(
|
|
1358
|
+
DOMAIN,
|
|
1359
|
+
TAG,
|
|
1360
|
+
'loadPixelMapFromPath failed: %{public}s path=%{public}s',
|
|
1361
|
+
ZxingImageHelper.errMsg(err),
|
|
1362
|
+
path
|
|
1363
|
+
);
|
|
1364
|
+
throw new Error('IMAGE_NOT_FOUND');
|
|
1365
|
+
} finally {
|
|
1366
|
+
try {
|
|
1367
|
+
source.release();
|
|
1368
|
+
} catch (releaseErr) {
|
|
1369
|
+
hilog.warn(
|
|
1370
|
+
DOMAIN,
|
|
1371
|
+
TAG,
|
|
1372
|
+
'loadPixelMapFromPath release failed: %{public}s',
|
|
1373
|
+
ZxingImageHelper.errMsg(releaseErr as Error)
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
private static fileExists(path: string): boolean {
|
|
1380
|
+
try {
|
|
1381
|
+
fs.accessSync(path);
|
|
1382
|
+
return true;
|
|
1383
|
+
} catch (e) {
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/** Read padding insets from TurboModule options (nested object via Record bridge). */
|
|
1389
|
+
private static parsePaddingFromOptions(options: TM.RNQrGenerator.GenerateOptions): PaddingInsets {
|
|
1390
|
+
const opts = options as Record<string, Object>;
|
|
1391
|
+
const paddingVal: Object | undefined = opts.padding;
|
|
1392
|
+
if (paddingVal !== undefined && paddingVal !== null) {
|
|
1393
|
+
const padding = paddingVal as Record<string, Object>;
|
|
1394
|
+
return {
|
|
1395
|
+
top: ZxingImageHelper.getOptionNumber(padding, 'top', 0),
|
|
1396
|
+
left: ZxingImageHelper.getOptionNumber(padding, 'left', 0),
|
|
1397
|
+
bottom: ZxingImageHelper.getOptionNumber(padding, 'bottom', 0),
|
|
1398
|
+
right: ZxingImageHelper.getOptionNumber(padding, 'right', 0),
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
const typedPadding = options.padding;
|
|
1402
|
+
if (typedPadding !== undefined && typedPadding !== null) {
|
|
1403
|
+
return {
|
|
1404
|
+
top: typedPadding.top ?? 0,
|
|
1405
|
+
left: typedPadding.left ?? 0,
|
|
1406
|
+
bottom: typedPadding.bottom ?? 0,
|
|
1407
|
+
right: typedPadding.right ?? 0,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
return { top: 0, left: 0, bottom: 0, right: 0 };
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/** Map correction level string to ZXing enum, aligned with Android RNQrGeneratorModule. */
|
|
1414
|
+
private static parseCorrectionLevel(level: string): QRCodeDecoderErrorCorrectionLevel {
|
|
1415
|
+
if (level === 'L') {
|
|
1416
|
+
return QRCodeDecoderErrorCorrectionLevel.L;
|
|
1417
|
+
}
|
|
1418
|
+
if (level === 'M') {
|
|
1419
|
+
return QRCodeDecoderErrorCorrectionLevel.M;
|
|
1420
|
+
}
|
|
1421
|
+
if (level === 'Q') {
|
|
1422
|
+
return QRCodeDecoderErrorCorrectionLevel.Q;
|
|
1423
|
+
}
|
|
1424
|
+
return QRCodeDecoderErrorCorrectionLevel.H;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private static getOptionString(options: Record<string, Object>, key: string, defaultValue: string): string {
|
|
1428
|
+
const val: Object | undefined = options[key];
|
|
1429
|
+
return val === undefined || val === null ? defaultValue : String(val);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
private static getOptionNumber(options: Record<string, Object>, key: string, defaultValue: number): number {
|
|
1433
|
+
const val: Object | undefined = options[key];
|
|
1434
|
+
return val === undefined || val === null ? defaultValue : Number(val);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
private static getOptionBoolean(options: Record<string, Object>, key: string, defaultValue: boolean): boolean {
|
|
1438
|
+
const val: Object | undefined = options[key];
|
|
1439
|
+
if (val === undefined || val === null) {
|
|
1440
|
+
return defaultValue;
|
|
1441
|
+
}
|
|
1442
|
+
return val as boolean;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
private static errMsg(e: Error): string {
|
|
1446
|
+
const msg: string = e.message;
|
|
1447
|
+
return msg !== undefined && msg !== null && msg.length > 0 ? msg : String(e);
|
|
1448
|
+
}
|
|
1449
|
+
}
|