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.
- package/LICENSE +1 -1
- package/README.md +375 -363
- package/dist/id-scanner-core.esm.js +427 -221
- package/dist/id-scanner-core.esm.js.map +1 -1
- package/dist/id-scanner-core.js +427 -221
- package/dist/id-scanner-core.js.map +1 -1
- package/dist/id-scanner-core.min.js +1 -9
- package/dist/id-scanner-core.min.js.map +1 -1
- package/dist/id-scanner-ocr.esm.js +451 -276
- package/dist/id-scanner-ocr.esm.js.map +1 -1
- package/dist/id-scanner-ocr.js +451 -276
- package/dist/id-scanner-ocr.js.map +1 -1
- package/dist/id-scanner-ocr.min.js +1 -9
- package/dist/id-scanner-ocr.min.js.map +1 -1
- package/dist/id-scanner-qr.esm.js +483 -233
- package/dist/id-scanner-qr.esm.js.map +1 -1
- package/dist/id-scanner-qr.js +482 -232
- package/dist/id-scanner-qr.js.map +1 -1
- package/dist/id-scanner-qr.min.js +1 -9
- package/dist/id-scanner-qr.min.js.map +1 -1
- package/dist/id-scanner.js +2138 -358
- package/dist/id-scanner.js.map +1 -1
- package/dist/id-scanner.min.js +1 -9
- package/dist/id-scanner.min.js.map +1 -1
- package/package.json +27 -7
- package/src/demo/demo.ts +178 -62
- package/src/id-recognition/anti-fake-detector.ts +317 -0
- package/src/id-recognition/id-detector.ts +184 -155
- package/src/id-recognition/ocr-processor.ts +193 -146
- package/src/id-recognition/ocr-worker.ts +82 -72
- package/src/index-umd.ts +347 -110
- package/src/index.ts +866 -91
- package/src/ocr-module.ts +108 -60
- package/src/qr-module.ts +104 -54
- package/src/scanner/barcode-scanner.ts +145 -58
- package/src/scanner/qr-scanner.ts +86 -47
- package/src/utils/image-processing.ts +479 -294
- package/dist/core.d.ts +0 -77
- package/dist/demo/demo.d.ts +0 -14
- package/dist/id-recognition/data-extractor.d.ts +0 -105
- package/dist/id-recognition/id-detector.d.ts +0 -100
- package/dist/id-recognition/ocr-processor.d.ts +0 -64
- package/dist/id-scanner.esm.js +0 -94656
- package/dist/id-scanner.esm.js.map +0 -1
- package/dist/index-umd.d.ts +0 -96
- package/dist/index.d.ts +0 -78
- package/dist/ocr-module.d.ts +0 -67
- package/dist/qr-module.d.ts +0 -68
- package/dist/scanner/barcode-scanner.d.ts +0 -90
- package/dist/scanner/qr-scanner.d.ts +0 -80
- package/dist/types/core.d.ts +0 -77
- package/dist/types/demo/demo.d.ts +0 -14
- package/dist/types/id-recognition/data-extractor.d.ts +0 -105
- package/dist/types/id-recognition/id-detector.d.ts +0 -100
- package/dist/types/id-recognition/ocr-processor.d.ts +0 -64
- package/dist/types/index-umd.d.ts +0 -96
- package/dist/types/index.d.ts +0 -78
- package/dist/types/ocr-module.d.ts +0 -67
- package/dist/types/qr-module.d.ts +0 -68
- package/dist/types/scanner/barcode-scanner.d.ts +0 -90
- package/dist/types/scanner/qr-scanner.d.ts +0 -80
- package/dist/types/utils/camera.d.ts +0 -81
- package/dist/types/utils/image-processing.d.ts +0 -75
- package/dist/types/utils/types.d.ts +0 -65
- package/dist/utils/camera.d.ts +0 -81
- package/dist/utils/image-processing.d.ts +0 -75
- 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.
|
|
5
|
+
* @version 1.3.0
|
|
6
6
|
* @license MIT
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { Camera, CameraOptions } from
|
|
10
|
-
import { IDCardInfo, DetectionResult } from
|
|
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
|
|
14
|
-
import type { BarcodeScannerOptions } from
|
|
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
|
-
|
|
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:
|
|
38
|
-
private videoElement: HTMLVideoElement | null = null
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
private
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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 {
|
|
214
|
-
export {
|
|
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
|
+
}
|