id-scanner-lib 1.2.0 → 1.3.0

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.
Files changed (67) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +192 -375
  3. package/dist/id-scanner-core.esm.js +427 -221
  4. package/dist/id-scanner-core.esm.js.map +1 -1
  5. package/dist/id-scanner-core.js +427 -221
  6. package/dist/id-scanner-core.js.map +1 -1
  7. package/dist/id-scanner-core.min.js +1 -9
  8. package/dist/id-scanner-core.min.js.map +1 -1
  9. package/dist/id-scanner-ocr.esm.js +439 -265
  10. package/dist/id-scanner-ocr.esm.js.map +1 -1
  11. package/dist/id-scanner-ocr.js +439 -265
  12. package/dist/id-scanner-ocr.js.map +1 -1
  13. package/dist/id-scanner-ocr.min.js +1 -9
  14. package/dist/id-scanner-ocr.min.js.map +1 -1
  15. package/dist/id-scanner-qr.esm.js +483 -233
  16. package/dist/id-scanner-qr.esm.js.map +1 -1
  17. package/dist/id-scanner-qr.js +482 -232
  18. package/dist/id-scanner-qr.js.map +1 -1
  19. package/dist/id-scanner-qr.min.js +1 -9
  20. package/dist/id-scanner-qr.min.js.map +1 -1
  21. package/dist/id-scanner.js +1779 -357
  22. package/dist/id-scanner.js.map +1 -1
  23. package/dist/id-scanner.min.js +1 -9
  24. package/dist/id-scanner.min.js.map +1 -1
  25. package/package.json +23 -5
  26. package/src/demo/demo.ts +178 -62
  27. package/src/id-recognition/id-detector.ts +184 -155
  28. package/src/id-recognition/ocr-processor.ts +193 -146
  29. package/src/id-recognition/ocr-worker.ts +2 -2
  30. package/src/index-umd.ts +347 -110
  31. package/src/index.ts +688 -87
  32. package/src/ocr-module.ts +108 -60
  33. package/src/qr-module.ts +104 -54
  34. package/src/scanner/barcode-scanner.ts +145 -58
  35. package/src/scanner/qr-scanner.ts +86 -47
  36. package/src/utils/image-processing.ts +479 -294
  37. package/src/utils/performance.ts +5 -3
  38. package/dist/core.d.ts +0 -77
  39. package/dist/demo/demo.d.ts +0 -14
  40. package/dist/id-recognition/data-extractor.d.ts +0 -105
  41. package/dist/id-recognition/id-detector.d.ts +0 -100
  42. package/dist/id-recognition/ocr-processor.d.ts +0 -64
  43. package/dist/id-scanner.esm.js +0 -94656
  44. package/dist/id-scanner.esm.js.map +0 -1
  45. package/dist/index-umd.d.ts +0 -96
  46. package/dist/index.d.ts +0 -78
  47. package/dist/ocr-module.d.ts +0 -67
  48. package/dist/qr-module.d.ts +0 -68
  49. package/dist/scanner/barcode-scanner.d.ts +0 -90
  50. package/dist/scanner/qr-scanner.d.ts +0 -80
  51. package/dist/types/core.d.ts +0 -77
  52. package/dist/types/demo/demo.d.ts +0 -14
  53. package/dist/types/id-recognition/data-extractor.d.ts +0 -105
  54. package/dist/types/id-recognition/id-detector.d.ts +0 -100
  55. package/dist/types/id-recognition/ocr-processor.d.ts +0 -64
  56. package/dist/types/index-umd.d.ts +0 -96
  57. package/dist/types/index.d.ts +0 -78
  58. package/dist/types/ocr-module.d.ts +0 -67
  59. package/dist/types/qr-module.d.ts +0 -68
  60. package/dist/types/scanner/barcode-scanner.d.ts +0 -90
  61. package/dist/types/scanner/qr-scanner.d.ts +0 -80
  62. package/dist/types/utils/camera.d.ts +0 -81
  63. package/dist/types/utils/image-processing.d.ts +0 -75
  64. package/dist/types/utils/types.d.ts +0 -65
  65. package/dist/utils/camera.d.ts +0 -81
  66. package/dist/utils/image-processing.d.ts +0 -75
  67. package/dist/utils/types.d.ts +0 -65
package/src/index.ts CHANGED
@@ -6,53 +6,59 @@
6
6
  * @license MIT
7
7
  */
8
8
 
9
- import { Camera, CameraOptions } from './utils/camera';
10
- import { IDCardInfo, DetectionResult } from './utils/types';
9
+ import { Camera, CameraOptions } from "./utils/camera"
10
+ import { IDCardInfo, DetectionResult } from "./utils/types"
11
+ import {
12
+ ImageProcessor,
13
+ ImageProcessorOptions,
14
+ ImageCompressionOptions,
15
+ } from "./utils/image-processing"
11
16
 
