id-scanner-lib 1.3.0 → 1.3.3
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/README.md +223 -25
- package/dist/id-scanner-core.esm.js +273 -0
- package/dist/id-scanner-core.js +273 -0
- package/dist/id-scanner-core.min.js +1 -1
- package/dist/id-scanner-ocr.esm.js +596 -76
- package/dist/id-scanner-ocr.js +596 -76
- package/dist/id-scanner-ocr.min.js +1 -1
- package/dist/id-scanner-qr.esm.js +273 -0
- package/dist/id-scanner-qr.js +273 -0
- package/dist/id-scanner-qr.min.js +1 -1
- package/dist/id-scanner.js +1268 -87
- package/dist/id-scanner.min.js +1 -1
- package/package.json +2 -2
- package/src/id-recognition/anti-fake-detector.ts +698 -0
- package/src/id-recognition/id-detector.ts +166 -19
- package/src/id-recognition/ocr-processor.ts +188 -41
- package/src/id-recognition/ocr-worker.ts +82 -72
- package/src/index.ts +189 -15
- package/src/types/browser-image-compression.d.ts +19 -0
- package/src/types/tesseract.d.ts +37 -0
- package/src/utils/image-processing.ts +364 -0
- package/dist/id-scanner-core.esm.js.map +0 -1
- package/dist/id-scanner-core.js.map +0 -1
- package/dist/id-scanner-core.min.js.map +0 -1
- package/dist/id-scanner-ocr.esm.js.map +0 -1
- package/dist/id-scanner-ocr.js.map +0 -1
- package/dist/id-scanner-ocr.min.js.map +0 -1
- package/dist/id-scanner-qr.esm.js.map +0 -1
- package/dist/id-scanner-qr.js.map +0 -1
- package/dist/id-scanner-qr.min.js.map +0 -1
- package/dist/id-scanner.js.map +0 -1
- package/dist/id-scanner.min.js.map +0 -1
|
@@ -4,143 +4,153 @@
|
|
|
4
4
|
* @module OCRWorker
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { IDCardInfo } from
|
|
7
|
+
import type { IDCardInfo } from "../utils/types"
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* OCR处理输入接口
|
|
11
11
|
*/
|
|
12
12
|
export interface OCRProcessInput {
|
|
13
|
-
imageBase64: string
|
|
14
|
-
tessWorkerOptions?: any
|
|
13
|
+
imageBase64: string
|
|
14
|
+
tessWorkerOptions?: any
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* OCR处理输出接口
|
|
19
19
|
*/
|
|
20
20
|
export interface OCRProcessOutput {
|
|
21
|
-
idCardInfo: IDCardInfo
|
|
22
|
-
processingTime: number
|
|
21
|
+
idCardInfo: IDCardInfo
|
|
22
|
+
processingTime: number
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* 在Web Worker中执行OCR处理的函数
|
|
27
|
-
*
|
|
27
|
+
*
|
|
28
28
|
* 该函数用于在使用 createWorker 创建的 Worker 中执行
|
|
29
|
-
*
|
|
29
|
+
*
|
|
30
30
|
* @param input OCR处理输入数据
|
|
31
31
|
* @returns OCR处理结果
|
|
32
32
|
*/
|
|
33
|
-
export async function processOCRInWorker(
|
|
33
|
+
export async function processOCRInWorker(
|
|
34
|
+
input: OCRProcessInput
|
|
35
|
+
): Promise<OCRProcessOutput> {
|
|
34
36
|
// 计时开始
|
|
35
|
-
const startTime = performance.now()
|
|
36
|
-
|
|
37
|
+
const startTime = performance.now()
|
|
38
|
+
|
|
37
39
|
// 加载Tesseract.js (Worker 环境下动态导入)
|
|
38
|
-
const { createWorker } = await import(
|
|
39
|
-
|
|
40
|
+
const { createWorker } = await import("tesseract.js")
|
|
41
|
+
|
|
40
42
|
// 创建OCR Worker
|
|
41
|
-
const worker = await createWorker(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const worker = (await createWorker(
|
|
44
|
+
input.tessWorkerOptions || {
|
|
45
|
+
logger: (m: any) => console.log(m),
|
|
46
|
+
}
|
|
47
|
+
)) as any // 添加类型断言,避免TypeScript错误
|
|
48
|
+
|
|
45
49
|
try {
|
|
46
50
|
// 初始化OCR引擎
|
|
47
|
-
await worker.load()
|
|
48
|
-
await worker.loadLanguage(
|
|
49
|
-
await worker.initialize(
|
|
51
|
+
await worker.load()
|
|
52
|
+
await worker.loadLanguage("chi_sim")
|
|
53
|
+
await worker.initialize("chi_sim")
|
|
50
54
|
await worker.setParameters({
|
|
51
|
-
tessedit_char_whitelist:
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
tessedit_char_whitelist:
|
|
56
|
+
"0123456789X-年月日一二三四五六七八九十零壹贰叁肆伍陆柒捌玖拾ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz民族汉族满族回族维吾尔族藏族苗族彝族壮族朝鲜族侗族瑶族白族土家族哈尼族哈萨克族傣族黎族傈僳族佤族高山族拉祜族水族东乡族钠西族景颇族柯尔克孜族士族达斡尔族仫佬族羌族布朗族撒拉族毛南族仡佬族锡伯族阿昌族普米族塔吉克族怒族乌孜别克族俄罗斯族鄂温克族德昂族保安族裕固族京族塔塔尔族独龙族鄂伦春族赫哲族门巴族珞巴族基诺族男女性别住址出生公民身份号码签发机关有效期",
|
|
57
|
+
})
|
|
58
|
+
|
|
54
59
|
// 识别图像
|
|
55
|
-
const { data } = await worker.recognize(input.imageBase64)
|
|
56
|
-
|
|
60
|
+
const { data } = await worker.recognize(input.imageBase64)
|
|
61
|
+
|
|
57
62
|
// 解析识别结果
|
|
58
|
-
const idCardInfo = parseIDCardText(data.text)
|
|
59
|
-
|
|
63
|
+
const idCardInfo = parseIDCardText(data.text)
|
|
64
|
+
|
|
60
65
|
// 处理完成后终止worker
|
|
61
|
-
await worker.terminate()
|
|
62
|
-
|
|
66
|
+
await worker.terminate()
|
|
67
|
+
|
|
63
68
|
// 计算处理时间
|
|
64
|
-
const processingTime = performance.now() - startTime
|
|
65
|
-
|
|
69
|
+
const processingTime = performance.now() - startTime
|
|
70
|
+
|
|
66
71
|
// 返回处理结果
|
|
67
72
|
return {
|
|
68
73
|
idCardInfo,
|
|
69
|
-
processingTime
|
|
70
|
-
}
|
|
74
|
+
processingTime,
|
|
75
|
+
}
|
|
71
76
|
} catch (error) {
|
|
72
77
|
// 确保资源被释放
|
|
73
|
-
await worker.terminate()
|
|
74
|
-
throw error
|
|
78
|
+
await worker.terminate()
|
|
79
|
+
throw error
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
/**
|
|
79
84
|
* 解析身份证文本信息
|
|
80
|
-
*
|
|
85
|
+
*
|
|
81
86
|
* 从OCR识别到的文本中提取结构化的身份证信息
|
|
82
|
-
*
|
|
87
|
+
*
|
|
83
88
|
* @private
|
|
84
89
|
* @param {string} text - OCR识别到的文本
|
|
85
90
|
* @returns {IDCardInfo} 提取到的身份证信息对象
|
|
86
91
|
*/
|
|
87
92
|
function parseIDCardText(text: string): IDCardInfo {
|
|
88
|
-
const info: IDCardInfo = {}
|
|
89
|
-
|
|
93
|
+
const info: IDCardInfo = {}
|
|
94
|
+
|
|
90
95
|
// 拆分为行
|
|
91
|
-
const lines = text.split(
|
|
92
|
-
|
|
96
|
+
const lines = text.split("\n").filter((line) => line.trim())
|
|
97
|
+
|
|
93
98
|
// 解析身份证号码(最容易识别的部分)
|
|
94
|
-
const idNumberRegex = /(\d{17}[\dX])
|
|
95
|
-
const idNumberMatch = text.match(idNumberRegex)
|
|
99
|
+
const idNumberRegex = /(\d{17}[\dX])/
|
|
100
|
+
const idNumberMatch = text.match(idNumberRegex)
|
|
96
101
|
if (idNumberMatch) {
|
|
97
|
-
info.idNumber = idNumberMatch[1]
|
|
102
|
+
info.idNumber = idNumberMatch[1]
|
|
98
103
|
}
|
|
99
|
-
|
|
104
|
+
|
|
100
105
|
// 解析姓名
|
|
101
106
|
for (const line of lines) {
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
107
|
+
if (
|
|
108
|
+
line.includes("姓名") ||
|
|
109
|
+
(line.length < 10 && line.length > 1 && !/\d/.test(line))
|
|
110
|
+
) {
|
|
111
|
+
info.name = line.replace("姓名", "").trim()
|
|
112
|
+
break
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
|
-
|
|
115
|
+
|
|
108
116
|
// 解析性别和民族
|
|
109
|
-
const genderNationalityRegex = /(男|女).*(族)
|
|
110
|
-
const genderMatch = text.match(genderNationalityRegex)
|
|
117
|
+
const genderNationalityRegex = /(男|女).*(族)/
|
|
118
|
+
const genderMatch = text.match(genderNationalityRegex)
|
|
111
119
|
if (genderMatch) {
|
|
112
|
-
info.gender = genderMatch[1]
|
|
113
|
-
const nationalityText = genderMatch[0]
|
|
114
|
-
info.nationality = nationalityText
|
|
120
|
+
info.gender = genderMatch[1]
|
|
121
|
+
const nationalityText = genderMatch[0]
|
|
122
|
+
info.nationality = nationalityText
|
|
123
|
+
.substring(nationalityText.indexOf(genderMatch[1]) + 1)
|
|
124
|
+
.trim()
|
|
115
125
|
}
|
|
116
|
-
|
|
126
|
+
|
|
117
127
|
// 解析出生日期
|
|
118
|
-
const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})
|
|
119
|
-
const birthDateMatch = text.match(birthDateRegex)
|
|
128
|
+
const birthDateRegex = /(\d{4})年(\d{1,2})月(\d{1,2})日/
|
|
129
|
+
const birthDateMatch = text.match(birthDateRegex)
|
|
120
130
|
if (birthDateMatch) {
|
|
121
|
-
info.birthDate = `${birthDateMatch[1]}-${birthDateMatch[2]}-${birthDateMatch[3]}
|
|
131
|
+
info.birthDate = `${birthDateMatch[1]}-${birthDateMatch[2]}-${birthDateMatch[3]}`
|
|
122
132
|
}
|
|
123
|
-
|
|
133
|
+
|
|
124
134
|
// 解析地址
|
|
125
|
-
const addressRegex = /住址([\s\S]*?)
|
|
126
|
-
const addressMatch = text.match(addressRegex)
|
|
135
|
+
const addressRegex = /住址([\s\S]*?)公民身份号码/
|
|
136
|
+
const addressMatch = text.match(addressRegex)
|
|
127
137
|
if (addressMatch) {
|
|
128
|
-
info.address = addressMatch[1].replace(/\n/g,
|
|
138
|
+
info.address = addressMatch[1].replace(/\n/g, "").trim()
|
|
129
139
|
}
|
|
130
|
-
|
|
140
|
+
|
|
131
141
|
// 解析签发机关
|
|
132
|
-
const authorityRegex = /签发机关([\s\S]*?)
|
|
133
|
-
const authorityMatch = text.match(authorityRegex)
|
|
142
|
+
const authorityRegex = /签发机关([\s\S]*?)有效期/
|
|
143
|
+
const authorityMatch = text.match(authorityRegex)
|
|
134
144
|
if (authorityMatch) {
|
|
135
|
-
info.issuingAuthority = authorityMatch[1].replace(/\n/g,
|
|
145
|
+
info.issuingAuthority = authorityMatch[1].replace(/\n/g, "").trim()
|
|
136
146
|
}
|
|
137
|
-
|
|
147
|
+
|
|
138
148
|
// 解析有效期限
|
|
139
|
-
const validPeriodRegex = /有效期限([\s\S]*?)(-|至)
|
|
140
|
-
const validPeriodMatch = text.match(validPeriodRegex)
|
|
149
|
+
const validPeriodRegex = /有效期限([\s\S]*?)(-|至)/
|
|
150
|
+
const validPeriodMatch = text.match(validPeriodRegex)
|
|
141
151
|
if (validPeriodMatch) {
|
|
142
|
-
info.validPeriod = validPeriodMatch[0].replace(
|
|
152
|
+
info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim()
|
|
143
153
|
}
|
|
144
|
-
|
|
145
|
-
return info
|
|
146
|
-
}
|
|
154
|
+
|
|
155
|
+
return info
|
|
156
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
|
|
@@ -18,6 +18,12 @@ import {
|
|
|
18
18
|
import type { QRScannerOptions } from "./scanner/qr-scanner"
|
|
19
19
|
import type { BarcodeScannerOptions } from "./scanner/barcode-scanner"
|
|
20
20
|
|
|
21
|
+
// 导入防伪检测器
|
|
22
|
+
import {
|
|
23
|
+
AntiFakeDetector,
|
|
24
|
+
AntiFakeDetectionResult,
|
|
25
|
+
} from "./id-recognition/anti-fake-detector"
|
|
26
|
+
|
|
21
27
|
/**
|
|
22
28
|
* IDScanner配置选项接口
|
|
23
29
|
*/
|
|
@@ -29,6 +35,7 @@ export interface IDScannerOptions {
|
|
|
29
35
|
onBarcodeScanned?: (result: string) => void
|
|
30
36
|
onIDCardScanned?: (info: IDCardInfo) => void
|
|
31
37
|
onImageProcessed?: (processedImage: ImageData | File) => void
|
|
38
|
+
onAntiFakeDetected?: (result: AntiFakeDetectionResult) => void
|
|
32
39
|
onError?: (error: Error) => void
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -42,15 +49,17 @@ export class IDScanner {
|
|
|
42
49
|
private camera: Camera
|
|
43
50
|
private scanMode: "qr" | "barcode" | "idcard" = "qr"
|
|
44
51
|
private videoElement: HTMLVideoElement | null = null
|
|
45
|
-
|
|
46
|
-
// 延迟加载的模块
|
|
52
|
+
private scanning = false
|
|
47
53
|
private qrModule: any = null
|
|
48
54
|
private ocrModule: any = null
|
|
49
|
-
|
|
50
|
-
// 模块加载状态
|
|
55
|
+
private scanTimer: number | null = null
|
|
51
56
|
private isQRModuleLoaded: boolean = false
|
|
52
57
|
private isOCRModuleLoaded: boolean = false
|
|
53
58
|
|
|
59
|
+
// 新增防伪检测器
|
|
60
|
+
private antiFakeDetector: AntiFakeDetector | null = null
|
|
61
|
+
private isAntiFakeModuleLoaded: boolean = false
|
|
62
|
+
|
|
54
63
|
/**
|
|
55
64
|
* 构造函数
|
|
56
65
|
* @param options 配置选项
|
|
@@ -61,7 +70,7 @@ export class IDScanner {
|
|
|
61
70
|
|
|
62
71
|
/**
|
|
63
72
|
* 初始化模块
|
|
64
|
-
* 根据需要初始化OCR
|
|
73
|
+
* 根据需要初始化OCR引擎和防伪检测模块
|
|
65
74
|
*/
|
|
66
75
|
async initialize(): Promise<void> {
|
|
67
76
|
try {
|
|
@@ -80,13 +89,44 @@ export class IDScanner {
|
|
|
80
89
|
await this.ocrModule.initialize()
|
|
81
90
|
}
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
// 初始化防伪检测模块
|
|
93
|
+
if (!this.isAntiFakeModuleLoaded) {
|
|
94
|
+
this.antiFakeDetector = new AntiFakeDetector()
|
|
95
|
+
this.isAntiFakeModuleLoaded = true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log("IDScanner初始化完成")
|
|
84
99
|
} catch (error) {
|
|
100
|
+
console.error("初始化失败:", error)
|
|
85
101
|
this.handleError(error as Error)
|
|
86
102
|
throw error
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
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()
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error("OCR模块初始化失败:", error)
|
|
126
|
+
throw error
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
90
130
|
/**
|
|
91
131
|
* 启动二维码扫描
|
|
92
132
|
* @param videoElement HTML视频元素
|
|
@@ -194,7 +234,7 @@ export class IDScanner {
|
|
|
194
234
|
if (this.options.onError) {
|
|
195
235
|
this.options.onError(error)
|
|
196
236
|
} else {
|
|
197
|
-
console.error("IDScanner
|
|
237
|
+
console.error("IDScanner错误:", error)
|
|
198
238
|
}
|
|
199
239
|
}
|
|
200
240
|
|
|
@@ -216,6 +256,13 @@ export class IDScanner {
|
|
|
216
256
|
this.qrModule = null
|
|
217
257
|
this.isQRModuleLoaded = false
|
|
218
258
|
}
|
|
259
|
+
|
|
260
|
+
// 释放防伪检测资源
|
|
261
|
+
if (this.antiFakeDetector) {
|
|
262
|
+
this.antiFakeDetector.dispose()
|
|
263
|
+
this.antiFakeDetector = null
|
|
264
|
+
this.isAntiFakeModuleLoaded = false
|
|
265
|
+
}
|
|
219
266
|
}
|
|
220
267
|
|
|
221
268
|
/**
|
|
@@ -388,12 +435,11 @@ export class IDScanner {
|
|
|
388
435
|
async processIDCardImage(
|
|
389
436
|
imageSource: HTMLImageElement | HTMLCanvasElement | string | File
|
|
390
437
|
): Promise<IDCardInfo> {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
await this.initialize()
|
|
395
|
-
}
|
|
438
|
+
if (!this.isOCRModuleLoaded) {
|
|
439
|
+
await this.initOCRModule()
|
|
440
|
+
}
|
|
396
441
|
|
|
442
|
+
try {
|
|
397
443
|
// 处理不同类型的图片源
|
|
398
444
|
let imageElement: HTMLImageElement
|
|
399
445
|
|
|
@@ -460,7 +506,26 @@ export class IDScanner {
|
|
|
460
506
|
})
|
|
461
507
|
|
|
462
508
|
// 使用OCR模块处理图像
|
|
463
|
-
|
|
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
|
|
464
529
|
} catch (error) {
|
|
465
530
|
this.handleError(error as Error)
|
|
466
531
|
throw error
|
|
@@ -596,6 +661,89 @@ export class IDScanner {
|
|
|
596
661
|
throw error
|
|
597
662
|
}
|
|
598
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
|
|
745
|
+
}
|
|
746
|
+
}
|
|
599
747
|
}
|
|
600
748
|
|
|
601
749
|
// 导出工具类和类型
|
|
@@ -607,6 +755,9 @@ export {
|
|
|
607
755
|
} from "./utils/image-processing"
|
|
608
756
|
export { IDCardInfo, DetectionResult } from "./utils/types"
|
|
609
757
|
|
|
758
|
+
// 导出防伪检测相关类型和类
|
|
759
|
+
export { AntiFakeDetector, AntiFakeDetectionResult }
|
|
760
|
+
|
|
610
761
|
// 为了向后兼容,我们创建一个演示类
|
|
611
762
|
export class IDScannerDemo {
|
|
612
763
|
private scanner: IDScanner
|
|
@@ -759,7 +910,7 @@ export class IDScannerDemo {
|
|
|
759
910
|
private handleIDCardResult(info: IDCardInfo): void {
|
|
760
911
|
// 格式化显示身份证信息
|
|
761
912
|
const infoHtml = Object.entries(info)
|
|
762
|
-
.filter(([key, value]) => value) //
|
|
913
|
+
.filter(([key, value]) => value && key !== "antiFakeResult") // 过滤掉空值和防伪结果
|
|
763
914
|
.map(([key, value]) => {
|
|
764
915
|
// 转换键名为中文显示
|
|
765
916
|
const keyMap: { [key: string]: string } = {
|
|
@@ -778,9 +929,32 @@ export class IDScannerDemo {
|
|
|
778
929
|
})
|
|
779
930
|
.join("")
|
|
780
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
|
+
|
|
781
954
|
this.updateResultDisplay(`
|
|
782
955
|
<h3>身份证信息:</h3>
|
|
783
956
|
${infoHtml}
|
|
957
|
+
${antiFakeHtml}
|
|
784
958
|
`)
|
|
785
959
|
}
|
|
786
960
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for browser-image-compression
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
declare module 'browser-image-compression' {
|
|
6
|
+
export interface Options {
|
|
7
|
+
maxSizeMB?: number;
|
|
8
|
+
maxWidthOrHeight?: number;
|
|
9
|
+
useWebWorker?: boolean;
|
|
10
|
+
maxIteration?: number;
|
|
11
|
+
quality?: number;
|
|
12
|
+
fileType?: string;
|
|
13
|
+
onProgress?: (progress: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function imageCompression(file: File, options?: Options): Promise<File>;
|
|
17
|
+
|
|
18
|
+
export default imageCompression;
|
|
19
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for tesseract.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
declare module 'tesseract.js' {
|
|
6
|
+
export interface WorkerOptions {
|
|
7
|
+
langPath?: string;
|
|
8
|
+
corePath?: string;
|
|
9
|
+
workerPath?: string;
|
|
10
|
+
logger?: (message: any) => void;
|
|
11
|
+
errorHandler?: (error: Error) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RecognizeResult {
|
|
15
|
+
data: {
|
|
16
|
+
text: string;
|
|
17
|
+
hocr: string;
|
|
18
|
+
tsv: string;
|
|
19
|
+
confidence: number;
|
|
20
|
+
blocks: any[];
|
|
21
|
+
lines: any[];
|
|
22
|
+
words: any[];
|
|
23
|
+
symbols: any[];
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Worker {
|
|
28
|
+
load(): Promise<any>;
|
|
29
|
+
loadLanguage(lang: string): Promise<any>;
|
|
30
|
+
initialize(lang: string): Promise<any>;
|
|
31
|
+
setParameters(params: Record<string, any>): Promise<any>;
|
|
32
|
+
recognize(image: HTMLCanvasElement | ImageData | Buffer | string): Promise<RecognizeResult>;
|
|
33
|
+
terminate(): Promise<any>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createWorker(options?: WorkerOptions): Worker;
|
|
37
|
+
}
|