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.
- package/LICENSE +1 -1
- package/README.md +192 -375
- 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 +439 -265
- package/dist/id-scanner-ocr.esm.js.map +1 -1
- package/dist/id-scanner-ocr.js +439 -265
- 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 +1779 -357
- 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 +23 -5
- package/src/demo/demo.ts +178 -62
- 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 +2 -2
- package/src/index-umd.ts +347 -110
- package/src/index.ts +688 -87
- 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/src/utils/performance.ts +5 -3
- 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
|
@@ -6,53 +6,59 @@
|
|
|
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"
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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 {
|
|
214
|
-
export {
|
|
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
|
+
}
|