12
17
  // 先只导入类型定义,不导入实际实现
13
- import type { QRScannerOptions } from './scanner/qr-scanner';
14
- import type { BarcodeScannerOptions } from './scanner/barcode-scanner';
18
+ import type { QRScannerOptions } from "./scanner/qr-scanner"
19
+ import type { BarcodeScannerOptions } from "./scanner/barcode-scanner"
15
20
 
16
21
  /**
17
22
  * IDScanner配置选项接口
18
23
  */
19
24
  export interface IDScannerOptions {
20
- cameraOptions?: CameraOptions;
21
- qrScannerOptions?: QRScannerOptions;
22
- barcodeScannerOptions?: BarcodeScannerOptions;
23
- onQRCodeScanned?: (result: string) => void;
24
- onBarcodeScanned?: (result: string) => void;
25
- onIDCardScanned?: (info: IDCardInfo) => void;
26
- onError?: (error: Error) => void;
25
+ cameraOptions?: CameraOptions
26
+ qrScannerOptions?: QRScannerOptions
27
+ barcodeScannerOptions?: BarcodeScannerOptions
28
+ onQRCodeScanned?: (result: string) => void
29
+ onBarcodeScanned?: (result: string) => void
30
+ onIDCardScanned?: (info: IDCardInfo) => void
31
+ onImageProcessed?: (processedImage: ImageData | File) => void
32
+ onError?: (error: Error) => void
27
33
  }
28
34
 
29
35
  /**
30
36
  * IDScanner 主类
31
- *
37
+ *
32
38
  * 整合二维码、条形码扫描和身份证识别功能,提供统一的接口
33
39
  * 使用动态导入实现按需加载
34
40
  */
35
41
  export class IDScanner {
36
- private camera: Camera;
37
- private scanMode: 'qr' | 'barcode' | 'idcard' = 'qr';
38
- private videoElement: HTMLVideoElement | null = null;
39
-
42
+ private camera: Camera
43
+ private scanMode: "qr" | "barcode" | "idcard" = "qr"
44
+ private videoElement: HTMLVideoElement | null = null
45
+
40
46
  // 延迟加载的模块
41
- private qrModule: any = null;
42
- private ocrModule: any = null;
43
-
47
+ private qrModule: any = null
48
+ private ocrModule: any = null
49
+
44
50
  // 模块加载状态
45
- private isQRModuleLoaded: boolean = false;
46
- private isOCRModuleLoaded: boolean = false;
47
-
51
+ private isQRModuleLoaded: boolean = false
52
+ private isOCRModuleLoaded: boolean = false
53
+
48
54
  /**
49
55
  * 构造函数
50
56
  * @param options 配置选项
51
57
  */
52
58
  constructor(private options: IDScannerOptions = {}) {
53
- this.camera = new Camera(options.cameraOptions);
59
+ this.camera = new Camera(options.cameraOptions)
54
60
  }
55
-
61
+
56
62
  /**
57
63
  * 初始化模块
58
64
  * 根据需要初始化OCR引擎
@@ -62,153 +68,748 @@ export class IDScanner {
62
68
  // 预加载OCR模块但不初始化
63
69
  if (!this.isOCRModuleLoaded) {
64
70
  // 动态导入OCR模块
65
- const OCRModule = await import('./ocr-module').then(m => m.OCRModule);
71
+ const OCRModule = await import("./ocr-module").then((m) => m.OCRModule)
66
72
  this.ocrModule = new OCRModule({
67
73
  cameraOptions: this.options.cameraOptions,
68
74
  onIDCardScanned: this.options.onIDCardScanned,
69
- onError: this.options.onError
70
- });
71
- this.isOCRModuleLoaded = true;
72
-
75
+ onError: this.options.onError,
76
+ })
77
+ this.isOCRModuleLoaded = true
78
+
73
79
  // 初始化OCR模块
74
- await this.ocrModule.initialize();
80
+ await this.ocrModule.initialize()
75
81
  }
76
-
77
- console.log('IDScanner initialized');
82
+
83
+ console.log("IDScanner initialized")
78
84
  } catch (error) {
79
- this.handleError(error as Error);
80
- throw error;
85
+ this.handleError(error as Error)
86
+ throw error
81
87
  }
82
88
  }
83
-
89
+
84
90
  /**
85
91
  * 启动二维码扫描
86
92
  * @param videoElement HTML视频元素
87
93
  */
88
94
  async startQRScanner(videoElement: HTMLVideoElement): Promise<void> {
89
- this.stop();
90
- this.videoElement = videoElement;
91
- this.scanMode = 'qr';
92
-
95
+ this.stop()
96
+ this.videoElement = videoElement
97
+ this.scanMode = "qr"
98
+
93
99
  try {
94
100
  // 动态加载二维码模块
95
101
  if (!this.isQRModuleLoaded) {
96
- const ScannerModule = await import('./qr-module').then(m => m.ScannerModule);
102
+ const ScannerModule = await import("./qr-module").then(
103
+ (m) => m.ScannerModule
104
+ )
97
105
  this.qrModule = new ScannerModule({
98
106
  cameraOptions: this.options.cameraOptions,
99
107
  qrScannerOptions: this.options.qrScannerOptions,
100
108
  barcodeScannerOptions: this.options.barcodeScannerOptions,
101
109
  onQRCodeScanned: this.options.onQRCodeScanned,
102
110
  onBarcodeScanned: this.options.onBarcodeScanned,
103
- onError: this.options.onError
104
- });
105
- this.isQRModuleLoaded = true;
111
+ onError: this.options.onError,
112
+ })
113
+ this.isQRModuleLoaded = true
106
114
  }
107
-
108
- await this.qrModule.startQRScanner(videoElement);
115
+
116
+ await this.qrModule.startQRScanner(videoElement)
109
117
  } catch (error) {
110
- this.handleError(error as Error);
118
+ this.handleError(error as Error)
111
119
  }
