id-scanner-lib 1.2.2 → 1.3.2

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