112
120
  }
113
-
121
+
114
122
  /**
115
123
  * 启动条形码扫描
116
124
  * @param videoElement HTML视频元素
117
125
  */
118
126
  async startBarcodeScanner(videoElement: HTMLVideoElement): Promise<void> {
119
- this.stop();
120
- this.videoElement = videoElement;
121
- this.scanMode = 'barcode';
122
-
127
+ this.stop()
128
+ this.videoElement = videoElement
129
+ this.scanMode = "barcode"
130
+
123
131
  try {
124
132
  // 动态加载二维码模块
125
133
  if (!this.isQRModuleLoaded) {
126
- const ScannerModule = await import('./qr-module').then(m => m.ScannerModule);
134
+ const ScannerModule = await import("./qr-module").then(
135
+ (m) => m.ScannerModule
136
+ )
127
137
  this.qrModule = new ScannerModule({
128
138
  cameraOptions: this.options.cameraOptions,
129
139
  qrScannerOptions: this.options.qrScannerOptions,
130
140
  barcodeScannerOptions: this.options.barcodeScannerOptions,
131
141
  onQRCodeScanned: this.options.onQRCodeScanned,
132
142
  onBarcodeScanned: this.options.onBarcodeScanned,
133
- onError: this.options.onError
134
- });
135
- this.isQRModuleLoaded = true;
143
+ onError: this.options.onError,
144
+ })
145
+ this.isQRModuleLoaded = true
136
146
  }
137
-
138
- await this.qrModule.startBarcodeScanner(videoElement);
147
+
148
+ await this.qrModule.startBarcodeScanner(videoElement)
139
149
  } catch (error) {
140
- this.handleError(error as Error);
150
+ this.handleError(error as Error)
141
151
  }
142
152
  }
143
-
153
+
144
154
  /**
145
155
  * 启动身份证扫描
146
156
  * @param videoElement HTML视频元素
147
157
  */
148
158
  async startIDCardScanner(videoElement: HTMLVideoElement): Promise<void> {
149
- this.stop();
150
- this.videoElement = videoElement;
151
- this.scanMode = 'idcard';
152
-
159
+ this.stop()
160
+ this.videoElement = videoElement
161
+ this.scanMode = "idcard"
162
+
153
163
  try {
154
164
  // 检查OCR模块是否已加载,若未加载则自动初始化
155
165
  if (!this.isOCRModuleLoaded) {
156
- await this.initialize();
166
+ await this.initialize()
157
167
  }
158
-
159
- await this.ocrModule.startIDCardScanner(videoElement);
168
+
169
+ await this.ocrModule.startIDCardScanner(videoElement)
160
170
  } catch (error) {
161
- this.handleError(error as Error);
171
+ this.handleError(error as Error)
162
172
  }
163
173
  }
164
-
174
+
165
175
  /**
166
176
  * 停止扫描
167
177
  */
168
178
  stop(): void {
169
- if (this.scanMode === 'qr' || this.scanMode === 'barcode') {
179
+ if (this.scanMode === "qr" || this.scanMode === "barcode") {
170
180
  if (this.qrModule) {
171
- this.qrModule.stop();
181
+ this.qrModule.stop()
172
182
  }
173
- } else if (this.scanMode === 'idcard') {
183
+ } else if (this.scanMode === "idcard") {
174
184
  if (this.ocrModule) {
175
- this.ocrModule.stop();
185
+ this.ocrModule.stop()
176
186
  }
177
187
  }
178
188
  }
179
-
189
+
180
190
  /**
181
191
  * 处理错误
182
192
  */
183
193
  private handleError(error: Error): void {
184
194
  if (this.options.onError) {
185
- this.options.onError(error);
195
+ this.options.onError(error)
186
196
  } else {
187
- console.error('IDScanner error:', error);
197
+ console.error("IDScanner error:", error)
188
198
  }
189
199
  }
190
-
200
+
191
201
  /**
192
202
  * 释放资源
193
203
  */
194
204
  async terminate(): Promise<void> {
195
- this.stop();
196
-
205
+ this.stop()
206
+
197
207
  // 释放OCR资源
198
208
  if (this.isOCRModuleLoaded && this.ocrModule) {
199
- await this.ocrModule.terminate();
200
- this.ocrModule = null;
201
- this.isOCRModuleLoaded = false;
209
+ await this.ocrModule.terminate()
210
+ this.ocrModule = null
211
+ this.isOCRModuleLoaded = false
202
212
  }
203
-
213
+
204
214
  // 释放QR扫描资源
205
215
  if (this.isQRModuleLoaded && this.qrModule) {
206
- this.qrModule = null;
207
- this.isQRModuleLoaded = false;
216
+ this.qrModule = null
217
+ this.isQRModuleLoaded = false
218
+ }
219
+ }
220
+
221
+ /**
222
+ * 处理图片中的二维码
223
+ * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
224
+ * @returns 返回Promise,解析为扫描结果
225
+ */
226
+ async processQRCodeImage(
227
+ imageSource: HTMLImageElement | HTMLCanvasElement | string | File
228
+ ): Promise<string> {
229
+ try {
230
+ // 动态加载二维码模块
231
+ if (!this.isQRModuleLoaded) {
232
+ const ScannerModule = await import("./qr-module").then(
233
+ (m) => m.ScannerModule
234
+ )
235
+ this.qrModule = new ScannerModule({
236
+ qrScannerOptions: this.options.qrScannerOptions,
237
+ onQRCodeScanned: this.options.onQRCodeScanned,
238
+ onError: this.options.onError,
239
+ })
240
+ this.isQRModuleLoaded = true
241
+ }
242
+
243
+ // 处理不同类型的图片源
244
+ let imageElement: HTMLImageElement
245
+
246
+ if (imageSource instanceof File) {
247
+ // 如果是File对象,创建新的Image元素并加载图片
248
+ imageElement = new Image()
249
+ imageElement.crossOrigin = "anonymous" // 处理跨域图片
250
+ const url = URL.createObjectURL(imageSource)
251
+ await new Promise((resolve, reject) => {
252
+ imageElement.onload = resolve
253
+ imageElement.onerror = reject
254
+ imageElement.src = url
255
+ })
256
+ // 使用后释放URL对象
257
+ URL.revokeObjectURL(url)
258
+ } else if (typeof imageSource === "string") {
259
+ // 如果是URL字符串,创建新的Image元素并加载图片
260
+ imageElement = new Image()
261
+ imageElement.crossOrigin = "anonymous" // 处理跨域图片
262
+ await new Promise((resolve, reject) => {
263
+ imageElement.onload = resolve
264
+ imageElement.onerror = reject
265
+ imageElement.src = imageSource
266
+ })
267
+ } else if (imageSource instanceof HTMLImageElement) {
268
+ // 如果已经是Image元素,直接使用
269
+ imageElement = imageSource
270
+ } else if (imageSource instanceof HTMLCanvasElement) {
271
+ // 如果是Canvas元素,创建Image并从Canvas获取数据
272
+ imageElement = new Image()
273
+ imageElement.src = imageSource.toDataURL()
274
+ await new Promise((resolve) => {
275
+ imageElement.onload = resolve
276
+ })
277
+ } else {
278
+ throw new Error("不支持的图片源类型")
279
+ }
280
+
281
+ // 获取图像数据
282
+ const canvas = document.createElement("canvas")
283
+ canvas.width = imageElement.naturalWidth
284
+ canvas.height = imageElement.naturalHeight
285
+ const ctx = canvas.getContext("2d")
286
+
287
+ if (!ctx) {
288
+ throw new Error("无法创建Canvas上下文")
289
+ }
290
+
291
+ ctx.drawImage(imageElement, 0, 0)
292
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
293
+
294
+ // 使用QR模块处理图像
295
+ return this.qrModule.processQRCodeImage(imageData)
296
+ } catch (error) {
297
+ this.handleError(error as Error)
298
+ throw error
299
+ }
300
+ }
301
+
302
+ /**
303
+ * 处理图片中的条形码
304
+ * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
305
+ * @returns 返回Promise,解析为扫描结果
306
+ */
307
+ async processBarcodeImage(
308
+ imageSource: HTMLImageElement | HTMLCanvasElement | string | File
309
+ ): Promise<string> {
310
+ try {
311
+ // 动态加载二维码模块
312
+ if (!this.isQRModuleLoaded) {
313
+ const ScannerModule = await import("./qr-module").then(
314
+ (m) => m.ScannerModule
315
+ )
316
+ this.qrModule = new ScannerModule({
317
+ barcodeScannerOptions: this.options.barcodeScannerOptions,
318
+ onBarcodeScanned: this.options.onBarcodeScanned,
319
+ onError: this.options.onError,
320
+ })
321
+ this.isQRModuleLoaded = true
322
+ }
323
+
324
+ // 处理不同类型的图片源
325
+ let imageElement: HTMLImageElement
326
+
327
+ if (imageSource instanceof File) {
328
+ // 如果是File对象,创建新的Image元素并加载图片
329
+ imageElement = new Image()
330
+ imageElement.crossOrigin = "anonymous" // 处理跨域图片
331
+ const url = URL.createObjectURL(imageSource)
332
+ await new Promise((resolve, reject) => {
333
+ imageElement.onload = resolve
334
+ imageElement.onerror = reject
335
+ imageElement.src = url
336
+ })
337
+ // 使用后释放URL对象
338
+ URL.revokeObjectURL(url)
339
+ } else if (typeof imageSource === "string") {
340
+ // 如果是URL字符串,创建新的Image元素并加载图片
341
+ imageElement = new Image()
342
+ imageElement.crossOrigin = "anonymous" // 处理跨域图片
343
+ await new Promise((resolve, reject) => {
344
+ imageElement.onload = resolve
345
+ imageElement.onerror = reject
346
+ imageElement.src = imageSource
347
+ })
348
+ } else if (imageSource instanceof HTMLImageElement) {
349
+ // 如果已经是Image元素,直接使用
350
+ imageElement = imageSource
351
+ } else if (imageSource instanceof HTMLCanvasElement) {
352
+ // 如果是Canvas元素,创建Image并从Canvas获取数据
353
+ imageElement = new Image()
354
+ imageElement.src = imageSource.toDataURL()
355
+ await new Promise((resolve) => {
356
+ imageElement.onload = resolve
357
+ })
358
+ } else {
359
+ throw new Error("不支持的图片源类型")
360
+ }
361
+
362
+ // 获取图像数据
363
+ const canvas = document.createElement("canvas")
364
+ canvas.width = imageElement.naturalWidth
365
+ canvas.height = imageElement.naturalHeight
366
+ const ctx = canvas.getContext("2d")
367
+
368
+ if (!ctx) {
369
+ throw new Error("无法创建Canvas上下文")
370
+ }
371
+
372
+ ctx.drawImage(imageElement, 0, 0)
373
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
374
+
375
+ // 使用Barcode模块处理图像
376
+ return this.qrModule.processBarcodeImage(imageData)
377
+ } catch (error) {
378
+ this.handleError(error as Error)
379
+ throw error
380
+ }
381
+ }
382
+
383
+ /**
384
+ * 处理图片中的身份证
385
+ * @param imageSource 图片源,可以是Image元素、Canvas元素或URL字符串
386
+ * @returns 返回Promise,解析为身份证信息
387
+ */
388
+ async processIDCardImage(
389
+ imageSource: HTMLImageElement | HTMLCanvasElement | string | File
390
+ ): Promise<IDCardInfo> {
391
+ try {
392
+ // 检查OCR模块是否已加载,若未加载则自动初始化
393
+ if (!this.isOCRModuleLoaded) {
394
+ await this.initialize()
395
+ }
396
+
397
+ // 处理不同类型的图片源
398
+ let imageElement: HTMLImageElement
399
+
400
+ if (imageSource instanceof File) {
401
+ // 如果是File对象,先进行压缩
402
+ const compressedFile = await ImageProcessor.compressImage(imageSource, {
403
+ maxSizeMB: 2, // 最大2MB
404
+ maxWidthOrHeight: 1800, // 最大尺寸
405
+ useWebWorker: true,
406
+ })
407
+
408
+ // 创建新的Image元素并加载图片
409
+ imageElement = new Image()
410
+ imageElement.crossOrigin = "anonymous" // 处理跨域图片
411
+ const url = URL.createObjectURL(compressedFile)
412
+ await new Promise((resolve, reject) => {
413
+ imageElement.onload = resolve
414
+ imageElement.onerror = reject
415
+ imageElement.src = url
416
+ })
417
+ // 使用后释放URL对象
418
+ URL.revokeObjectURL(url)
419
+ } else if (typeof imageSource === "string") {
420
+ // 如果是URL字符串,创建新的Image元素并加载图片
421
+ imageElement = new Image()
422
+ imageElement.crossOrigin = "anonymous" // 处理跨域图片
423
+ await new Promise((resolve, reject) => {
424
+ imageElement.onload = resolve
425
+ imageElement.onerror = reject
426
+ imageElement.src = imageSource
427
+ })
428
+ } else if (imageSource instanceof HTMLImageElement) {
429
+ // 如果已经是Image元素,直接使用
430
+ imageElement = imageSource
431
+ } else if (imageSource instanceof HTMLCanvasElement) {
432
+ // 如果是Canvas元素,创建Image并从Canvas获取数据
433
+ imageElement = new Image()
434
+ imageElement.src = imageSource.toDataURL()
435
+ await new Promise((resolve) => {
436
+ imageElement.onload = resolve
437
+ })
438
+ } else {
439
+ throw new Error("不支持的图片源类型")
440
+ }
441
+
442
+ // 获取图像数据
443
+ const canvas = document.createElement("canvas")
444
+ canvas.width = imageElement.naturalWidth
445
+ canvas.height = imageElement.naturalHeight
446
+ const ctx = canvas.getContext("2d")
447
+
448
+ if (!ctx) {
449
+ throw new Error("无法创建Canvas上下文")
450
+ }
451
+
452
+ ctx.drawImage(imageElement, 0, 0)
453
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
454
+
455
+ // 对图像进行预处理,提高识别率
456
+ const enhancedImageData = ImageProcessor.batchProcess(imageData, {
457
+ brightness: 10,
458
+ contrast: 15,
459
+ sharpen: true,
460
+ })
461
+
462
+ // 使用OCR模块处理图像
463
+ return this.ocrModule.processIDCard(enhancedImageData)
464
+ } catch (error) {
465
+ this.handleError(error as Error)
466
+ throw error
467
+ }
468
+ }
469
+
470
+ /**
471
+ * 批量处理图像
472
+ * @param imageSource 图片源,可以是Image元素、Canvas元素、URL字符串或File对象
473
+ * @param options 图像处理选项
474
+ * @param outputFormat 输出格式,'imagedata'或'file'
475
+ * @returns 返回Promise,解析为处理后的ImageData或File
476
+ */
477
+ async processImage(
478
+ imageSource: HTMLImageElement | HTMLCanvasElement | string | File,
479
+ options: ImageProcessorOptions = {},
480
+ outputFormat: "imagedata" | "file" = "imagedata"
481
+ ): Promise<ImageData | File> {
482
+ try {
483
+ // 处理不同类型的图片源
484
+ let imageData: ImageData
485
+
486
+ if (imageSource instanceof File) {
487
+ // 如果是File对象,先进行压缩
488
+ const compressedFile = await ImageProcessor.compressImage(imageSource, {
489
+ maxSizeMB: 2, // 最大2MB
490
+ maxWidthOrHeight: 1920, // 最大尺寸
491
+ useWebWorker: true,
492
+ })
493
+
494
+ // 从File创建ImageData
495
+ imageData = await ImageProcessor.createImageDataFromFile(compressedFile)
496
+ } else if (typeof imageSource === "string") {
497
+ // 如果是URL字符串,创建新的Image元素并加载图片
498
+ const imageElement = new Image()
499
+ imageElement.crossOrigin = "anonymous" // 处理跨域图片
500
+ await new Promise((resolve, reject) => {
501
+ imageElement.onload = resolve
502
+ imageElement.onerror = reject
503
+ imageElement.src = imageSource
504
+ })
505
+
506
+ // 获取图像数据
507
+ const canvas = document.createElement("canvas")
508
+ canvas.width = imageElement.naturalWidth
509
+ canvas.height = imageElement.naturalHeight
510
+ const ctx = canvas.getContext("2d")
511
+
512
+ if (!ctx) {
513
+ throw new Error("无法创建Canvas上下文")
514
+ }
515
+
516
+ ctx.drawImage(imageElement, 0, 0)
517
+ imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
518
+ } else if (imageSource instanceof HTMLImageElement) {
519
+ // 如果是Image元素,从它创建ImageData
520
+ const canvas = document.createElement("canvas")
521
+ canvas.width = imageSource.naturalWidth
522
+ canvas.height = imageSource.naturalHeight
523
+ const ctx = canvas.getContext("2d")
524
+
525
+ if (!ctx) {
526
+ throw new Error("无法创建Canvas上下文")
527
+ }
528
+
529
+ ctx.drawImage(imageSource, 0, 0)
530
+ imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
531
+ } else if (imageSource instanceof HTMLCanvasElement) {
532
+ // 如果是Canvas元素,直接获取其ImageData
533
+ const ctx = imageSource.getContext("2d")
534
+
535
+ if (!ctx) {
536
+ throw new Error("无法获取Canvas上下文")
537
+ }
538
+
539
+ imageData = ctx.getImageData(
540
+ 0,
541
+ 0,
542
+ imageSource.width,
543
+ imageSource.height
544
+ )
545
+ } else {
546
+ throw new Error("不支持的图片源类型")
547
+ }
548
+
549
+ // 进行图像处理
550
+ const processedImageData = ImageProcessor.batchProcess(imageData, options)
551
+
552
+ // 根据需要的输出格式返回结果
553
+ if (outputFormat === "file") {
554
+ // 将ImageData转换为File
555
+ const file = await ImageProcessor.imageDataToFile(
556
+ processedImageData,
557
+ "processed_image.jpg",
558
+ "image/jpeg",
559
+ 0.85
560
+ )
561
+
562
+ // 触发回调
563
+ if (this.options.onImageProcessed) {
564
+ this.options.onImageProcessed(file)
565
+ }
566
+
567
+ return file
568
+ } else {
569
+ // 触发回调
570
+ if (this.options.onImageProcessed) {
571
+ this.options.onImageProcessed(processedImageData)
572
+ }
573
+
574
+ return processedImageData
575
+ }
576
+ } catch (error) {
577
+ this.handleError(error as Error)
578
+ throw error
579
+ }
580
+ }
581
+
582
+ /**
583
+ * 压缩图片
584
+ * @param file 要压缩的图片文件
585
+ * @param options 压缩选项
586
+ * @returns 返回Promise,解析为压缩后的文件
587
+ */
588
+ async compressImage(
589
+ file: File,
590
+ options?: ImageCompressionOptions
591
+ ): Promise<File> {
592
+ try {
593
+ return await ImageProcessor.compressImage(file, options)
594
+ } catch (error) {
595
+ this.handleError(error as Error)
596
+ throw error
208
597
  }
209
598
  }
210
599
  }
211
600
 
212
- // 导出核心类型
213
- export { IDCardInfo } from './utils/types';
214
- export { CameraOptions } from './utils/camera';
601
+ // 导出工具类和类型
602
+ export { Camera, CameraOptions } from "./utils/camera"
603
+ export {
604
+ ImageProcessor,
605
+ ImageProcessorOptions,
606
+ ImageCompressionOptions,
607
+ } from "./utils/image-processing"
608
+ export { IDCardInfo, DetectionResult } from "./utils/types"
609
+
610
+ // 为了向后兼容,我们创建一个演示类
611
+ export class IDScannerDemo {
612
+ private scanner: IDScanner
613
+ private currentMode: "qr" | "idcard" = "qr"
614
+ private videoElement: HTMLVideoElement
615
+ private resultElement: HTMLElement
616
+
617
+ /**
618
+ * 创建演示类实例
619
+ * @param videoElementId 视频元素ID
620
+ * @param resultElementId 结果显示元素ID
621
+ * @param switchButtonId 切换按钮ID
622
+ * @param imageInputId 图片输入元素ID
623
+ */
624
+ constructor(
625
+ videoElementId: string,
626
+ resultElementId: string,
627
+ switchButtonId?: string,
628
+ imageInputId?: string
629
+ ) {
630
+ this.videoElement = document.getElementById(
631
+ videoElementId
632
+ ) as HTMLVideoElement
633
+ this.resultElement = document.getElementById(resultElementId) as HTMLElement
634
+
635
+ // 创建扫描器实例
636
+ this.scanner = new IDScanner({
637
+ onQRCodeScanned: (result) => this.handleScanResult(result),
638
+ onIDCardScanned: (info) => this.handleIDCardResult(info),
639
+ onError: (error) => this.handleError(error),
640
+ })
641
+
642
+ // 设置切换按钮事件
643
+ if (switchButtonId) {
644
+ const switchButton = document.getElementById(switchButtonId)
645
+ if (switchButton) {
646
+ switchButton.addEventListener("click", () => this.toggleMode())
647
+ }
648
+ }
649
+
650
+ // 设置图片输入事件
651
+ if (imageInputId) {
652
+ const imageInput = document.getElementById(
653
+ imageInputId
654
+ ) as HTMLInputElement
655
+ if (imageInput) {
656
+ imageInput.addEventListener("change", (e) => this.handleImageInput(e))
657
+ }
658
+ }
659
+ }
660
+
661
+ /**
662
+ * 初始化扫描器
663
+ */
664
+ async initialize(): Promise<void> {
665
+ try {
666
+ // 初始化身份证识别引擎
667
+ await this.scanner.initialize()
668
+
669
+ // 默认启动二维码扫描
670
+ await this.startQRMode()
671
+ } catch (error) {
672
+ this.handleError(error as Error)
673
+ }
674
+ }
675
+
676
+ /**
677
+ * 切换扫描模式
678
+ */
679
+ async toggleMode(): Promise<void> {
680
+ try {
681
+ this.scanner.stop()
682
+
683
+ if (this.currentMode === "qr") {
684
+ this.currentMode = "idcard"
685
+ await this.startIDCardMode()
686
+ } else {
687
+ this.currentMode = "qr"
688
+ await this.startQRMode()
689
+ }
690
+ } catch (error) {
691
+ this.handleError(error as Error)
692
+ }
693
+ }
694
+
695
+ /**
696
+ * 启动二维码扫描模式
697
+ */
698
+ private async startQRMode(): Promise<void> {
699
+ try {
700
+ this.updateResultDisplay("等待扫描二维码...")
701
+ await this.scanner.startQRScanner(this.videoElement)
702
+ } catch (error) {
703
+ this.handleError(error as Error)
704
+ }
705
+ }
706
+
707
+ /**
708
+ * 启动身份证扫描模式
709
+ */
710
+ private async startIDCardMode(): Promise<void> {
711
+ try {
712
+ this.updateResultDisplay("等待扫描身份证...")
713
+ await this.scanner.startIDCardScanner(this.videoElement)
714
+ } catch (error) {
715
+ this.handleError(error as Error)
716
+ }
717
+ }
718
+
719
+ /**
720
+ * 处理图片输入
721
+ */
722
+ private async handleImageInput(event: Event): Promise<void> {
723
+ try {
724
+ const input = event.target as HTMLInputElement
725
+
726
+ if (!input.files || input.files.length === 0) {
727
+ return
728
+ }
729
+
730
+ const file = input.files[0]
731
+ this.updateResultDisplay("正在处理图片...")
732
+
733
+ // 根据当前模式处理图片
734
+ if (this.currentMode === "qr") {
735
+ const result = await this.scanner.processQRCodeImage(file)
736
+ this.handleScanResult(result)
737
+ } else {
738
+ const info = await this.scanner.processIDCardImage(file)
739
+ this.handleIDCardResult(info)
740
+ }
741
+ } catch (error) {
742
+ this.handleError(error as Error)
743
+ }
744
+ }
745
+
746
+ /**
747
+ * 处理扫描结果
748
+ */
749
+ private handleScanResult(result: string): void {
750
+ this.updateResultDisplay(`
751
+ <h3>扫描结果:</h3>
752
+ <p>${result}</p>
753
+ `)
754
+ }
755
+
756
+ /**
757
+ * 处理身份证识别结果
758
+ */
759
+ private handleIDCardResult(info: IDCardInfo): void {
760
+ // 格式化显示身份证信息
761
+ const infoHtml = Object.entries(info)
762
+ .filter(([key, value]) => value) // 过滤掉空值
763
+ .map(([key, value]) => {
764
+ // 转换键名为中文显示
765
+ const keyMap: { [key: string]: string } = {
766
+ name: "姓名",
767
+ gender: "性别",
768
+ nationality: "民族",
769
+ birthDate: "出生日期",
770
+ address: "地址",
771
+ idNumber: "身份证号",
772
+ issuingAuthority: "签发机关",
773
+ validPeriod: "有效期限",
774
+ }
775
+
776
+ const displayKey = keyMap[key] || key
777
+ return `<div><strong>${displayKey}:</strong> ${value}</div>`
778
+ })
779
+ .join("")
780
+
781
+ this.updateResultDisplay(`
782
+ <h3>身份证信息:</h3>
783
+ ${infoHtml}
784
+ `)
785
+ }
786
+
787
+ /**
788
+ * 处理错误
789
+ */
790
+ private handleError(error: Error): void {
791
+ console.error("识别错误:", error)
792
+ this.updateResultDisplay(`
793
+ <div class="error">
794
+ <h3>错误:</h3>
795
+ <p>${error.message}</p>
796
+ </div>
797
+ `)
798
+ }
799
+
800
+ /**
801
+ * 更新结果显示
802
+ */
803
+ private updateResultDisplay(html: string): void {
804
+ if (this.resultElement) {
805
+ this.resultElement.innerHTML = html
806
+ }
807
+ }
808
+
809
+ /**
810
+ * 停止扫描
811
+ */
812
+ stop(): void {
813
+ this.scanner.stop()
814
+ }
815
+ }