id-scanner-lib 1.3.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +55 -460
  2. package/dist/id-scanner-lib.esm.js +4641 -0
  3. package/dist/id-scanner-lib.esm.js.map +1 -0
  4. package/dist/id-scanner-lib.js +14755 -0
  5. package/dist/id-scanner-lib.js.map +1 -0
  6. package/dist/types/core/base-module.d.ts +44 -0
  7. package/dist/types/core/camera-manager.d.ts +258 -0
  8. package/dist/types/core/config.d.ts +88 -0
  9. package/dist/types/core/errors.d.ts +111 -0
  10. package/dist/types/core/event-emitter.d.ts +55 -0
  11. package/dist/types/core/logger.d.ts +277 -0
  12. package/dist/types/core/module-manager.d.ts +78 -0
  13. package/dist/types/core/plugin-manager.d.ts +158 -0
  14. package/dist/types/core/resource-manager.d.ts +246 -0
  15. package/dist/types/core/result.d.ts +83 -0
  16. package/dist/types/core/scanner-factory.d.ts +93 -0
  17. package/dist/types/index.bundle.d.ts +1303 -0
  18. package/dist/types/index.d.ts +86 -0
  19. package/dist/types/interfaces/external-types.d.ts +174 -0
  20. package/dist/types/interfaces/face-detection.d.ts +293 -0
  21. package/dist/types/interfaces/scanner-module.d.ts +280 -0
  22. package/dist/types/modules/face/face-detector.d.ts +170 -0
  23. package/dist/types/modules/face/index.d.ts +56 -0
  24. package/dist/types/modules/face/liveness-detector.d.ts +177 -0
  25. package/dist/types/modules/face/types.d.ts +136 -0
  26. package/dist/types/modules/id-card/anti-fake-detector.d.ts +170 -0
  27. package/dist/types/modules/id-card/id-card-detector.d.ts +131 -0
  28. package/dist/types/modules/id-card/index.d.ts +89 -0
  29. package/dist/types/modules/id-card/ocr-processor.d.ts +110 -0
  30. package/dist/types/modules/id-card/ocr-worker.d.ts +31 -0
  31. package/dist/types/modules/id-card/types.d.ts +181 -0
  32. package/dist/types/modules/qrcode/index.d.ts +51 -0
  33. package/dist/types/modules/qrcode/qr-code-scanner.d.ts +64 -0
  34. package/dist/types/modules/qrcode/types.d.ts +67 -0
  35. package/dist/types/utils/camera.d.ts +81 -0
  36. package/dist/types/utils/image-processing.d.ts +176 -0
  37. package/dist/types/utils/index.d.ts +175 -0
  38. package/dist/types/utils/performance.d.ts +81 -0
  39. package/dist/types/utils/resource-manager.d.ts +53 -0
  40. package/dist/types/utils/types.d.ts +166 -0
  41. package/dist/types/utils/worker.d.ts +52 -0
  42. package/dist/types/version.d.ts +7 -0
  43. package/package.json +76 -75
  44. package/src/core/base-module.ts +78 -0
  45. package/src/core/camera-manager.ts +798 -0
  46. package/src/core/config.ts +268 -0
  47. package/src/core/errors.ts +174 -0
  48. package/src/core/event-emitter.ts +110 -0
  49. package/src/core/logger.ts +549 -0
  50. package/src/core/module-manager.ts +165 -0
  51. package/src/core/plugin-manager.ts +429 -0
  52. package/src/core/resource-manager.ts +762 -0
  53. package/src/core/result.ts +163 -0
  54. package/src/core/scanner-factory.ts +237 -0
  55. package/src/index.ts +113 -936
  56. package/src/interfaces/external-types.ts +200 -0
  57. package/src/interfaces/face-detection.ts +309 -0
  58. package/src/interfaces/scanner-module.ts +384 -0
  59. package/src/modules/face/face-detector.ts +931 -0
  60. package/src/modules/face/index.ts +208 -0
  61. package/src/modules/face/liveness-detector.ts +908 -0
  62. package/src/modules/face/types.ts +133 -0
  63. package/src/{id-recognition → modules/id-card}/anti-fake-detector.ts +273 -239
  64. package/src/modules/id-card/id-card-detector.ts +474 -0
  65. package/src/modules/id-card/index.ts +425 -0
  66. package/src/{id-recognition → modules/id-card}/ocr-processor.ts +149 -92
  67. package/src/modules/id-card/ocr-worker.ts +259 -0
  68. package/src/modules/id-card/types.ts +178 -0
  69. package/src/modules/qrcode/index.ts +175 -0
  70. package/src/modules/qrcode/qr-code-scanner.ts +230 -0
  71. package/src/modules/qrcode/types.ts +65 -0
  72. package/src/types/tesseract.d.ts +265 -22
  73. package/src/utils/image-processing.ts +68 -49
  74. package/src/utils/index.ts +426 -0
  75. package/src/utils/performance.ts +168 -131
  76. package/src/utils/resource-manager.ts +65 -146
  77. package/src/utils/types.ts +90 -2
  78. package/src/utils/worker.ts +123 -84
  79. package/src/version.ts +11 -0
  80. package/tools/scaffold.js +543 -0
  81. package/dist/id-scanner-core.esm.js +0 -11349
  82. package/dist/id-scanner-core.js +0 -11361
  83. package/dist/id-scanner-core.min.js +0 -1
  84. package/dist/id-scanner-ocr.esm.js +0 -2319
  85. package/dist/id-scanner-ocr.js +0 -2328
  86. package/dist/id-scanner-ocr.min.js +0 -1
  87. package/dist/id-scanner-qr.esm.js +0 -1296
  88. package/dist/id-scanner-qr.js +0 -1305
  89. package/dist/id-scanner-qr.min.js +0 -1
  90. package/dist/id-scanner.js +0 -4561
  91. package/dist/id-scanner.min.js +0 -1
  92. package/src/core.ts +0 -138
  93. package/src/demo/demo.ts +0 -204
  94. package/src/id-recognition/data-extractor.ts +0 -262
  95. package/src/id-recognition/id-detector.ts +0 -510
  96. package/src/id-recognition/ocr-worker.ts +0 -156
  97. package/src/index-umd.ts +0 -477
  98. package/src/ocr-module.ts +0 -187
  99. package/src/qr-module.ts +0 -179
  100. package/src/scanner/barcode-scanner.ts +0 -251
  101. package/src/scanner/qr-scanner.ts +0 -167
@@ -0,0 +1,4641 @@
1
+ import { createWorker as createWorker$1 } from 'tesseract.js';
2
+ import imageCompression from 'browser-image-compression';
3
+ import jsQR from 'jsqr';
4
+
5
+ /**
6
+ * @file 配置管理器
7
+ * @description 提供全局配置管理功能
8
+ * @module core/config
9
+ */
10
+ /**
11
+ * 配置管理器
12
+ * 负责存储和管理应用程序的配置
13
+ */
14
+ class ConfigManager {
15
+ /**
16
+ * 私有构造函数
17
+ */
18
+ constructor() {
19
+ /** 配置存储 */
20
+ this.config = {};
21
+ /** 配置变更回调 */
22
+ this.changeCallbacks = new Map();
23
+ // 设置默认配置
24
+ this.config = {
25
+ debug: false,
26
+ logLevel: 'info',
27
+ camera: {
28
+ resolution: {
29
+ width: 1280,
30
+ height: 720
31
+ },
32
+ frameRate: 30,
33
+ facingMode: 'environment'
34
+ },
35
+ performance: {
36
+ useCache: true
37
+ }
38
+ };
39
+ }
40
+ /**
41
+ * 获取单例实例
42
+ */
43
+ static getInstance() {
44
+ if (!ConfigManager.instance) {
45
+ ConfigManager.instance = new ConfigManager();
46
+ }
47
+ return ConfigManager.instance;
48
+ }
49
+ /**
50
+ * 获取配置值
51
+ * @param key 配置键,支持点号分隔的路径
52
+ * @param defaultValue 默认值
53
+ */
54
+ get(key, defaultValue) {
55
+ const value = this.getNestedValue(this.config, key);
56
+ return (value !== undefined) ? value : defaultValue;
57
+ }
58
+ /**
59
+ * 设置配置值
60
+ * @param key 配置键,支持点号分隔的路径
61
+ * @param value 配置值
62
+ */
63
+ set(key, value) {
64
+ const oldValue = this.get(key);
65
+ // 如果值相同,不做任何事
66
+ if (oldValue === value) {
67
+ return;
68
+ }
69
+ this.setNestedValue(this.config, key, value);
70
+ // 触发变更回调
71
+ this.triggerChangeCallbacks(key, value, oldValue);
72
+ }
73
+ /**
74
+ * 批量更新配置
75
+ * @param config 配置对象
76
+ */
77
+ updateConfig(config) {
78
+ Object.entries(config).forEach(([key, value]) => {
79
+ this.set(key, value);
80
+ });
81
+ }
82
+ /**
83
+ * 重置为默认配置
84
+ */
85
+ reset() {
86
+ const oldConfig = { ...this.config };
87
+ // 重新创建默认配置
88
+ this.config = {
89
+ debug: false,
90
+ logLevel: 'info',
91
+ camera: {
92
+ resolution: {
93
+ width: 1280,
94
+ height: 720
95
+ },
96
+ frameRate: 30,
97
+ facingMode: 'environment'
98
+ },
99
+ performance: {
100
+ useCache: true
101
+ }
102
+ };
103
+ // 触发所有回调
104
+ Object.keys(oldConfig).forEach(key => {
105
+ this.triggerChangeCallbacks(key, this.get(key), oldConfig[key]);
106
+ });
107
+ }
108
+ /**
109
+ * 注册配置变更回调
110
+ * @param key 配置键
111
+ * @param callback 回调函数
112
+ */
113
+ onConfigChange(key, callback) {
114
+ if (!this.changeCallbacks.has(key)) {
115
+ this.changeCallbacks.set(key, []);
116
+ }
117
+ this.changeCallbacks.get(key).push(callback);
118
+ }
119
+ /**
120
+ * 移除配置变更回调
121
+ * @param key 配置键
122
+ * @param callback 特定回调函数,如不提供则移除所有
123
+ */
124
+ offConfigChange(key, callback) {
125
+ if (!this.changeCallbacks.has(key)) {
126
+ return;
127
+ }
128
+ if (callback) {
129
+ // 移除特定回调
130
+ const callbacks = this.changeCallbacks.get(key);
131
+ const index = callbacks.indexOf(callback);
132
+ if (index !== -1) {
133
+ callbacks.splice(index, 1);
134
+ }
135
+ // 如果没有回调,删除键
136
+ if (callbacks.length === 0) {
137
+ this.changeCallbacks.delete(key);
138
+ }
139
+ }
140
+ else {
141
+ // 移除所有回调
142
+ this.changeCallbacks.delete(key);
143
+ }
144
+ }
145
+ /**
146
+ * 获取嵌套值
147
+ * @param obj 对象
148
+ * @param path 路径
149
+ */
150
+ getNestedValue(obj, path) {
151
+ // 处理根路径
152
+ if (!path) {
153
+ return obj;
154
+ }
155
+ // 处理嵌套路径
156
+ const parts = path.split('.');
157
+ let current = obj;
158
+ for (const part of parts) {
159
+ if (current === undefined || current === null) {
160
+ return undefined;
161
+ }
162
+ current = current[part];
163
+ }
164
+ return current;
165
+ }
166
+ /**
167
+ * 设置嵌套值
168
+ * @param obj 对象
169
+ * @param path 路径
170
+ * @param value 值
171
+ */
172
+ setNestedValue(obj, path, value) {
173
+ // 处理根路径
174
+ if (!path) {
175
+ return;
176
+ }
177
+ // 处理嵌套路径
178
+ const parts = path.split('.');
179
+ let current = obj;
180
+ // 遍历路径,直到倒数第二部分
181
+ for (let i = 0; i < parts.length - 1; i++) {
182
+ const part = parts[i];
183
+ // 如果不存在,创建新对象
184
+ if (current[part] === undefined || current[part] === null || typeof current[part] !== 'object') {
185
+ current[part] = {};
186
+ }
187
+ current = current[part];
188
+ }
189
+ // 设置最终值
190
+ current[parts[parts.length - 1]] = value;
191
+ }
192
+ /**
193
+ * 触发变更回调
194
+ * @param key 配置键
195
+ * @param value 新值
196
+ * @param oldValue 旧值
197
+ */
198
+ triggerChangeCallbacks(key, value, oldValue) {
199
+ // 触发特定键的回调
200
+ if (this.changeCallbacks.has(key)) {
201
+ const callbacks = this.changeCallbacks.get(key);
202
+ callbacks.forEach(callback => {
203
+ try {
204
+ callback(value, oldValue);
205
+ }
206
+ catch (error) {
207
+ console.error(`Error in config change callback for key ${key}:`, error);
208
+ }
209
+ });
210
+ }
211
+ // 触发父路径的回调
212
+ const parts = key.split('.');
213
+ while (parts.length > 1) {
214
+ parts.pop();
215
+ const parentKey = parts.join('.');
216
+ if (this.changeCallbacks.has(parentKey)) {
217
+ const parentValue = this.get(parentKey);
218
+ this.changeCallbacks.get(parentKey).forEach(callback => {
219
+ try {
220
+ callback(parentValue, parentValue);
221
+ }
222
+ catch (error) {
223
+ console.error(`Error in config change callback for parent key ${parentKey}:`, error);
224
+ }
225
+ });
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * @file 日志系统
233
+ * @description 提供统一的日志记录与管理功能
234
+ * @module core/logger
235
+ */
236
+ /**
237
+ * 日志级别枚举
238
+ */
239
+ var LogLevel;
240
+ (function (LogLevel) {
241
+ LogLevel["DEBUG"] = "debug";
242
+ LogLevel["INFO"] = "info";
243
+ LogLevel["WARN"] = "warn";
244
+ LogLevel["ERROR"] = "error";
245
+ })(LogLevel || (LogLevel = {}));
246
+ /**
247
+ * 控制台日志处理器
248
+ * 将日志输出到浏览器控制台
249
+ */
250
+ class ConsoleLogHandler {
251
+ /**
252
+ * 处理日志条目
253
+ * @param entry 日志条目
254
+ */
255
+ handle(entry) {
256
+ const timestamp = new Date(entry.timestamp).toISOString();
257
+ const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
258
+ switch (entry.level) {
259
+ case LogLevel.DEBUG:
260
+ console.debug(prefix, entry.message, entry.error || '');
261
+ break;
262
+ case LogLevel.INFO:
263
+ console.info(prefix, entry.message, entry.error || '');
264
+ break;
265
+ case LogLevel.WARN:
266
+ console.warn(prefix, entry.message, entry.error || '');
267
+ break;
268
+ case LogLevel.ERROR:
269
+ console.error(prefix, entry.message, entry.error || '');
270
+ break;
271
+ // 输出什么也不做
272
+ }
273
+ }
274
+ }
275
+ /**
276
+ * 内存日志处理器
277
+ * 将日志保存在内存中,用于后续分析或显示
278
+ */
279
+ class MemoryLogHandler {
280
+ /**
281
+ * 构造函数
282
+ * @param maxEntries 最大日志条目数,默认为1000
283
+ */
284
+ constructor(maxEntries = 1000) {
285
+ /** 日志条目数组 */
286
+ this.entries = [];
287
+ this.maxEntries = maxEntries;
288
+ }
289
+ /**
290
+ * 处理日志条目
291
+ * @param entry 日志条目
292
+ */
293
+ handle(entry) {
294
+ this.entries.push(entry);
295
+ // 如果超过最大条目数,移除最老的
296
+ if (this.entries.length > this.maxEntries) {
297
+ this.entries.shift();
298
+ }
299
+ }
300
+ /**
301
+ * 获取所有日志条目
302
+ */
303
+ getEntries() {
304
+ return [...this.entries];
305
+ }
306
+ /**
307
+ * 根据级别过滤日志条目
308
+ * @param level 日志级别
309
+ */
310
+ getEntriesByLevel(level) {
311
+ return this.entries.filter(entry => entry.level === level);
312
+ }
313
+ /**
314
+ * 根据标签过滤日志条目
315
+ * @param tag 日志标签
316
+ */
317
+ getEntriesByTag(tag) {
318
+ return this.entries.filter(entry => entry.tag === tag);
319
+ }
320
+ /**
321
+ * 清空日志
322
+ */
323
+ clear() {
324
+ this.entries = [];
325
+ }
326
+ }
327
+ /**
328
+ * 远程日志处理器
329
+ * 将日志发送到远程服务器
330
+ */
331
+ class RemoteLogHandler {
332
+ /**
333
+ * 构造函数
334
+ * @param endpoint 远程服务器URL
335
+ * @param maxQueueSize 最大队列长度,默认为100
336
+ * @param flushInterval 发送间隔(毫秒),默认为5000
337
+ */
338
+ constructor(endpoint, maxQueueSize = 100, flushInterval = 5000) {
339
+ /** 批量发送的队列 */
340
+ this.queue = [];
341
+ /** 定时发送的计时器ID */
342
+ this.timerId = null;
343
+ this.endpoint = endpoint;
344
+ this.maxQueueSize = maxQueueSize;
345
+ this.flushInterval = flushInterval;
346
+ // 设置定时发送
347
+ this.startTimer();
348
+ // 页面卸载前尝试发送剩余日志
349
+ window.addEventListener('beforeunload', () => {
350
+ this.flush();
351
+ });
352
+ }
353
+ /**
354
+ * 处理日志条目
355
+ * @param entry 日志条目
356
+ */
357
+ handle(entry) {
358
+ // 只处理INFO以上级别的日志
359
+ if (entry.level >= LogLevel.INFO) {
360
+ this.queue.push(entry);
361
+ // 如果队列满了,立即发送
362
+ if (this.queue.length >= this.maxQueueSize) {
363
+ this.flush();
364
+ }
365
+ }
366
+ }
367
+ /**
368
+ * 发送队列中的日志
369
+ */
370
+ flush() {
371
+ if (this.queue.length === 0)
372
+ return;
373
+ const entriesToSend = [...this.queue];
374
+ this.queue = [];
375
+ try {
376
+ fetch(this.endpoint, {
377
+ method: 'POST',
378
+ headers: {
379
+ 'Content-Type': 'application/json'
380
+ },
381
+ body: JSON.stringify(entriesToSend),
382
+ // 不等待响应,避免阻塞
383
+ keepalive: true
384
+ }).catch(err => {
385
+ console.error('Failed to send logs to remote server:', err);
386
+ // 失败时把日志放回队列,但防止无限增长
387
+ if (this.queue.length < this.maxQueueSize) {
388
+ this.queue = [...entriesToSend.slice(0, this.maxQueueSize - this.queue.length), ...this.queue];
389
+ }
390
+ });
391
+ }
392
+ catch (error) {
393
+ console.error('Error sending logs:', error);
394
+ }
395
+ }
396
+ /**
397
+ * 开始定时发送
398
+ */
399
+ startTimer() {
400
+ if (this.timerId !== null)
401
+ return;
402
+ this.timerId = window.setInterval(() => {
403
+ this.flush();
404
+ }, this.flushInterval);
405
+ }
406
+ /**
407
+ * 停止定时发送
408
+ */
409
+ stopTimer() {
410
+ if (this.timerId !== null) {
411
+ window.clearInterval(this.timerId);
412
+ this.timerId = null;
413
+ }
414
+ }
415
+ }
416
+ /**
417
+ * 日志管理类
418
+ * 中央日志管理器,提供统一的日志记录接口
419
+ */
420
+ class Logger {
421
+ /**
422
+ * 私有构造函数,防止直接实例化
423
+ */
424
+ constructor() {
425
+ /** 日志处理器 */
426
+ this.handlers = [];
427
+ /** 默认标签 */
428
+ this.defaultTag = 'IDScanner';
429
+ /** 日志级别 */
430
+ this.logLevel = LogLevel.INFO;
431
+ this.config = ConfigManager.getInstance();
432
+ // 默认添加控制台处理器
433
+ this.addHandler(new ConsoleLogHandler());
434
+ // 监听配置变化
435
+ this.config.onConfigChange('logLevel', (level) => {
436
+ this.debug('Logger', `Log level changed to ${level}`);
437
+ });
438
+ }
439
+ /**
440
+ * 获取单例实例
441
+ */
442
+ static getInstance() {
443
+ if (!Logger.instance) {
444
+ Logger.instance = new Logger();
445
+ }
446
+ return Logger.instance;
447
+ }
448
+ /**
449
+ * 添加日志处理器
450
+ * @param handler 日志处理器
451
+ */
452
+ addHandler(handler) {
453
+ this.handlers.push(handler);
454
+ }
455
+ /**
456
+ * 移除日志处理器
457
+ * @param handler 要移除的处理器
458
+ */
459
+ removeHandler(handler) {
460
+ const index = this.handlers.indexOf(handler);
461
+ if (index !== -1) {
462
+ this.handlers.splice(index, 1);
463
+ }
464
+ }
465
+ /**
466
+ * 移除所有处理器
467
+ */
468
+ clearHandlers() {
469
+ this.handlers = [];
470
+ }
471
+ /**
472
+ * 设置默认标签
473
+ * @param tag 默认标签
474
+ */
475
+ setDefaultTag(tag) {
476
+ this.defaultTag = tag;
477
+ }
478
+ /**
479
+ * 记录调试级别日志
480
+ * @param tag 标签
481
+ * @param message 消息
482
+ * @param error 错误
483
+ */
484
+ debug(tag, message, error) {
485
+ this.log(LogLevel.DEBUG, tag, message, error);
486
+ }
487
+ /**
488
+ * 记录信息级别日志
489
+ * @param tag 标签
490
+ * @param message 消息
491
+ * @param error 错误
492
+ */
493
+ info(tag, message, error) {
494
+ this.log(LogLevel.INFO, tag, message, error);
495
+ }
496
+ /**
497
+ * 记录警告级别日志
498
+ * @param tag 标签
499
+ * @param message 消息
500
+ * @param error 错误
501
+ */
502
+ warn(tag, message, error) {
503
+ this.log(LogLevel.WARN, tag, message, error);
504
+ }
505
+ /**
506
+ * 记录错误级别日志
507
+ * @param tag 标签
508
+ * @param message 消息
509
+ * @param error 错误
510
+ */
511
+ error(tag, message, error) {
512
+ this.log(LogLevel.ERROR, tag, message, error);
513
+ }
514
+ /**
515
+ * 创建标记了特定标签的日志记录器
516
+ * @param tag 标签
517
+ */
518
+ getTaggedLogger(tag) {
519
+ return new TaggedLogger(this, tag);
520
+ }
521
+ /**
522
+ * 记录日志
523
+ * @param level 日志级别
524
+ * @param tag 标签
525
+ * @param message 消息
526
+ * @param error 错误
527
+ */
528
+ log(level, tag, message, error) {
529
+ // 检查日志级别
530
+ const levelValue = this.getLevelValue(level);
531
+ const currentLevelValue = this.getLevelValue(this.logLevel);
532
+ if (levelValue < currentLevelValue) {
533
+ return;
534
+ }
535
+ // 创建日志条目
536
+ const entry = {
537
+ timestamp: Date.now(),
538
+ level: level,
539
+ tag: tag || this.defaultTag,
540
+ message,
541
+ error
542
+ };
543
+ // 分发到所有处理程序
544
+ for (const handler of this.handlers) {
545
+ try {
546
+ handler.handle(entry);
547
+ }
548
+ catch (handlerError) {
549
+ console.error(`[Logger] 处理程序错误:`, handlerError);
550
+ }
551
+ }
552
+ // 如果没有处理程序,使用控制台
553
+ if (this.handlers.length === 0) {
554
+ this.consoleOutput(entry);
555
+ }
556
+ }
557
+ /**
558
+ * 控制台输出
559
+ * @param entry 日志条目
560
+ */
561
+ consoleOutput(entry) {
562
+ const timestamp = new Date(entry.timestamp).toISOString();
563
+ const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
564
+ switch (entry.level) {
565
+ case LogLevel.DEBUG:
566
+ console.debug(`${prefix} ${entry.message}`, entry.error || '');
567
+ break;
568
+ case LogLevel.INFO:
569
+ console.info(`${prefix} ${entry.message}`, entry.error || '');
570
+ break;
571
+ case LogLevel.WARN:
572
+ console.warn(`${prefix} ${entry.message}`, entry.error || '');
573
+ break;
574
+ case LogLevel.ERROR:
575
+ console.error(`${prefix} ${entry.message}`, entry.error || '');
576
+ break;
577
+ }
578
+ }
579
+ /**
580
+ * 获取日志级别值
581
+ * @param level 日志级别
582
+ */
583
+ getLevelValue(level) {
584
+ switch (level) {
585
+ case LogLevel.DEBUG:
586
+ return 0;
587
+ case LogLevel.INFO:
588
+ return 1;
589
+ case LogLevel.WARN:
590
+ return 2;
591
+ case LogLevel.ERROR:
592
+ return 3;
593
+ default:
594
+ return 1; // 默认INFO级别
595
+ }
596
+ }
597
+ /**
598
+ * 设置日志级别
599
+ * @param level 日志级别
600
+ */
601
+ setLevel(level) {
602
+ if (typeof level === 'string') {
603
+ switch (level) {
604
+ case 'debug':
605
+ this.logLevel = LogLevel.DEBUG;
606
+ break;
607
+ case 'info':
608
+ this.logLevel = LogLevel.INFO;
609
+ break;
610
+ case 'warn':
611
+ this.logLevel = LogLevel.WARN;
612
+ break;
613
+ case 'error':
614
+ this.logLevel = LogLevel.ERROR;
615
+ break;
616
+ default:
617
+ this.logLevel = LogLevel.INFO;
618
+ }
619
+ }
620
+ else {
621
+ this.logLevel = level;
622
+ }
623
+ this.debug('Logger', `日志级别已设置为 ${this.logLevel}`);
624
+ }
625
+ /**
626
+ * 获取当前日志级别
627
+ * @returns 当前日志级别
628
+ */
629
+ getLevel() {
630
+ return this.logLevel;
631
+ }
632
+ }
633
+ /**
634
+ * 带标签的日志记录器
635
+ * 提供特定标签的简易日志接口
636
+ */
637
+ class TaggedLogger {
638
+ /**
639
+ * 构造函数
640
+ * @param logger 所属的主日志记录器
641
+ * @param tag 标签
642
+ */
643
+ constructor(logger, tag) {
644
+ this.logger = logger;
645
+ this.tag = tag;
646
+ }
647
+ /**
648
+ * 记录调试级别日志
649
+ * @param message 消息
650
+ * @param error 错误
651
+ */
652
+ debug(message, error) {
653
+ this.logger.debug(this.tag, message, error);
654
+ }
655
+ /**
656
+ * 记录信息级别日志
657
+ * @param message 消息
658
+ * @param error 错误
659
+ */
660
+ info(message, error) {
661
+ this.logger.info(this.tag, message, error);
662
+ }
663
+ /**
664
+ * 记录警告级别日志
665
+ * @param message 消息
666
+ * @param error 错误
667
+ */
668
+ warn(message, error) {
669
+ this.logger.warn(this.tag, message, error);
670
+ }
671
+ /**
672
+ * 记录错误级别日志
673
+ * @param message 消息
674
+ * @param error 错误
675
+ */
676
+ error(message, error) {
677
+ this.logger.error(this.tag, message, error);
678
+ }
679
+ }
680
+ /**
681
+ * 日志级别枚举
682
+ */
683
+
684
+ /**
685
+ * @file 事件发射器
686
+ * @description 提供基础的事件发射和订阅功能
687
+ * @module core/event-emitter
688
+ */
689
+ /**
690
+ * 事件发射器基类
691
+ * 提供基础的事件发射和订阅功能
692
+ */
693
+ class EventEmitter {
694
+ constructor() {
695
+ /** 事件处理器映射 */
696
+ this.eventHandlers = new Map();
697
+ }
698
+ /**
699
+ * 订阅事件
700
+ * @param eventName 事件名称
701
+ * @param handler 事件处理器
702
+ */
703
+ on(eventName, handler) {
704
+ if (!this.eventHandlers.has(eventName)) {
705
+ this.eventHandlers.set(eventName, new Set());
706
+ }
707
+ this.eventHandlers.get(eventName).add(handler);
708
+ }
709
+ /**
710
+ * 取消订阅事件
711
+ * @param eventName 事件名称
712
+ * @param handler 事件处理器,如果不提供则移除该事件的所有处理器
713
+ */
714
+ off(eventName, handler) {
715
+ if (!this.eventHandlers.has(eventName)) {
716
+ return;
717
+ }
718
+ if (handler) {
719
+ this.eventHandlers.get(eventName).delete(handler);
720
+ // 如果没有处理器了,删除这个事件
721
+ if (this.eventHandlers.get(eventName).size === 0) {
722
+ this.eventHandlers.delete(eventName);
723
+ }
724
+ }
725
+ else {
726
+ // 移除该事件的所有处理器
727
+ this.eventHandlers.delete(eventName);
728
+ }
729
+ }
730
+ /**
731
+ * 订阅事件,但只触发一次
732
+ * @param eventName 事件名称
733
+ * @param handler 事件处理器
734
+ */
735
+ once(eventName, handler) {
736
+ const onceHandler = (data) => {
737
+ handler(data);
738
+ this.off(eventName, onceHandler);
739
+ };
740
+ this.on(eventName, onceHandler);
741
+ }
742
+ /**
743
+ * 发射事件
744
+ * @param eventName 事件名称
745
+ * @param data 事件数据
746
+ */
747
+ emit(eventName, data) {
748
+ if (!this.eventHandlers.has(eventName)) {
749
+ return;
750
+ }
751
+ for (const handler of this.eventHandlers.get(eventName)) {
752
+ try {
753
+ handler(data);
754
+ }
755
+ catch (error) {
756
+ console.error(`Error in event handler for "${eventName}":`, error);
757
+ }
758
+ }
759
+ }
760
+ /**
761
+ * 获取某个事件的处理器数量
762
+ * @param eventName 事件名称
763
+ */
764
+ listenerCount(eventName) {
765
+ return this.eventHandlers.has(eventName) ? this.eventHandlers.get(eventName).size : 0;
766
+ }
767
+ /**
768
+ * 移除所有事件处理器
769
+ */
770
+ removeAllListeners() {
771
+ this.eventHandlers.clear();
772
+ }
773
+ /**
774
+ * 获取所有事件名称
775
+ */
776
+ eventNames() {
777
+ return Array.from(this.eventHandlers.keys());
778
+ }
779
+ }
780
+
781
+ /**
782
+ * @file 版本号文件
783
+ * @description 定义库的版本号
784
+ * @module Version
785
+ */
786
+ // 当前版本号
787
+ const VERSION = '1.5.0';
788
+ // 构建日期
789
+ const BUILD_DATE = new Date().toISOString();
790
+
791
+ /**
792
+ * @file 模块管理器
793
+ * @description 统一管理库的各功能模块,提供模块的注册、初始化和卸载功能
794
+ * @module core/module-manager
795
+ */
796
+ /**
797
+ * 模块管理器类
798
+ * 负责管理所有功能模块的生命周期
799
+ */
800
+ class ModuleManager extends EventEmitter {
801
+ /**
802
+ * 获取模块管理器单例
803
+ */
804
+ static getInstance() {
805
+ if (!ModuleManager.instance) {
806
+ ModuleManager.instance = new ModuleManager();
807
+ }
808
+ return ModuleManager.instance;
809
+ }
810
+ /**
811
+ * 私有构造函数,确保单例模式
812
+ */
813
+ constructor() {
814
+ super();
815
+ this.modules = new Map();
816
+ this.initialized = false;
817
+ this.logger = Logger.getInstance();
818
+ this.logger.debug('ModuleManager', `初始化模块管理器 v${VERSION}`);
819
+ }
820
+ /**
821
+ * 注册模块
822
+ * @param module 要注册的模块
823
+ * @returns 模块管理器实例,支持链式调用
824
+ */
825
+ register(module) {
826
+ if (this.modules.has(module.name)) {
827
+ this.logger.warn('ModuleManager', `模块 "${module.name}" 已经注册,将被覆盖`);
828
+ }
829
+ this.modules.set(module.name, module);
830
+ this.logger.debug('ModuleManager', `注册模块: ${module.name} v${module.version}`);
831
+ this.emit('module:registered', { name: module.name });
832
+ return this;
833
+ }
834
+ /**
835
+ * 获取模块
836
+ * @param name 模块名称
837
+ * @returns 模块实例
838
+ */
839
+ getModule(name) {
840
+ return this.modules.get(name);
841
+ }
842
+ /**
843
+ * 初始化所有注册的模块
844
+ */
845
+ async initialize() {
846
+ if (this.initialized) {
847
+ return;
848
+ }
849
+ this.logger.debug('ModuleManager', '开始初始化所有模块...');
850
+ for (const [name, module] of this.modules.entries()) {
851
+ try {
852
+ this.logger.debug('ModuleManager', `初始化模块: ${name}`);
853
+ await module.initialize();
854
+ this.emit('module:initialized', { name });
855
+ this.logger.debug('ModuleManager', `模块 ${name} 初始化完成`);
856
+ }
857
+ catch (error) {
858
+ const errorObj = error instanceof Error ? error : new Error(String(error));
859
+ this.logger.error('ModuleManager', `模块 ${name} 初始化失败`, errorObj);
860
+ this.emit('module:error', { name, error });
861
+ throw new Error(`模块 ${name} 初始化失败: ${error instanceof Error ? error.message : String(error)}`);
862
+ }
863
+ }
864
+ this.initialized = true;
865
+ this.logger.debug('ModuleManager', '所有模块初始化完成');
866
+ this.emit('modules:initialized');
867
+ }
868
+ /**
869
+ * 卸载所有模块并释放资源
870
+ */
871
+ async dispose() {
872
+ this.logger.debug('ModuleManager', '开始释放所有模块资源...');
873
+ for (const [name, module] of this.modules.entries()) {
874
+ try {
875
+ this.logger.debug('ModuleManager', `释放模块资源: ${name}`);
876
+ await module.dispose();
877
+ this.emit('module:disposed', { name });
878
+ }
879
+ catch (error) {
880
+ const errorObj = error instanceof Error ? error : new Error(String(error));
881
+ this.logger.error('ModuleManager', `模块 ${name} 资源释放失败`, errorObj);
882
+ this.emit('module:error', { name, error });
883
+ }
884
+ }
885
+ this.modules.clear();
886
+ this.initialized = false;
887
+ this.logger.debug('ModuleManager', '所有模块资源已释放');
888
+ this.emit('modules:disposed');
889
+ }
890
+ /**
891
+ * 获取所有已注册的模块名称
892
+ */
893
+ getRegisteredModules() {
894
+ return Array.from(this.modules.keys());
895
+ }
896
+ /**
897
+ * 检查模块是否已注册
898
+ * @param name 模块名称
899
+ */
900
+ hasModule(name) {
901
+ return this.modules.has(name);
902
+ }
903
+ }
904
+
905
+ /**
906
+ * @file 基础模块
907
+ * @description 提供基础模块实现,作为所有功能模块的基类
908
+ * @module core/base-module
909
+ */
910
+ /**
911
+ * 基础模块类
912
+ * 提供模块的基本功能和生命周期管理
913
+ */
914
+ class BaseModule extends EventEmitter {
915
+ /**
916
+ * 构造函数
917
+ */
918
+ constructor() {
919
+ super();
920
+ /** 模块版本 */
921
+ this.version = VERSION;
922
+ /** 模块是否已初始化 */
923
+ this._isInitialized = false;
924
+ this.logger = Logger.getInstance();
925
+ }
926
+ /**
927
+ * 获取模块是否已初始化
928
+ */
929
+ get isInitialized() {
930
+ return this._isInitialized;
931
+ }
932
+ /**
933
+ * 释放模块资源
934
+ * 子类可以覆盖此方法以添加额外的资源释放逻辑
935
+ */
936
+ async dispose() {
937
+ if (!this._isInitialized) {
938
+ return;
939
+ }
940
+ this.logger.debug(this.name, '释放模块资源');
941
+ // 重置初始化状态
942
+ this._isInitialized = false;
943
+ // 删除所有事件监听器
944
+ this.removeAllListeners();
945
+ this.logger.debug(this.name, '模块资源已释放');
946
+ }
947
+ /**
948
+ * 检查模块是否已初始化,如果未初始化则抛出错误
949
+ */
950
+ ensureInitialized() {
951
+ if (!this._isInitialized) {
952
+ throw new Error(`模块 ${this.name} 尚未初始化`);
953
+ }
954
+ }
955
+ }
956
+
957
+ /**
958
+ * @file 结果包装类
959
+ * @description 提供统一的操作结果封装
960
+ * @module core/result
961
+ */
962
+ /**
963
+ * 结果类型
964
+ * 用于封装操作的成功或失败结果
965
+ */
966
+ class Result {
967
+ /**
968
+ * 构造函数
969
+ * @param success 是否成功
970
+ * @param data 结果数据
971
+ * @param error 错误对象
972
+ * @param meta 元数据
973
+ */
974
+ constructor(success, data, error, meta) {
975
+ this._success = success;
976
+ this._data = data;
977
+ this._error = error;
978
+ this._meta = meta;
979
+ }
980
+ /**
981
+ * 创建成功结果
982
+ * @param data 结果数据
983
+ * @param meta 元数据
984
+ */
985
+ static success(data, meta) {
986
+ return new Result(true, data, undefined, meta);
987
+ }
988
+ /**
989
+ * 创建失败结果
990
+ * @param error 错误对象
991
+ * @param meta 元数据
992
+ */
993
+ static failure(error, meta) {
994
+ return new Result(false, undefined, error, meta);
995
+ }
996
+ /**
997
+ * 检查结果是否成功
998
+ */
999
+ isSuccess() {
1000
+ return this._success;
1001
+ }
1002
+ /**
1003
+ * 检查结果是否失败
1004
+ */
1005
+ isFailure() {
1006
+ return !this._success;
1007
+ }
1008
+ /**
1009
+ * 获取结果数据
1010
+ */
1011
+ get data() {
1012
+ return this._data;
1013
+ }
1014
+ /**
1015
+ * 获取错误对象
1016
+ */
1017
+ get error() {
1018
+ return this._error;
1019
+ }
1020
+ /**
1021
+ * 获取元数据
1022
+ */
1023
+ get meta() {
1024
+ return this._meta;
1025
+ }
1026
+ /**
1027
+ * 映射结果(如果成功)
1028
+ * @param fn 映射函数
1029
+ */
1030
+ map(fn) {
1031
+ if (this.isSuccess() && this._data !== undefined) {
1032
+ try {
1033
+ const newData = fn(this._data);
1034
+ return Result.success(newData, this._meta);
1035
+ }
1036
+ catch (error) {
1037
+ return Result.failure(error instanceof Error ? error : new Error(String(error)), this._meta);
1038
+ }
1039
+ }
1040
+ return Result.failure(this._error, this._meta);
1041
+ }
1042
+ /**
1043
+ * 如果成功,则执行函数
1044
+ * @param fn 要执行的函数
1045
+ */
1046
+ onSuccess(fn) {
1047
+ if (this.isSuccess()) {
1048
+ try {
1049
+ fn(this._data);
1050
+ }
1051
+ catch (error) {
1052
+ console.error('Error in onSuccess handler:', error);
1053
+ }
1054
+ }
1055
+ return this;
1056
+ }
1057
+ /**
1058
+ * 如果失败,则执行函数
1059
+ * @param fn 要执行的函数
1060
+ */
1061
+ onFailure(fn) {
1062
+ if (this.isFailure() && this._error) {
1063
+ try {
1064
+ fn(this._error);
1065
+ }
1066
+ catch (error) {
1067
+ console.error('Error in onFailure handler:', error);
1068
+ }
1069
+ }
1070
+ return this;
1071
+ }
1072
+ /**
1073
+ * 无论成功失败,都执行函数
1074
+ * @param fn 要执行的函数
1075
+ */
1076
+ onFinally(fn) {
1077
+ try {
1078
+ fn();
1079
+ }
1080
+ catch (error) {
1081
+ console.error('Error in onFinally handler:', error);
1082
+ }
1083
+ return this;
1084
+ }
1085
+ /**
1086
+ * 转换为字符串
1087
+ */
1088
+ toString() {
1089
+ if (this.isSuccess()) {
1090
+ return `Success: ${JSON.stringify(this._data)}`;
1091
+ }
1092
+ else {
1093
+ return `Failure: ${this._error?.message || 'Unknown error'}`;
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ /**
1099
+ * @file 身份证模块类型定义
1100
+ * @description 身份证模块相关的类型和接口定义
1101
+ * @module modules/id-card/types
1102
+ */
1103
+ /**
1104
+ * 身份证类型枚举
1105
+ */
1106
+ var IDCardType;
1107
+ (function (IDCardType) {
1108
+ /** 第二代居民身份证正面 */
1109
+ IDCardType["FRONT"] = "front";
1110
+ /** 第二代居民身份证背面 */
1111
+ IDCardType["BACK"] = "back";
1112
+ /** 第一代居民身份证 */
1113
+ IDCardType["FIRST_GENERATION"] = "first_generation";
1114
+ /** 临时身份证 */
1115
+ IDCardType["TEMPORARY"] = "temporary";
1116
+ /** 外国人永久居留证 */
1117
+ IDCardType["FOREIGN_PERMANENT"] = "foreign_permanent";
1118
+ /** 港澳台居民居住证 */
1119
+ IDCardType["HMT_RESIDENT"] = "hmt_resident";
1120
+ /** 未知类型 */
1121
+ IDCardType["UNKNOWN"] = "unknown";
1122
+ })(IDCardType || (IDCardType = {}));
1123
+
1124
+ /**
1125
+ * @file 身份证检测器
1126
+ * @description 提供身份证检测和解析功能
1127
+ * @module modules/id-card/id-card-detector
1128
+ */
1129
+ /**
1130
+ * 身份证检测器类
1131
+ */
1132
+ class IDCardDetector extends EventEmitter {
1133
+ /**
1134
+ * 构造函数
1135
+ * @param options 配置选项
1136
+ */
1137
+ constructor(options = {}) {
1138
+ super();
1139
+ this.initialized = false;
1140
+ this.models = {};
1141
+ this.options = {
1142
+ enabled: true,
1143
+ minConfidence: 0.7,
1144
+ detectType: true,
1145
+ detectEdge: true,
1146
+ enableEdgeDetection: false,
1147
+ enableOCR: true,
1148
+ cropAndAlign: true,
1149
+ enableAntiFake: false,
1150
+ returnImage: false,
1151
+ modelPath: '/models/id-card',
1152
+ ...options
1153
+ };
1154
+ this.logger = Logger.getInstance();
1155
+ }
1156
+ /**
1157
+ * 初始化检测器
1158
+ */
1159
+ async initialize() {
1160
+ if (this.initialized || !this.options.enabled) {
1161
+ return;
1162
+ }
1163
+ this.logger.debug('IDCardDetector', '初始化身份证检测器');
1164
+ try {
1165
+ // 加载检测模型
1166
+ await this.loadDetectionModel();
1167
+ // 如果启用OCR,加载OCR模型
1168
+ if (this.options.enableOCR) {
1169
+ await this.loadOCRModel();
1170
+ }
1171
+ // 如果启用防伪检测,加载防伪模型
1172
+ if (this.options.enableAntiFake) {
1173
+ await this.loadAntiFakeModel();
1174
+ }
1175
+ this.initialized = true;
1176
+ this.emit('detector:initialized', {});
1177
+ this.logger.debug('IDCardDetector', '身份证检测器初始化完成');
1178
+ }
1179
+ catch (error) {
1180
+ this.logger.error('IDCardDetector', '身份证检测器初始化失败', error);
1181
+ throw error;
1182
+ }
1183
+ }
1184
+ /**
1185
+ * 加载检测模型
1186
+ * @private
1187
+ */
1188
+ async loadDetectionModel() {
1189
+ // 实际项目中,这里应该加载检测模型
1190
+ this.logger.debug('IDCardDetector', '加载身份证检测模型');
1191
+ // 模拟加载模型的延迟
1192
+ await new Promise(resolve => setTimeout(resolve, 100));
1193
+ // 设置模型
1194
+ this.models.detection = {
1195
+ loaded: true,
1196
+ name: 'id-card-detection'
1197
+ };
1198
+ }
1199
+ /**
1200
+ * 加载OCR模型
1201
+ * @private
1202
+ */
1203
+ async loadOCRModel() {
1204
+ // 实际项目中,这里应该加载OCR模型
1205
+ this.logger.debug('IDCardDetector', '加载身份证OCR模型');
1206
+ // 模拟加载模型的延迟
1207
+ await new Promise(resolve => setTimeout(resolve, 100));
1208
+ // 设置模型
1209
+ this.models.ocr = {
1210
+ loaded: true,
1211
+ name: 'id-card-ocr'
1212
+ };
1213
+ }
1214
+ /**
1215
+ * 加载防伪模型
1216
+ * @private
1217
+ */
1218
+ async loadAntiFakeModel() {
1219
+ // 实际项目中,这里应该加载防伪模型
1220
+ this.logger.debug('IDCardDetector', '加载身份证防伪模型');
1221
+ // 模拟加载模型的延迟
1222
+ await new Promise(resolve => setTimeout(resolve, 100));
1223
+ // 设置模型
1224
+ this.models.antiFake = {
1225
+ loaded: true,
1226
+ name: 'id-card-anti-fake'
1227
+ };
1228
+ }
1229
+ /**
1230
+ * 处理图像
1231
+ * @param image 图像源(可以是ImageData、HTMLImageElement、HTMLCanvasElement等)
1232
+ * @param processOptions 图像处理选项
1233
+ * @returns 处理结果
1234
+ */
1235
+ async processImage(image, processOptions = {}) {
1236
+ if (!this.initialized) {
1237
+ return Result.failure(new Error('身份证检测器未初始化'));
1238
+ }
1239
+ try {
1240
+ this.logger.debug('IDCardDetector', '开始处理图像');
1241
+ // 预处理图像
1242
+ const processedImage = await this.preprocessImage(image, processOptions);
1243
+ // 检测身份证
1244
+ const detectionResult = await this.detectIDCard(processedImage);
1245
+ if (!detectionResult || detectionResult.confidence < (this.options.minConfidence || 0.7)) {
1246
+ return Result.failure(new Error('未检测到身份证或置信度过低'));
1247
+ }
1248
+ let idCardInfo = {
1249
+ type: detectionResult.type,
1250
+ edge: detectionResult.edge,
1251
+ confidence: detectionResult.confidence
1252
+ };
1253
+ // 如果启用OCR识别,提取文字信息
1254
+ if (this.options.enableOCR && this.models.ocr) {
1255
+ // 裁剪并校正图像
1256
+ const alignedImage = this.options.cropAndAlign ?
1257
+ await this.cropAndAlign(processedImage, detectionResult.edge) :
1258
+ processedImage;
1259
+ // 识别文字
1260
+ const ocrResult = await this.recognizeText(alignedImage, detectionResult.type);
1261
+ // 合并结果
1262
+ idCardInfo = {
1263
+ ...idCardInfo,
1264
+ ...ocrResult
1265
+ };
1266
+ }
1267
+ // 如果启用防伪检测,进行防伪检测
1268
+ if (this.options.enableAntiFake && this.models.antiFake) {
1269
+ const antiFakeResult = await this.detectAntiFake(processedImage, detectionResult);
1270
+ idCardInfo.antiFake = antiFakeResult;
1271
+ }
1272
+ // 如果需要返回原始图像
1273
+ if (this.options.returnImage) {
1274
+ // 根据图像类型获取ImageData
1275
+ if (image instanceof ImageData) {
1276
+ idCardInfo.image = image;
1277
+ }
1278
+ else if (image instanceof HTMLCanvasElement) {
1279
+ const context = image.getContext('2d');
1280
+ if (context) {
1281
+ idCardInfo.image = context.getImageData(0, 0, image.width, image.height);
1282
+ }
1283
+ }
1284
+ else if (image instanceof HTMLImageElement && image.complete) {
1285
+ const canvas = document.createElement('canvas');
1286
+ canvas.width = image.naturalWidth;
1287
+ canvas.height = image.naturalHeight;
1288
+ const context = canvas.getContext('2d');
1289
+ if (context) {
1290
+ context.drawImage(image, 0, 0);
1291
+ idCardInfo.image = context.getImageData(0, 0, canvas.width, canvas.height);
1292
+ }
1293
+ }
1294
+ }
1295
+ this.logger.debug('IDCardDetector', '图像处理完成');
1296
+ this.emit('detector:result', { result: idCardInfo });
1297
+ return Result.success(idCardInfo);
1298
+ }
1299
+ catch (error) {
1300
+ this.logger.error('IDCardDetector', '图像处理失败', error);
1301
+ return Result.failure(error);
1302
+ }
1303
+ }
1304
+ /**
1305
+ * 预处理图像
1306
+ * @param image 图像源
1307
+ * @param options 处理选项
1308
+ * @returns 处理后的图像
1309
+ * @private
1310
+ */
1311
+ async preprocessImage(image, options) {
1312
+ // 实际项目中,这里应该对图像进行预处理
1313
+ this.logger.debug('IDCardDetector', '预处理图像');
1314
+ // 创建ImageData对象
1315
+ let imageData;
1316
+ if (image instanceof ImageData) {
1317
+ imageData = image;
1318
+ }
1319
+ else {
1320
+ const canvas = document.createElement('canvas');
1321
+ const width = image instanceof HTMLImageElement ? image.naturalWidth : image.width;
1322
+ const height = image instanceof HTMLImageElement ? image.naturalHeight : image.height;
1323
+ canvas.width = width;
1324
+ canvas.height = height;
1325
+ const context = canvas.getContext('2d');
1326
+ if (!context) {
1327
+ throw new Error('无法获取Canvas上下文');
1328
+ }
1329
+ if (image instanceof HTMLImageElement) {
1330
+ context.drawImage(image, 0, 0);
1331
+ }
1332
+ else {
1333
+ context.drawImage(image, 0, 0);
1334
+ }
1335
+ imageData = context.getImageData(0, 0, width, height);
1336
+ }
1337
+ // 应用图像处理选项
1338
+ // 实际项目中,这里应该根据options进行相应的图像处理
1339
+ return imageData;
1340
+ }
1341
+ /**
1342
+ * 检测身份证
1343
+ * @param image 图像数据
1344
+ * @returns 检测结果
1345
+ * @private
1346
+ */
1347
+ async detectIDCard(image) {
1348
+ // 实际项目中,这里应该调用模型进行身份证检测
1349
+ this.logger.debug('IDCardDetector', '检测身份证');
1350
+ // 模拟检测结果
1351
+ // 在实际应用中,这里应该使用机器学习模型进行推理
1352
+ return {
1353
+ type: IDCardType.FRONT,
1354
+ edge: {
1355
+ topLeft: { x: 10, y: 10 },
1356
+ topRight: { x: image.width - 10, y: 10 },
1357
+ bottomRight: { x: image.width - 10, y: image.height - 10 },
1358
+ bottomLeft: { x: 10, y: image.height - 10 }
1359
+ },
1360
+ confidence: 0.95
1361
+ };
1362
+ }
1363
+ /**
1364
+ * 裁剪并校正图像
1365
+ * @param image 图像数据
1366
+ * @param edge 边缘信息
1367
+ * @returns 校正后的图像
1368
+ * @private
1369
+ */
1370
+ async cropAndAlign(image, edge) {
1371
+ // 实际项目中,这里应该进行透视变换以校正图像
1372
+ this.logger.debug('IDCardDetector', '裁剪并校正图像');
1373
+ // 创建Canvas
1374
+ const canvas = document.createElement('canvas');
1375
+ // 设置标准身份证尺寸比例
1376
+ canvas.width = 428;
1377
+ canvas.height = 270;
1378
+ const context = canvas.getContext('2d');
1379
+ if (!context) {
1380
+ throw new Error('无法获取Canvas上下文');
1381
+ }
1382
+ // 创建临时Canvas
1383
+ const tempCanvas = document.createElement('canvas');
1384
+ tempCanvas.width = image.width;
1385
+ tempCanvas.height = image.height;
1386
+ const tempContext = tempCanvas.getContext('2d');
1387
+ if (!tempContext) {
1388
+ throw new Error('无法获取临时Canvas上下文');
1389
+ }
1390
+ // 将ImageData绘制到临时Canvas
1391
+ tempContext.putImageData(image, 0, 0);
1392
+ // 在实际应用中,这里应该使用透视变换算法
1393
+ // 例如使用Canvas的transform或WebGL进行变换
1394
+ // 简化处理:直接裁剪
1395
+ context.drawImage(tempCanvas, edge.topLeft.x, edge.topLeft.y, edge.topRight.x - edge.topLeft.x, edge.bottomLeft.y - edge.topLeft.y, 0, 0, canvas.width, canvas.height);
1396
+ return context.getImageData(0, 0, canvas.width, canvas.height);
1397
+ }
1398
+ /**
1399
+ * 识别文字
1400
+ * @param image 图像数据
1401
+ * @param type 身份证类型
1402
+ * @returns 识别结果
1403
+ * @private
1404
+ */
1405
+ async recognizeText(image, type) {
1406
+ // 实际项目中,这里应该调用OCR模型进行文字识别
1407
+ this.logger.debug('IDCardDetector', '识别文字');
1408
+ // 模拟OCR结果
1409
+ // 在实际应用中,这里应该使用OCR模型进行文字识别
1410
+ if (type === IDCardType.FRONT) {
1411
+ return {
1412
+ name: '张三',
1413
+ gender: '男',
1414
+ ethnicity: '汉',
1415
+ birthDate: '1990-01-01',
1416
+ address: '北京市朝阳区某某街道某某社区1号楼1单元101',
1417
+ idNumber: '110101199001010001',
1418
+ photoRegion: {
1419
+ x: 300,
1420
+ y: 40,
1421
+ width: 100,
1422
+ height: 130
1423
+ }
1424
+ };
1425
+ }
1426
+ else if (type === IDCardType.BACK) {
1427
+ return {
1428
+ issueAuthority: '北京市公安局朝阳分局',
1429
+ validFrom: '2015-01-01',
1430
+ validTo: '2035-01-01'
1431
+ };
1432
+ }
1433
+ return {};
1434
+ }
1435
+ /**
1436
+ * 检测防伪特征
1437
+ * @param image 图像数据
1438
+ * @param detectionResult 检测结果
1439
+ * @returns 防伪检测结果
1440
+ * @private
1441
+ */
1442
+ async detectAntiFake(image, detectionResult) {
1443
+ // 实际项目中,这里应该调用防伪模型进行特征检测
1444
+ this.logger.debug('IDCardDetector', '检测防伪特征');
1445
+ // 模拟防伪检测结果
1446
+ // 在实际应用中,这里应该使用机器学习模型检测防伪特征
1447
+ return {
1448
+ passed: true,
1449
+ score: 0.92,
1450
+ features: {
1451
+ fluorescent: true,
1452
+ microtext: true,
1453
+ opticalVariable: true,
1454
+ texture: true,
1455
+ watermark: true
1456
+ }
1457
+ };
1458
+ }
1459
+ /**
1460
+ * 释放资源
1461
+ */
1462
+ dispose() {
1463
+ this.logger.debug('IDCardDetector', '释放资源');
1464
+ // 清理模型
1465
+ this.models = {};
1466
+ this.initialized = false;
1467
+ // 清理事件监听
1468
+ this.removeAllListeners();
1469
+ }
1470
+ }
1471
+
1472
+ /**
1473
+ * @file 图像处理工具类
1474
+ * @description 提供图像预处理功能,用于提高OCR识别率
1475
+ * @module ImageProcessor
1476
+ * @version 1.3.2
1477
+ */
1478
+ /**
1479
+ * 图像处理工具类
1480
+ *
1481
+ * 提供各种图像处理功能,用于优化识别效果
1482
+ */
1483
+ class ImageProcessor {
1484
+ /**
1485
+ * 将ImageData转换为Canvas元素
1486
+ *
1487
+ * @param {ImageData} imageData - 要转换的图像数据
1488
+ * @returns {HTMLCanvasElement} 包含图像的Canvas元素
1489
+ */
1490
+ static imageDataToCanvas(imageData) {
1491
+ const canvas = document.createElement("canvas");
1492
+ canvas.width = imageData.width;
1493
+ canvas.height = imageData.height;
1494
+ const ctx = canvas.getContext("2d");
1495
+ if (ctx) {
1496
+ ctx.putImageData(imageData, 0, 0);
1497
+ }
1498
+ return canvas;
1499
+ }
1500
+ /**
1501
+ * 将Canvas转换为ImageData
1502
+ *
1503
+ * @param {HTMLCanvasElement} canvas - 要转换的Canvas元素
1504
+ * @returns {ImageData|null} Canvas的图像数据,如果获取失败则返回null
1505
+ */
1506
+ static canvasToImageData(canvas) {
1507
+ const ctx = canvas.getContext("2d");
1508
+ return ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
1509
+ }
1510
+ /**
1511
+ * 调整图像亮度和对比度
1512
+ *
1513
+ * @param imageData 原始图像数据
1514
+ * @param brightness 亮度调整值 (-100到100)
1515
+ * @param contrast 对比度调整值 (-100到100)
1516
+ * @returns 处理后的图像数据
1517
+ */
1518
+ static adjustBrightnessContrast(imageData, brightness = 0, contrast = 0) {
1519
+ // 将亮度和对比度范围限制在 -100 到 100 之间
1520
+ brightness = Math.max(-100, Math.min(100, brightness));
1521
+ contrast = Math.max(-100, Math.min(100, contrast));
1522
+ // 将范围转换为适合计算的值
1523
+ const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
1524
+ const briAdjust = (brightness / 100) * 255;
1525
+ const data = imageData.data;
1526
+ const length = data.length;
1527
+ for (let i = 0; i < length; i += 4) {
1528
+ // 分别处理 RGB 三个通道
1529
+ for (let j = 0; j < 3; j++) {
1530
+ // 应用亮度和对比度调整公式
1531
+ const newValue = factor * (data[i + j] + briAdjust - 128) + 128;
1532
+ data[i + j] = Math.max(0, Math.min(255, newValue));
1533
+ }
1534
+ // Alpha 通道保持不变
1535
+ }
1536
+ return imageData;
1537
+ }
1538
+ /**
1539
+ * 将图像转换为灰度图
1540
+ *
1541
+ * @param imageData 原始图像数据
1542
+ * @returns 灰度图像数据
1543
+ */
1544
+ static toGrayscale(imageData) {
1545
+ const data = imageData.data;
1546
+ const length = data.length;
1547
+ for (let i = 0; i < length; i += 4) {
1548
+ // 使用加权平均法将 RGB 转换为灰度值
1549
+ const gray = data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11;
1550
+ data[i] = data[i + 1] = data[i + 2] = gray;
1551
+ }
1552
+ return imageData;
1553
+ }
1554
+ /**
1555
+ * 锐化图像
1556
+ *
1557
+ * @param imageData 原始图像数据
1558
+ * @param amount 锐化程度,默认为2
1559
+ * @returns 锐化后的图像数据
1560
+ */
1561
+ static sharpen(imageData, amount = 2) {
1562
+ if (!imageData || !imageData.data)
1563
+ return imageData;
1564
+ const width = imageData.width;
1565
+ const height = imageData.height;
1566
+ const data = imageData.data;
1567
+ const outputData = new Uint8ClampedArray(data.length);
1568
+ // 锐化卷积核
1569
+ const kernel = [
1570
+ 0,
1571
+ -amount,
1572
+ 0,
1573
+ -amount,
1574
+ 1 + 4 * amount,
1575
+ -amount,
1576
+ 0,
1577
+ -amount,
1578
+ 0,
1579
+ ];
1580
+ // 应用卷积
1581
+ for (let y = 1; y < height - 1; y++) {
1582
+ for (let x = 1; x < width - 1; x++) {
1583
+ const pos = (y * width + x) * 4;
1584
+ // 对每个通道应用卷积
1585
+ for (let c = 0; c < 3; c++) {
1586
+ let val = 0;
1587
+ for (let ky = -1; ky <= 1; ky++) {
1588
+ for (let kx = -1; kx <= 1; kx++) {
1589
+ const kernelPos = (ky + 1) * 3 + (kx + 1);
1590
+ const dataPos = ((y + ky) * width + (x + kx)) * 4 + c;
1591
+ val += data[dataPos] * kernel[kernelPos];
1592
+ }
1593
+ }
1594
+ outputData[pos + c] = Math.max(0, Math.min(255, val));
1595
+ }
1596
+ outputData[pos + 3] = data[pos + 3]; // 保持透明度不变
1597
+ }
1598
+ }
1599
+ // 处理边缘像素
1600
+ for (let y = 0; y < height; y++) {
1601
+ for (let x = 0; x < width; x++) {
1602
+ if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
1603
+ const pos = (y * width + x) * 4;
1604
+ outputData[pos] = data[pos];
1605
+ outputData[pos + 1] = data[pos + 1];
1606
+ outputData[pos + 2] = data[pos + 2];
1607
+ outputData[pos + 3] = data[pos + 3];
1608
+ }
1609
+ }
1610
+ }
1611
+ // 创建新的ImageData对象
1612
+ return new ImageData(outputData, width, height);
1613
+ }
1614
+ /**
1615
+ * 对图像应用阈值操作,增强对比度
1616
+ *
1617
+ * @param imageData 原始图像数据
1618
+ * @param threshold 阈值 (0-255)
1619
+ * @returns 处理后的图像数据
1620
+ */
1621
+ static threshold(imageData, threshold = 128) {
1622
+ // 先转换为灰度图
1623
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
1624
+ const data = grayscaleImage.data;
1625
+ const length = data.length;
1626
+ for (let i = 0; i < length; i += 4) {
1627
+ // 二值化处理
1628
+ const value = data[i] < threshold ? 0 : 255;
1629
+ data[i] = data[i + 1] = data[i + 2] = value;
1630
+ }
1631
+ return grayscaleImage;
1632
+ }
1633
+ /**
1634
+ * 将图像转换为黑白图像(二值化)
1635
+ *
1636
+ * @param imageData 原始图像数据
1637
+ * @returns 二值化后的图像数据
1638
+ */
1639
+ static toBinaryImage(imageData) {
1640
+ // 先转换为灰度图
1641
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
1642
+ // 使用OTSU算法自动确定阈值
1643
+ const threshold = this.getOtsuThreshold(grayscaleImage);
1644
+ return this.threshold(grayscaleImage, threshold);
1645
+ }
1646
+ /**
1647
+ * 使用OTSU算法计算最佳阈值
1648
+ *
1649
+ * @param imageData 灰度图像数据
1650
+ * @returns 最佳阈值
1651
+ */
1652
+ static getOtsuThreshold(imageData) {
1653
+ const data = imageData.data;
1654
+ const histogram = new Array(256).fill(0);
1655
+ // 统计灰度直方图
1656
+ for (let i = 0; i < data.length; i += 4) {
1657
+ histogram[data[i]]++;
1658
+ }
1659
+ const total = imageData.width * imageData.height;
1660
+ let sum = 0;
1661
+ // 计算总灰度值和
1662
+ for (let i = 0; i < 256; i++) {
1663
+ sum += i * histogram[i];
1664
+ }
1665
+ let sumB = 0;
1666
+ let wB = 0;
1667
+ let wF = 0;
1668
+ let maxVariance = 0;
1669
+ let threshold = 0;
1670
+ // 遍历所有可能的阈值,找到最大类间方差
1671
+ for (let t = 0; t < 256; t++) {
1672
+ wB += histogram[t]; // 背景权重
1673
+ if (wB === 0)
1674
+ continue;
1675
+ wF = total - wB; // 前景权重
1676
+ if (wF === 0)
1677
+ break;
1678
+ sumB += t * histogram[t];
1679
+ const mB = sumB / wB; // 背景平均灰度
1680
+ const mF = (sum - sumB) / wF; // 前景平均灰度
1681
+ // 计算类间方差
1682
+ const variance = wB * wF * (mB - mF) * (mB - mF);
1683
+ if (variance > maxVariance) {
1684
+ maxVariance = variance;
1685
+ threshold = t;
1686
+ }
1687
+ }
1688
+ return threshold;
1689
+ }
1690
+ /**
1691
+ * 批量应用图像处理
1692
+ *
1693
+ * @param imageData 原始图像数据
1694
+ * @param options 处理选项
1695
+ * @returns 处理后的图像数据
1696
+ */
1697
+ static batchProcess(imageData, options) {
1698
+ let processedImage = new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height);
1699
+ // 应用亮度和对比度调整
1700
+ if (options.brightness !== undefined || options.contrast !== undefined) {
1701
+ processedImage = this.adjustBrightnessContrast(processedImage, options.brightness || 0, options.contrast || 0);
1702
+ }
1703
+ // 应用灰度转换
1704
+ if (options.grayscale) {
1705
+ processedImage = this.toGrayscale(processedImage);
1706
+ }
1707
+ // 应用锐化
1708
+ if (options.sharpen) {
1709
+ processedImage = this.sharpen(processedImage);
1710
+ }
1711
+ // 应用颜色反转
1712
+ if (options.invert) {
1713
+ const data = processedImage.data;
1714
+ for (let i = 0; i < data.length; i += 4) {
1715
+ // 反转RGB值
1716
+ data[i] = 255 - data[i];
1717
+ data[i + 1] = 255 - data[i + 1];
1718
+ data[i + 2] = 255 - data[i + 2];
1719
+ // Alpha通道保持不变
1720
+ }
1721
+ }
1722
+ return processedImage;
1723
+ }
1724
+ /**
1725
+ * 压缩图片文件
1726
+ *
1727
+ * @param file 图片文件
1728
+ * @param options 压缩选项
1729
+ * @returns Promise<File> 压缩后的文件
1730
+ */
1731
+ static async compressImage(file, options) {
1732
+ const defaultOptions = {
1733
+ maxSizeMB: 1,
1734
+ maxWidthOrHeight: 1920,
1735
+ useWebWorker: true,
1736
+ quality: 0.8,
1737
+ fileType: file.type || "image/jpeg",
1738
+ };
1739
+ const compressOptions = { ...defaultOptions, ...options };
1740
+ try {
1741
+ return await imageCompression(file, compressOptions);
1742
+ }
1743
+ catch (error) {
1744
+ console.error("图片压缩失败:", error);
1745
+ return file; // 如果压缩失败,返回原始文件
1746
+ }
1747
+ }
1748
+ /**
1749
+ * 从图片文件创建ImageData
1750
+ *
1751
+ * @param file 图片文件
1752
+ * @returns Promise<ImageData>
1753
+ */
1754
+ static async createImageDataFromFile(file) {
1755
+ return new Promise((resolve, reject) => {
1756
+ try {
1757
+ const img = new Image();
1758
+ const url = URL.createObjectURL(file);
1759
+ img.onload = () => {
1760
+ try {
1761
+ // 创建canvas元素
1762
+ const canvas = document.createElement("canvas");
1763
+ const ctx = canvas.getContext("2d");
1764
+ if (!ctx) {
1765
+ reject(new Error("无法创建2D上下文"));
1766
+ return;
1767
+ }
1768
+ canvas.width = img.width;
1769
+ canvas.height = img.height;
1770
+ // 绘制图片到canvas
1771
+ ctx.drawImage(img, 0, 0);
1772
+ // 获取图像数据
1773
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
1774
+ // 释放资源
1775
+ URL.revokeObjectURL(url);
1776
+ resolve(imageData);
1777
+ }
1778
+ catch (e) {
1779
+ reject(e);
1780
+ }
1781
+ };
1782
+ img.onerror = () => {
1783
+ URL.revokeObjectURL(url);
1784
+ reject(new Error("图片加载失败"));
1785
+ };
1786
+ img.src = url;
1787
+ }
1788
+ catch (error) {
1789
+ reject(error);
1790
+ }
1791
+ });
1792
+ }
1793
+ /**
1794
+ * 将ImageData转换为File对象
1795
+ *
1796
+ * @param imageData ImageData对象
1797
+ * @param fileName 输出文件名
1798
+ * @param fileType 输出文件类型
1799
+ * @param quality 图片质量 (0-1)
1800
+ * @returns Promise<File>
1801
+ */
1802
+ static async imageDataToFile(imageData, fileName = "image.jpg", fileType = "image/jpeg", quality = 0.8) {
1803
+ return new Promise((resolve, reject) => {
1804
+ try {
1805
+ const canvas = document.createElement("canvas");
1806
+ canvas.width = imageData.width;
1807
+ canvas.height = imageData.height;
1808
+ const ctx = canvas.getContext("2d");
1809
+ if (!ctx) {
1810
+ reject(new Error("无法创建2D上下文"));
1811
+ return;
1812
+ }
1813
+ ctx.putImageData(imageData, 0, 0);
1814
+ canvas.toBlob((blob) => {
1815
+ if (!blob) {
1816
+ reject(new Error("无法创建图片Blob"));
1817
+ return;
1818
+ }
1819
+ const file = new File([blob], fileName, { type: fileType });
1820
+ resolve(file);
1821
+ }, fileType, quality);
1822
+ }
1823
+ catch (error) {
1824
+ reject(error);
1825
+ }
1826
+ });
1827
+ }
1828
+ /**
1829
+ * 将图像调整到指定大小
1830
+ * @param image 输入图像
1831
+ * @param maxWidth 最大宽度
1832
+ * @param maxHeight 最大高度
1833
+ * @param keepAspectRatio 是否保持宽高比
1834
+ * @returns 调整后的图像
1835
+ */
1836
+ static resizeImage(image, maxWidth, maxHeight, keepAspectRatio = true) {
1837
+ // 创建canvas元素
1838
+ const canvas = document.createElement('canvas');
1839
+ const ctx = canvas.getContext('2d');
1840
+ if (!ctx) {
1841
+ throw new Error('无法创建Canvas上下文');
1842
+ }
1843
+ // 获取图像尺寸
1844
+ let width;
1845
+ let height;
1846
+ if (image instanceof ImageData) {
1847
+ width = image.width;
1848
+ height = image.height;
1849
+ }
1850
+ else {
1851
+ width = image.width;
1852
+ height = image.height;
1853
+ }
1854
+ // 计算调整后的尺寸
1855
+ let newWidth = width;
1856
+ let newHeight = height;
1857
+ if (keepAspectRatio) {
1858
+ if (width > height) {
1859
+ if (width > maxWidth) {
1860
+ newHeight = Math.round(height * (maxWidth / width));
1861
+ newWidth = maxWidth;
1862
+ }
1863
+ }
1864
+ else {
1865
+ if (height > maxHeight) {
1866
+ newWidth = Math.round(width * (maxHeight / height));
1867
+ newHeight = maxHeight;
1868
+ }
1869
+ }
1870
+ }
1871
+ else {
1872
+ newWidth = Math.min(width, maxWidth);
1873
+ newHeight = Math.min(height, maxHeight);
1874
+ }
1875
+ // 设置canvas尺寸
1876
+ canvas.width = newWidth;
1877
+ canvas.height = newHeight;
1878
+ // 绘制调整后的图像
1879
+ if (image instanceof ImageData) {
1880
+ // 创建临时canvas存储ImageData
1881
+ const tempCanvas = document.createElement('canvas');
1882
+ const tempCtx = tempCanvas.getContext('2d');
1883
+ if (!tempCtx) {
1884
+ throw new Error('无法创建临时Canvas上下文');
1885
+ }
1886
+ tempCanvas.width = image.width;
1887
+ tempCanvas.height = image.height;
1888
+ tempCtx.putImageData(image, 0, 0);
1889
+ // 绘制调整后的图像
1890
+ ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, newWidth, newHeight);
1891
+ }
1892
+ else {
1893
+ ctx.drawImage(image, 0, 0, width, height, 0, 0, newWidth, newHeight);
1894
+ }
1895
+ // 返回调整后的ImageData
1896
+ return ctx.getImageData(0, 0, newWidth, newHeight);
1897
+ }
1898
+ /**
1899
+ * 边缘检测算法,用于识别图像中的边缘
1900
+ * 基于Sobel算子实现
1901
+ *
1902
+ * @param imageData 原始图像数据,应已转为灰度图
1903
+ * @param threshold 边缘阈值,默认为30
1904
+ * @returns 检测到边缘的图像数据
1905
+ */
1906
+ static detectEdges(imageData, threshold = 30) {
1907
+ // 确保输入图像是灰度图
1908
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
1909
+ const width = grayscaleImage.width;
1910
+ const height = grayscaleImage.height;
1911
+ const inputData = grayscaleImage.data;
1912
+ const outputData = new Uint8ClampedArray(inputData.length);
1913
+ // Sobel算子 - 水平和垂直方向
1914
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
1915
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
1916
+ // 对每个像素应用Sobel算子
1917
+ for (let y = 1; y < height - 1; y++) {
1918
+ for (let x = 1; x < width - 1; x++) {
1919
+ let gx = 0;
1920
+ let gy = 0;
1921
+ // 应用卷积
1922
+ for (let ky = -1; ky <= 1; ky++) {
1923
+ for (let kx = -1; kx <= 1; kx++) {
1924
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
1925
+ const pixelVal = inputData[pixelPos]; // 灰度值
1926
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
1927
+ gx += pixelVal * sobelX[kernelIdx];
1928
+ gy += pixelVal * sobelY[kernelIdx];
1929
+ }
1930
+ }
1931
+ // 计算梯度强度
1932
+ let magnitude = Math.sqrt(gx * gx + gy * gy);
1933
+ // 应用阈值
1934
+ magnitude = magnitude > threshold ? 255 : 0;
1935
+ // 设置输出像素
1936
+ const pos = (y * width + x) * 4;
1937
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
1938
+ outputData[pos + 3] = 255; // 透明度保持完全不透明
1939
+ }
1940
+ }
1941
+ // 处理边缘像素
1942
+ for (let i = 0; i < width * 4; i++) {
1943
+ // 顶部和底部行
1944
+ outputData[i] = 0;
1945
+ outputData[(height - 1) * width * 4 + i] = 0;
1946
+ }
1947
+ for (let i = 0; i < height; i++) {
1948
+ // 左右两侧列
1949
+ const leftPos = i * width * 4;
1950
+ const rightPos = (i * width + width - 1) * 4;
1951
+ for (let j = 0; j < 4; j++) {
1952
+ outputData[leftPos + j] = 0;
1953
+ outputData[rightPos + j] = 0;
1954
+ }
1955
+ }
1956
+ return new ImageData(outputData, width, height);
1957
+ }
1958
+ /**
1959
+ * 卡尼-德里奇边缘检测
1960
+ * 相比Sobel更精确的边缘检测算法
1961
+ *
1962
+ * @param imageData 灰度图像数据
1963
+ * @param lowThreshold 低阈值
1964
+ * @param highThreshold 高阈值
1965
+ * @returns 边缘检测结果
1966
+ */
1967
+ static cannyEdgeDetection(imageData, lowThreshold = 20, highThreshold = 50) {
1968
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
1969
+ // 1. 高斯模糊
1970
+ const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
1971
+ // 2. 使用Sobel算子计算梯度
1972
+ const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
1973
+ // 3. 非极大值抛弃
1974
+ const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
1975
+ // 4. 双阈值处理
1976
+ const thresholdResult = this.hysteresisThresholding(nonMaxSuppressed, blurredImage.width, blurredImage.height, lowThreshold, highThreshold);
1977
+ // 创建输出图像
1978
+ const outputData = new Uint8ClampedArray(imageData.data.length);
1979
+ // 将结果转换为ImageData
1980
+ for (let i = 0; i < thresholdResult.length; i++) {
1981
+ const pos = i * 4;
1982
+ const value = thresholdResult[i] ? 255 : 0;
1983
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
1984
+ outputData[pos + 3] = 255;
1985
+ }
1986
+ return new ImageData(outputData, blurredImage.width, blurredImage.height);
1987
+ }
1988
+ /**
1989
+ * 高斯模糊
1990
+ */
1991
+ static gaussianBlur(imageData, sigma = 1.5) {
1992
+ const width = imageData.width;
1993
+ const height = imageData.height;
1994
+ const inputData = imageData.data;
1995
+ const outputData = new Uint8ClampedArray(inputData.length);
1996
+ // 生成高斯核
1997
+ const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
1998
+ const halfKernel = Math.floor(kernelSize / 2);
1999
+ const kernel = this.generateGaussianKernel(kernelSize, sigma);
2000
+ // 应用高斯核
2001
+ for (let y = 0; y < height; y++) {
2002
+ for (let x = 0; x < width; x++) {
2003
+ let sum = 0;
2004
+ let weightSum = 0;
2005
+ for (let ky = -halfKernel; ky <= halfKernel; ky++) {
2006
+ for (let kx = -halfKernel; kx <= halfKernel; kx++) {
2007
+ const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
2008
+ const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
2009
+ const pixelPos = (pixelY * width + pixelX) * 4;
2010
+ const kernelY = ky + halfKernel;
2011
+ const kernelX = kx + halfKernel;
2012
+ const weight = kernel[kernelY * kernelSize + kernelX];
2013
+ sum += inputData[pixelPos] * weight;
2014
+ weightSum += weight;
2015
+ }
2016
+ }
2017
+ const pos = (y * width + x) * 4;
2018
+ const value = Math.round(sum / weightSum);
2019
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
2020
+ outputData[pos + 3] = 255;
2021
+ }
2022
+ }
2023
+ return new ImageData(outputData, width, height);
2024
+ }
2025
+ /**
2026
+ * 生成高斯核
2027
+ */
2028
+ static generateGaussianKernel(size, sigma) {
2029
+ const kernel = new Array(size * size);
2030
+ const center = Math.floor(size / 2);
2031
+ let sum = 0;
2032
+ for (let y = 0; y < size; y++) {
2033
+ for (let x = 0; x < size; x++) {
2034
+ const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
2035
+ const value = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
2036
+ kernel[y * size + x] = value;
2037
+ sum += value;
2038
+ }
2039
+ }
2040
+ // 归一化
2041
+ for (let i = 0; i < kernel.length; i++) {
2042
+ kernel[i] /= sum;
2043
+ }
2044
+ return kernel;
2045
+ }
2046
+ /**
2047
+ * 计算梯度强度和方向
2048
+ */
2049
+ static computeGradients(imageData) {
2050
+ const width = imageData.width;
2051
+ const height = imageData.height;
2052
+ const inputData = imageData.data;
2053
+ const gradientMagnitude = new Array(width * height);
2054
+ const gradientDirection = new Array(width * height);
2055
+ // Sobel算子
2056
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
2057
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
2058
+ for (let y = 1; y < height - 1; y++) {
2059
+ for (let x = 1; x < width - 1; x++) {
2060
+ let gx = 0;
2061
+ let gy = 0;
2062
+ for (let ky = -1; ky <= 1; ky++) {
2063
+ for (let kx = -1; kx <= 1; kx++) {
2064
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
2065
+ const pixelVal = inputData[pixelPos];
2066
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
2067
+ gx += pixelVal * sobelX[kernelIdx];
2068
+ gy += pixelVal * sobelY[kernelIdx];
2069
+ }
2070
+ }
2071
+ const idx = y * width + x;
2072
+ gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
2073
+ gradientDirection[idx] = Math.atan2(gy, gx);
2074
+ }
2075
+ }
2076
+ // 处理边界
2077
+ for (let y = 0; y < height; y++) {
2078
+ for (let x = 0; x < width; x++) {
2079
+ if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
2080
+ const idx = y * width + x;
2081
+ gradientMagnitude[idx] = 0;
2082
+ gradientDirection[idx] = 0;
2083
+ }
2084
+ }
2085
+ }
2086
+ return { gradientMagnitude, gradientDirection };
2087
+ }
2088
+ /**
2089
+ * 非极大值抛弃
2090
+ */
2091
+ static nonMaxSuppression(gradientMagnitude, gradientDirection, width, height) {
2092
+ const result = new Array(width * height).fill(0);
2093
+ for (let y = 1; y < height - 1; y++) {
2094
+ for (let x = 1; x < width - 1; x++) {
2095
+ const idx = y * width + x;
2096
+ const magnitude = gradientMagnitude[idx];
2097
+ const direction = gradientDirection[idx];
2098
+ // 将方向转化为角度
2099
+ const degrees = (direction * 180 / Math.PI + 180) % 180;
2100
+ // 获取相邻像素索引
2101
+ let neighbor1Idx, neighbor2Idx;
2102
+ // 将方向量化为四个方向: 0°, 45°, 90°, 135°
2103
+ if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
2104
+ // 水平方向
2105
+ neighbor1Idx = idx - 1;
2106
+ neighbor2Idx = idx + 1;
2107
+ }
2108
+ else if (degrees >= 22.5 && degrees < 67.5) {
2109
+ // 45度方向
2110
+ neighbor1Idx = (y - 1) * width + (x + 1);
2111
+ neighbor2Idx = (y + 1) * width + (x - 1);
2112
+ }
2113
+ else if (degrees >= 67.5 && degrees < 112.5) {
2114
+ // 垂直方向
2115
+ neighbor1Idx = (y - 1) * width + x;
2116
+ neighbor2Idx = (y + 1) * width + x;
2117
+ }
2118
+ else {
2119
+ // 135度方向
2120
+ neighbor1Idx = (y - 1) * width + (x - 1);
2121
+ neighbor2Idx = (y + 1) * width + (x + 1);
2122
+ }
2123
+ // 检查当前像素是否是最大值
2124
+ if (magnitude >= gradientMagnitude[neighbor1Idx] &&
2125
+ magnitude >= gradientMagnitude[neighbor2Idx]) {
2126
+ result[idx] = magnitude;
2127
+ }
2128
+ }
2129
+ }
2130
+ return result;
2131
+ }
2132
+ /**
2133
+ * 双阈值处理
2134
+ */
2135
+ static hysteresisThresholding(nonMaxSuppressed, width, height, lowThreshold, highThreshold) {
2136
+ const result = new Array(width * height).fill(false);
2137
+ const visited = new Array(width * height).fill(false);
2138
+ const stack = [];
2139
+ // 标记强边缘点
2140
+ for (let i = 0; i < nonMaxSuppressed.length; i++) {
2141
+ if (nonMaxSuppressed[i] >= highThreshold) {
2142
+ result[i] = true;
2143
+ stack.push(i);
2144
+ visited[i] = true;
2145
+ }
2146
+ }
2147
+ // 使用深度优先搜索连接弱边缘
2148
+ const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
2149
+ const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
2150
+ while (stack.length > 0) {
2151
+ const currentIdx = stack.pop();
2152
+ const currentX = currentIdx % width;
2153
+ const currentY = Math.floor(currentIdx / width);
2154
+ // 检查88个相邻方向
2155
+ for (let i = 0; i < 8; i++) {
2156
+ const newX = currentX + dx[i];
2157
+ const newY = currentY + dy[i];
2158
+ if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
2159
+ const newIdx = newY * width + newX;
2160
+ if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
2161
+ result[newIdx] = true;
2162
+ stack.push(newIdx);
2163
+ visited[newIdx] = true;
2164
+ }
2165
+ }
2166
+ }
2167
+ }
2168
+ return result;
2169
+ }
2170
+ }
2171
+
2172
+ /**
2173
+ * @file 性能优化工具
2174
+ * @description 提供性能优化相关的工具函数
2175
+ * @module utils/performance
2176
+ */
2177
+ /**
2178
+ * LRU缓存实现
2179
+ */
2180
+ class LRUCache {
2181
+ /**
2182
+ * 构造函数
2183
+ * @param capacity 缓存容量
2184
+ */
2185
+ constructor(capacity = 100) {
2186
+ this.capacity = capacity;
2187
+ this.cache = new Map();
2188
+ }
2189
+ /**
2190
+ * 获取缓存项
2191
+ * @param key 键
2192
+ * @returns 值,如果不存在则返回undefined
2193
+ */
2194
+ get(key) {
2195
+ if (!this.cache.has(key)) {
2196
+ return undefined;
2197
+ }
2198
+ // 获取值
2199
+ const value = this.cache.get(key);
2200
+ // 删除旧位置
2201
+ this.cache.delete(key);
2202
+ // 添加到最新位置
2203
+ this.cache.set(key, value);
2204
+ return value;
2205
+ }
2206
+ /**
2207
+ * 设置缓存项
2208
+ * @param key 键
2209
+ * @param value 值
2210
+ */
2211
+ set(key, value) {
2212
+ // 如果已存在,先删除
2213
+ if (this.cache.has(key)) {
2214
+ this.cache.delete(key);
2215
+ }
2216
+ // 如果缓存已满,删除最旧的项
2217
+ else if (this.cache.size >= this.capacity) {
2218
+ this.cache.delete(this.cache.keys().next().value);
2219
+ }
2220
+ // 添加新项
2221
+ this.cache.set(key, value);
2222
+ }
2223
+ /**
2224
+ * 检查键是否存在
2225
+ * @param key 键
2226
+ * @returns 是否存在
2227
+ */
2228
+ has(key) {
2229
+ return this.cache.has(key);
2230
+ }
2231
+ /**
2232
+ * 删除缓存项
2233
+ * @param key 键
2234
+ * @returns 是否成功删除
2235
+ */
2236
+ delete(key) {
2237
+ return this.cache.delete(key);
2238
+ }
2239
+ /**
2240
+ * 清空缓存
2241
+ */
2242
+ clear() {
2243
+ this.cache.clear();
2244
+ }
2245
+ /**
2246
+ * 获取缓存大小
2247
+ */
2248
+ get size() {
2249
+ return this.cache.size;
2250
+ }
2251
+ /**
2252
+ * 获取所有键
2253
+ */
2254
+ keys() {
2255
+ return this.cache.keys();
2256
+ }
2257
+ /**
2258
+ * 获取所有值
2259
+ */
2260
+ values() {
2261
+ return this.cache.values();
2262
+ }
2263
+ /**
2264
+ * 获取所有项
2265
+ */
2266
+ entries() {
2267
+ return this.cache.entries();
2268
+ }
2269
+ }
2270
+ /**
2271
+ * 计算图像指纹
2272
+ * @param imageData 图像数据
2273
+ * @returns 图像指纹
2274
+ */
2275
+ function calculateImageFingerprint(imageData) {
2276
+ const { width, height, data } = imageData;
2277
+ // 缩小图像以加快计算速度
2278
+ const scale = Math.min(1, 32 / Math.max(width, height));
2279
+ const scaledWidth = Math.max(8, Math.floor(width * scale));
2280
+ const scaledHeight = Math.max(8, Math.floor(height * scale));
2281
+ // 创建缩小的图像
2282
+ const canvas = document.createElement('canvas');
2283
+ const ctx = canvas.getContext('2d');
2284
+ if (!ctx) {
2285
+ throw new Error('无法创建Canvas上下文');
2286
+ }
2287
+ // 设置canvas尺寸
2288
+ canvas.width = scaledWidth;
2289
+ canvas.height = scaledHeight;
2290
+ // 创建临时canvas存储原始ImageData
2291
+ const tempCanvas = document.createElement('canvas');
2292
+ const tempCtx = tempCanvas.getContext('2d');
2293
+ if (!tempCtx) {
2294
+ throw new Error('无法创建临时Canvas上下文');
2295
+ }
2296
+ tempCanvas.width = width;
2297
+ tempCanvas.height = height;
2298
+ tempCtx.putImageData(imageData, 0, 0);
2299
+ // 绘制缩小的图像
2300
+ ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, scaledWidth, scaledHeight);
2301
+ // 获取缩小的图像数据
2302
+ const scaledImageData = ctx.getImageData(0, 0, scaledWidth, scaledHeight);
2303
+ // 计算灰度值
2304
+ const grayValues = new Uint8Array(scaledWidth * scaledHeight);
2305
+ for (let i = 0; i < scaledWidth * scaledHeight; i++) {
2306
+ const idx = i * 4;
2307
+ grayValues[i] = Math.round(0.299 * scaledImageData.data[idx] +
2308
+ 0.587 * scaledImageData.data[idx + 1] +
2309
+ 0.114 * scaledImageData.data[idx + 2]);
2310
+ }
2311
+ // 计算平均值
2312
+ let sum = 0;
2313
+ for (let i = 0; i < grayValues.length; i++) {
2314
+ sum += grayValues[i];
2315
+ }
2316
+ const avg = sum / grayValues.length;
2317
+ // 计算哈希值
2318
+ let hash = '';
2319
+ for (let i = 0; i < grayValues.length; i++) {
2320
+ hash += grayValues[i] >= avg ? '1' : '0';
2321
+ }
2322
+ return hash;
2323
+ }
2324
+
2325
+ /**
2326
+ * @file Worker工具
2327
+ * @description 提供Web Worker相关的工具函数
2328
+ * @module utils/worker
2329
+ */
2330
+ /**
2331
+ * 检查是否支持Web Worker
2332
+ * @returns 是否支持Web Worker
2333
+ */
2334
+ function isWorkerSupported() {
2335
+ return typeof Worker !== 'undefined';
2336
+ }
2337
+ /**
2338
+ * 创建Worker
2339
+ * @param workerFunction Worker函数
2340
+ * @returns Worker实例
2341
+ */
2342
+ function createWorker(workerFunction) {
2343
+ // 检查是否支持Web Worker
2344
+ if (!isWorkerSupported()) {
2345
+ // 回退到主线程执行
2346
+ return {
2347
+ postMessage: async (input) => {
2348
+ return await Promise.resolve(workerFunction(input));
2349
+ },
2350
+ terminate: () => { }
2351
+ };
2352
+ }
2353
+ // 将函数转换为字符串
2354
+ const workerFunctionStr = workerFunction.toString();
2355
+ // 创建Worker脚本
2356
+ const workerScript = `
2357
+ // 定义Worker函数
2358
+ const workerFunction = ${workerFunctionStr};
2359
+
2360
+ // 监听消息
2361
+ self.addEventListener('message', async (event) => {
2362
+ try {
2363
+ const input = event.data;
2364
+ const result = await workerFunction(input);
2365
+ self.postMessage({ success: true, result });
2366
+ } catch (error) {
2367
+ self.postMessage({
2368
+ success: false,
2369
+ error: error instanceof Error ? error.message : String(error)
2370
+ });
2371
+ }
2372
+ });
2373
+ `;
2374
+ // 创建Blob URL
2375
+ const blob = new Blob([workerScript], { type: 'application/javascript' });
2376
+ const url = URL.createObjectURL(blob);
2377
+ // 创建Worker
2378
+ const worker = new Worker(url);
2379
+ // 创建Promise映射
2380
+ const promiseMap = new Map();
2381
+ // 消息计数器
2382
+ let messageCounter = 0;
2383
+ // 监听Worker消息
2384
+ worker.addEventListener('message', (event) => {
2385
+ const { messageId, success, result, error } = event.data;
2386
+ // 查找对应的Promise
2387
+ const promiseHandlers = promiseMap.get(messageId);
2388
+ if (promiseHandlers) {
2389
+ if (success) {
2390
+ promiseHandlers.resolve(result);
2391
+ }
2392
+ else {
2393
+ promiseHandlers.reject(new Error(error));
2394
+ }
2395
+ // 删除Promise映射
2396
+ promiseMap.delete(messageId);
2397
+ }
2398
+ });
2399
+ // 返回Worker接口
2400
+ return {
2401
+ postMessage: (input) => {
2402
+ return new Promise((resolve, reject) => {
2403
+ // 生成消息ID
2404
+ const messageId = messageCounter++;
2405
+ // 保存Promise处理函数
2406
+ promiseMap.set(messageId, { resolve, reject });
2407
+ // 发送消息到Worker
2408
+ worker.postMessage({ messageId, input });
2409
+ });
2410
+ },
2411
+ terminate: () => {
2412
+ // 终止Worker
2413
+ worker.terminate();
2414
+ // 释放Blob URL
2415
+ URL.revokeObjectURL(url);
2416
+ // 拒绝所有未完成的Promise
2417
+ for (const [, { reject }] of promiseMap) {
2418
+ reject(new Error('Worker已终止'));
2419
+ }
2420
+ // 清空Promise映射
2421
+ promiseMap.clear();
2422
+ }
2423
+ };
2424
+ }
2425
+
2426
+ /**
2427
+ * @file OCR Worker
2428
+ * @description OCR处理的Worker线程实现
2429
+ * @module modules/id-card/ocr-worker
2430
+ */
2431
+ /**
2432
+ * 在Worker中处理OCR识别
2433
+ * @param input OCR处理输入参数
2434
+ * @returns OCR处理结果
2435
+ */
2436
+ async function processOCRInWorker(input) {
2437
+ const startTime = performance.now();
2438
+ try {
2439
+ // 导入Tesseract.js
2440
+ const { createWorker } = await import('tesseract.js');
2441
+ // 创建Tesseract Worker
2442
+ const worker = createWorker({
2443
+ logger: input.tessWorkerOptions?.logger
2444
+ });
2445
+ // 初始化Worker
2446
+ await worker.load();
2447
+ await worker.loadLanguage('chi_sim');
2448
+ await worker.initialize('chi_sim');
2449
+ // 设置识别参数
2450
+ await worker.setParameters({
2451
+ tessedit_char_whitelist: '0123456789X年月日壹贰叁肆伍陆柒捌玖拾民族汉满回维吾尔藏苗彝壮朝鲜侗瑶白土家哈尼哈萨克傣黎傈僳佤高山拉祜水东乡纳西景颇柯尔克孜达斡尔仫佬羌布朗撒拉毛南仡佬锡伯阿昌普米塔吉克怒乌孜别克俄罗斯鄂温克德昂保安裕固京塔塔尔独龙鄂伦春赫哲门巴珞巴基诺男女住址出生公民身份号码签发机关有效期省市区县乡镇街道号楼单元室ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
2452
+ tessedit_pageseg_mode: 7, // PSM_SINGLE_LINE
2453
+ preserve_interword_spaces: '1'
2454
+ });
2455
+ // 识别图像
2456
+ const { data } = await worker.recognize(input.imageBase64);
2457
+ // 解析身份证信息
2458
+ const idCardInfo = parseIDCardText(data.text);
2459
+ // 释放Worker资源
2460
+ await worker.terminate();
2461
+ const processingTime = performance.now() - startTime;
2462
+ return { idCardInfo, processingTime };
2463
+ }
2464
+ catch (error) {
2465
+ console.error('OCR处理错误:', error);
2466
+ return {
2467
+ idCardInfo: {},
2468
+ processingTime: performance.now() - startTime
2469
+ };
2470
+ }
2471
+ }
2472
+ /**
2473
+ * 解析身份证文本
2474
+ * @param text OCR识别的文本
2475
+ * @returns 解析后的身份证信息
2476
+ */
2477
+ function parseIDCardText(text) {
2478
+ const info = {};
2479
+ // 预处理文本,清除多余空白
2480
+ const processedText = text.replace(/\s+/g, ' ').trim();
2481
+ // 解析身份证号码
2482
+ const idNumberRegex = /(\d{17}[\dX])/;
2483
+ const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/;
2484
+ const basicMatch = processedText.match(idNumberRegex);
2485
+ const prefixMatch = processedText.match(idNumberWithPrefixRegex);
2486
+ if (prefixMatch && prefixMatch[1]) {
2487
+ info.idNumber = prefixMatch[1];
2488
+ }
2489
+ else if (basicMatch && basicMatch[1]) {
2490
+ info.idNumber = basicMatch[1];
2491
+ }
2492
+ // 解析姓名
2493
+ const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/;
2494
+ const nameMatch = processedText.match(nameWithLabelRegex);
2495
+ if (nameMatch && nameMatch[1]) {
2496
+ info.name = nameMatch[1].trim();
2497
+ }
2498
+ else {
2499
+ // 备用方案:查找短行且内容全是汉字
2500
+ const lines = processedText.split('\n').filter(line => line.trim());
2501
+ for (const line of lines) {
2502
+ if (line.length >= 2 &&
2503
+ line.length <= 5 &&
2504
+ /^[一-龥]+$/.test(line) &&
2505
+ !/性别|民族|住址|公民|签发|有效/.test(line)) {
2506
+ info.name = line.trim();
2507
+ break;
2508
+ }
2509
+ }
2510
+ }
2511
+ // 解析性别和民族
2512
+ const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/;
2513
+ const genderOnlyRegex = /性别[\s\:]*([男女])/;
2514
+ const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/;
2515
+ const genderNationalityMatch = processedText.match(genderAndNationalityRegex);
2516
+ const genderOnlyMatch = processedText.match(genderOnlyRegex);
2517
+ const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex);
2518
+ if (genderNationalityMatch) {
2519
+ info.gender = genderNationalityMatch[1];
2520
+ info.ethnicity = genderNationalityMatch[2];
2521
+ }
2522
+ else {
2523
+ if (genderOnlyMatch)
2524
+ info.gender = genderOnlyMatch[1];
2525
+ if (nationalityOnlyMatch)
2526
+ info.ethnicity = nationalityOnlyMatch[1];
2527
+ }
2528
+ // 根据内容判断身份证类型
2529
+ if (processedText.includes('出生') || processedText.includes('公民身份号码')) {
2530
+ info.type = IDCardType.FRONT; // 确保类型为枚举值而不是字符串
2531
+ }
2532
+ else if (processedText.includes('签发机关') || processedText.includes('有效期')) {
2533
+ info.type = IDCardType.BACK; // 确保类型为枚举值而不是字符串
2534
+ }
2535
+ // 解析出生日期
2536
+ const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/;
2537
+ const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/;
2538
+ const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/;
2539
+ let birthDateMatch = processedText.match(birthDateRegex1) ||
2540
+ processedText.match(birthDateRegex2) ||
2541
+ processedText.match(birthDateRegex3);
2542
+ if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
2543
+ const year = info.idNumber.substring(6, 10);
2544
+ const month = info.idNumber.substring(10, 12);
2545
+ const day = info.idNumber.substring(12, 14);
2546
+ info.birthDate = `${year}-${month}-${day}`;
2547
+ }
2548
+ else if (birthDateMatch) {
2549
+ const year = birthDateMatch[1];
2550
+ const month = birthDateMatch[2].padStart(2, '0');
2551
+ const day = birthDateMatch[3].padStart(2, '0');
2552
+ info.birthDate = `${year}-${month}-${day}`;
2553
+ }
2554
+ // 解析地址
2555
+ const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/;
2556
+ const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/;
2557
+ const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2);
2558
+ if (addressMatch && addressMatch[1]) {
2559
+ info.address = addressMatch[1]
2560
+ .replace(/\s+/g, '')
2561
+ .replace(/\n/g, '')
2562
+ .trim();
2563
+ if (info.address.length > 70) {
2564
+ info.address = info.address.substring(0, 70);
2565
+ }
2566
+ if (!/[一-龥]/.test(info.address)) {
2567
+ info.address = '';
2568
+ }
2569
+ }
2570
+ // 解析签发机关
2571
+ const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/;
2572
+ const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/;
2573
+ const authorityMatch = processedText.match(authorityRegex1) ||
2574
+ processedText.match(authorityRegex2);
2575
+ if (authorityMatch && authorityMatch[1]) {
2576
+ info.issueAuthority = authorityMatch[1]
2577
+ .replace(/\s+/g, '')
2578
+ .replace(/\n/g, '')
2579
+ .trim();
2580
+ }
2581
+ // 解析有效期限
2582
+ const validPeriodRegex1 = /有效期限[\s\:]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日\s]*)[-\s]*(至|-)[-\s]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日]*|[永久长期]*)/;
2583
+ const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/;
2584
+ const validPeriodMatch = processedText.match(validPeriodRegex1) ||
2585
+ processedText.match(validPeriodRegex2);
2586
+ if (validPeriodMatch) {
2587
+ if (validPeriodMatch[1] && validPeriodMatch[3]) {
2588
+ const startDate = formatDateString(validPeriodMatch[1]);
2589
+ const endDate = /\d/.test(validPeriodMatch[3])
2590
+ ? formatDateString(validPeriodMatch[3])
2591
+ : '长期有效';
2592
+ info.validFrom = startDate;
2593
+ info.validTo = endDate;
2594
+ info.validPeriod = `${startDate}-${endDate}`;
2595
+ }
2596
+ else {
2597
+ info.validPeriod = validPeriodMatch[0].replace('有效期限', '').trim();
2598
+ }
2599
+ }
2600
+ return info;
2601
+ }
2602
+ /**
2603
+ * 格式化日期字符串
2604
+ * @param dateStr 原始日期字符串
2605
+ * @returns 格式化后的日期字符串
2606
+ */
2607
+ function formatDateString(dateStr) {
2608
+ // 提取年月日
2609
+ const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/);
2610
+ if (dateMatch) {
2611
+ const year = dateMatch[1];
2612
+ const month = dateMatch[2].padStart(2, '0');
2613
+ const day = dateMatch[3].padStart(2, '0');
2614
+ return `${year}-${month}-${day}`;
2615
+ }
2616
+ // 纯数字格式如 20220101
2617
+ if (/^\d{8}$/.test(dateStr)) {
2618
+ const year = dateStr.substring(0, 4);
2619
+ const month = dateStr.substring(4, 6);
2620
+ const day = dateStr.substring(6, 8);
2621
+ return `${year}-${month}-${day}`;
2622
+ }
2623
+ // 无法格式化,返回原始字符串
2624
+ return dateStr;
2625
+ }
2626
+
2627
+ /**
2628
+ * @file OCR处理器
2629
+ * @description 提供身份证OCR识别功能
2630
+ * @module modules/id-card/ocr-processor
2631
+ */
2632
+ /**
2633
+ * OCR处理器类
2634
+ *
2635
+ * 使用Tesseract.js实现对身份证图像的OCR文字识别和信息提取功能
2636
+ *
2637
+ * @example
2638
+ * ```typescript
2639
+ * // 创建OCR处理器
2640
+ * const ocrProcessor = new OCRProcessor();
2641
+ *
2642
+ * // 初始化OCR引擎
2643
+ * await ocrProcessor.initialize();
2644
+ *
2645
+ * // 处理身份证图像
2646
+ * const idInfo = await ocrProcessor.processIDCard(idCardImageData);
2647
+ * console.log('识别到的身份证信息:', idInfo);
2648
+ *
2649
+ * // 使用结束后释放资源
2650
+ * await ocrProcessor.terminate();
2651
+ * ```
2652
+ */
2653
+ class OCRProcessor {
2654
+ /**
2655
+ * 创建OCR处理器实例
2656
+ *
2657
+ * @param options OCR处理器选项
2658
+ */
2659
+ constructor(options = {}) {
2660
+ this.worker = null; // 使用导入的 TesseractWorker 类型
2661
+ this.ocrWorker = null;
2662
+ this.initialized = false;
2663
+ this.options = {
2664
+ useWorker: isWorkerSupported(),
2665
+ enableCache: true,
2666
+ cacheSize: 50,
2667
+ maxImageDimension: 1000,
2668
+ logger: console.log,
2669
+ ...options,
2670
+ };
2671
+ // 初始化缓存
2672
+ this.resultCache = new LRUCache(this.options.cacheSize);
2673
+ }
2674
+ /**
2675
+ * 初始化OCR引擎
2676
+ *
2677
+ * 加载Tesseract OCR引擎和中文简体语言包,并设置适合身份证识别的参数
2678
+ *
2679
+ * @returns {Promise<void>} 初始化完成的Promise
2680
+ */
2681
+ async initialize() {
2682
+ if (this.initialized)
2683
+ return;
2684
+ if (this.options.useWorker) {
2685
+ // 使用自定义Worker线程处理OCR
2686
+ this.ocrWorker = createWorker(processOCRInWorker); // 使用类型断言解决类型不兼容问题
2687
+ this.initialized = true;
2688
+ this.options.logger?.("OCR Worker 初始化完成");
2689
+ }
2690
+ else {
2691
+ // 使用主线程处理OCR
2692
+ this.worker = createWorker$1({
2693
+ logger: this.options.logger,
2694
+ });
2695
+ await this.worker.load();
2696
+ await this.worker.loadLanguage("chi_sim");
2697
+ await this.worker.initialize("chi_sim");
2698
+ await this.worker.setParameters({
2699
+ tessedit_char_whitelist: "0123456789X年月日壹贰叁肆伍陆柒捌玖拾民族汉满回维吾尔藏苗彝壮朝鲜侗瑶白土家哈尼哈萨克傣黎傈僳佤高山拉祜水东乡纳西景颇柯尔克孜达斡尔仫佬羌布朗撒拉毛南仡佬锡伯阿昌普米塔吉克怒乌孜别克俄罗斯鄂温克德昂保安裕固京塔塔尔独龙鄂伦春赫哲门巴珞巴基诺男女住址出生公民身份号码签发机关有效期省市区县乡镇街道号楼单元室ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", // 优化字符白名单,增加常见地址字符,移除部分不常用汉字
2700
+ });
2701
+ // 增加一些针对性的参数,提高识别率
2702
+ await this.worker.setParameters({
2703
+ tessedit_pageseg_mode: 7, // PSM_SINGLE_LINE,使用数字而不是字符串
2704
+ preserve_interword_spaces: "1", // 保留单词间的空格
2705
+ });
2706
+ this.initialized = true;
2707
+ this.options.logger?.("OCR引擎初始化完成");
2708
+ }
2709
+ }
2710
+ /**
2711
+ * 处理身份证图像并提取信息
2712
+ * @param imageData 要处理的身份证图像数据
2713
+ * @returns 提取的身份证信息
2714
+ */
2715
+ async processIDCard(imageData) {
2716
+ if (!this.initialized) {
2717
+ await this.initialize();
2718
+ }
2719
+ // 计算图像指纹,用于缓存查找
2720
+ if (this.options.enableCache) {
2721
+ const fingerprint = calculateImageFingerprint(imageData);
2722
+ // 检查缓存中是否有结果
2723
+ const cachedResult = this.resultCache.get(fingerprint);
2724
+ if (cachedResult) {
2725
+ this.options.logger?.("使用缓存的OCR结果");
2726
+ return cachedResult;
2727
+ }
2728
+ }
2729
+ // 调整图像大小以提高性能和准确性
2730
+ const downsampledImage = ImageProcessor.resizeImage(imageData, this.options.maxImageDimension || 1000, this.options.maxImageDimension || 1000, true // 保持宽高比
2731
+ );
2732
+ // 提高图像质量以获得更好的OCR结果
2733
+ const enhancedImage = ImageProcessor.batchProcess(downsampledImage, {
2734
+ brightness: this.options.brightness !== undefined ? this.options.brightness : 10, // 调整默认亮度
2735
+ contrast: this.options.contrast !== undefined ? this.options.contrast : 20, // 调整默认对比度
2736
+ sharpen: true, // 默认启用锐化,通常对OCR有益
2737
+ });
2738
+ // 转换为base64供Tesseract处理
2739
+ // 创建一个canvas元素
2740
+ const canvas = document.createElement("canvas");
2741
+ canvas.width = enhancedImage.width;
2742
+ canvas.height = enhancedImage.height;
2743
+ const ctx = canvas.getContext("2d");
2744
+ if (!ctx) {
2745
+ throw new Error("无法创建canvas上下文");
2746
+ }
2747
+ // 将ImageData绘制到canvas
2748
+ ctx.putImageData(enhancedImage, 0, 0);
2749
+ // 转换为Base64
2750
+ const base64Image = canvas.toDataURL("image/jpeg", 0.7);
2751
+ // OCR识别
2752
+ try {
2753
+ let idCardInfo;
2754
+ if (this.options.useWorker && this.ocrWorker) {
2755
+ // 使用Worker线程处理
2756
+ const result = await this.ocrWorker.postMessage({
2757
+ imageBase64: base64Image,
2758
+ // 不传递函数对象,避免DataCloneError
2759
+ tessWorkerOptions: {},
2760
+ });
2761
+ idCardInfo = result.idCardInfo;
2762
+ this.options.logger?.(`OCR处理完成,用时: ${result.processingTime.toFixed(2)}ms`);
2763
+ }
2764
+ else {
2765
+ // 使用主线程处理
2766
+ const startTime = performance.now();
2767
+ // 转换ImageData为Canvas
2768
+ const canvas = ImageProcessor.imageDataToCanvas(enhancedImage);
2769
+ // 确保worker已初始化
2770
+ if (!this.worker) {
2771
+ throw new Error("OCR引擎未初始化");
2772
+ }
2773
+ const { data } = (await this.worker.recognize(canvas));
2774
+ // 解析身份证信息
2775
+ idCardInfo = this.parseIDCardText(data.text);
2776
+ const processingTime = performance.now() - startTime;
2777
+ this.options.logger?.(`OCR处理完成,用时: ${processingTime.toFixed(2)}ms`);
2778
+ }
2779
+ // 缓存结果
2780
+ if (this.options.enableCache) {
2781
+ const fingerprint = calculateImageFingerprint(imageData);
2782
+ this.resultCache.set(fingerprint, idCardInfo);
2783
+ }
2784
+ return idCardInfo;
2785
+ }
2786
+ catch (error) {
2787
+ // 改进错误处理
2788
+ const errorMessage = error instanceof Error
2789
+ ? error.message
2790
+ : typeof error === 'object'
2791
+ ? JSON.stringify(error)
2792
+ : String(error);
2793
+ this.options.logger?.(`OCR识别错误: ${errorMessage}`);
2794
+ // 返回空对象,避免完全失败
2795
+ return {};
2796
+ }
2797
+ }
2798
+ /**
2799
+ * 解析身份证文本信息
2800
+ *
2801
+ * 从OCR识别到的文本中提取结构化的身份证信息
2802
+ *
2803
+ * @private
2804
+ * @param {string} text - OCR识别到的文本
2805
+ * @returns {IDCardInfo} 提取到的身份证信息对象
2806
+ */
2807
+ /**
2808
+ * 格式化日期字符串为标准格式 (YYYY-MM-DD)
2809
+ * @param dateStr 原始日期字符串
2810
+ * @returns 格式化后的日期字符串
2811
+ */
2812
+ formatDateString(dateStr) {
2813
+ // 先尝试提取年月日
2814
+ const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/);
2815
+ if (dateMatch) {
2816
+ const year = dateMatch[1];
2817
+ const month = dateMatch[2].padStart(2, "0");
2818
+ const day = dateMatch[3].padStart(2, "0");
2819
+ return `${year}-${month}-${day}`;
2820
+ }
2821
+ // 如果是纯数字格式如 20220101
2822
+ if (/^\d{8}$/.test(dateStr)) {
2823
+ const year = dateStr.substring(0, 4);
2824
+ const month = dateStr.substring(4, 6);
2825
+ const day = dateStr.substring(6, 8);
2826
+ return `${year}-${month}-${day}`;
2827
+ }
2828
+ // 如果无法格式化,返回原始字符串
2829
+ return dateStr;
2830
+ }
2831
+ /**
2832
+ * 验证身份证号是否符合规则
2833
+ * @param idNumber 身份证号
2834
+ * @returns 是否有效
2835
+ */
2836
+ validateIDNumber(idNumber) {
2837
+ // 基本验证,校验位有效性和长度
2838
+ if (!idNumber || idNumber.length !== 18) {
2839
+ return false;
2840
+ }
2841
+ // 检查格式,前17位必须为数字,最后一位可以是数字或'X'
2842
+ const pattern = /^\d{17}[\dX]$/;
2843
+ if (!pattern.test(idNumber)) {
2844
+ return false;
2845
+ }
2846
+ // 检查日期部分
2847
+ parseInt(idNumber.substr(6, 4));
2848
+ const month = parseInt(idNumber.substr(10, 2));
2849
+ const day = parseInt(idNumber.substr(12, 2));
2850
+ if (month < 1 || month > 12 || day < 1 || day > 31) {
2851
+ return false;
2852
+ }
2853
+ // 更详细的检查可以添加校验位的验证等逻辑...
2854
+ return true;
2855
+ }
2856
+ parseIDCardText(text) {
2857
+ const info = {};
2858
+ // 预处理文本,清除多余空白
2859
+ const processedText = text.replace(/\s+/g, " ").trim();
2860
+ // 拆分为行,并过滤空行
2861
+ const lines = processedText.split("\n").filter((line) => line.trim());
2862
+ // 解析身份证号码 - 多种模式匹配
2863
+ // 1. 普通18位身份证号模式
2864
+ const idNumberRegex = /(\d{17}[\dX])/;
2865
+ // 2. 带前缀的模式
2866
+ const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/;
2867
+ // 尝试所有模式
2868
+ let idNumber = null;
2869
+ const basicMatch = processedText.match(idNumberRegex);
2870
+ const prefixMatch = processedText.match(idNumberWithPrefixRegex);
2871
+ if (prefixMatch && prefixMatch[1]) {
2872
+ idNumber = prefixMatch[1]; // 首选带前缀的匹配,因为最可靠
2873
+ }
2874
+ else if (basicMatch && basicMatch[1]) {
2875
+ idNumber = basicMatch[1]; // 其次是常规匹配
2876
+ }
2877
+ if (idNumber) {
2878
+ info.idNumber = idNumber;
2879
+ }
2880
+ // 解析姓名 - 使用多种策略
2881
+ // 1. 直接匹配姓名标签近的内容
2882
+ const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/;
2883
+ const nameMatch = processedText.match(nameWithLabelRegex);
2884
+ // 2. 分析行文本寻找姓名
2885
+ if (nameMatch && nameMatch[1]) {
2886
+ info.name = nameMatch[1].trim();
2887
+ }
2888
+ else {
2889
+ // 备用方案:查找短行且内容全是汉字
2890
+ for (const line of lines) {
2891
+ if (line.length >= 2 &&
2892
+ line.length <= 5 &&
2893
+ /^[一-龥]+$/.test(line) &&
2894
+ !/性别|民族|住址|公民|签发|有效/.test(line)) {
2895
+ info.name = line.trim();
2896
+ break;
2897
+ }
2898
+ }
2899
+ }
2900
+ // 解析性别和民族 - 多种模式匹配
2901
+ // 1. 标准格式匹配
2902
+ const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/;
2903
+ const genderNationalityMatch = processedText.match(genderAndNationalityRegex);
2904
+ // 2. 只匹配性别
2905
+ const genderOnlyRegex = /性别[\s\:]*([男女])/;
2906
+ const genderOnlyMatch = processedText.match(genderOnlyRegex);
2907
+ // 3. 只匹配民族
2908
+ const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/;
2909
+ const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex);
2910
+ if (genderNationalityMatch) {
2911
+ info.gender = genderNationalityMatch[1];
2912
+ info.nationality = genderNationalityMatch[2];
2913
+ }
2914
+ else {
2915
+ // 分开获取
2916
+ if (genderOnlyMatch)
2917
+ info.gender = genderOnlyMatch[1];
2918
+ if (nationalityOnlyMatch)
2919
+ info.nationality = nationalityOnlyMatch[1];
2920
+ }
2921
+ // 解析出生日期 - 支持多种格式
2922
+ // 1. 标准格式:YYYY年MM月DD日
2923
+ const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/;
2924
+ // 2. 美式日期格式:YYYY-MM-DD或YYYY/MM/DD
2925
+ const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/;
2926
+ // 3. 带前缀的格式
2927
+ const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/;
2928
+ let birthDateMatch = processedText.match(birthDateRegex1) ||
2929
+ processedText.match(birthDateRegex2) ||
2930
+ processedText.match(birthDateRegex3);
2931
+ // 4. 从身份证号码中提取出生日期(如果上述方法失败)
2932
+ if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
2933
+ const year = info.idNumber.substring(6, 10);
2934
+ const month = info.idNumber.substring(10, 12);
2935
+ const day = info.idNumber.substring(12, 14);
2936
+ info.birthDate = `${year}-${month}-${day}`;
2937
+ }
2938
+ else if (birthDateMatch) {
2939
+ // 确保月份和日期是两位数
2940
+ const year = birthDateMatch[1];
2941
+ const month = birthDateMatch[2].padStart(2, "0");
2942
+ const day = birthDateMatch[3].padStart(2, "0");
2943
+ info.birthDate = `${year}-${month}-${day}`;
2944
+ }
2945
+ // 解析地址 - 改进的正则匹配
2946
+ // 1. 常规模式
2947
+ const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/;
2948
+ // 2. 更宽松的模式
2949
+ const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/;
2950
+ const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2);
2951
+ if (addressMatch && addressMatch[1]) {
2952
+ // 清理地址中的常见错误和多余空格
2953
+ info.address = addressMatch[1]
2954
+ .replace(/\s+/g, "")
2955
+ .replace(/\n/g, "")
2956
+ .trim();
2957
+ // 限制地址长度并判断地址合理性
2958
+ if (info.address.length > 70) {
2959
+ info.address = info.address.substring(0, 70);
2960
+ }
2961
+ // 确保地址是合理的(不仅仅包含符号或数字)
2962
+ if (!/[一-龥]/.test(info.address)) {
2963
+ info.address = ""; // 如果没有中文字符,可能不是有效地址
2964
+ }
2965
+ }
2966
+ // 解析签发机关
2967
+ const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/;
2968
+ const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/;
2969
+ const authorityMatch = processedText.match(authorityRegex1) ||
2970
+ processedText.match(authorityRegex2);
2971
+ if (authorityMatch && authorityMatch[1]) {
2972
+ info.issuingAuthority = authorityMatch[1]
2973
+ .replace(/\s+/g, "")
2974
+ .replace(/\n/g, "")
2975
+ .trim();
2976
+ }
2977
+ // 解析有效期限 - 支持多种格式
2978
+ // 1. 常规格式:YYYY.MM.DD-YYYY.MM.DD
2979
+ const validPeriodRegex1 = /有效期限[\s\:]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日\s]*)[-\s]*(至|-)[-\s]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日]*|[永久长期]*)/;
2980
+ // 2. 简化格式:YYYYMMDD-YYYYMMDD
2981
+ const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/;
2982
+ const validPeriodMatch = processedText.match(validPeriodRegex1) ||
2983
+ processedText.match(validPeriodRegex2);
2984
+ if (validPeriodMatch) {
2985
+ // 格式化为统一的有效期限形式
2986
+ if (validPeriodMatch[1] && validPeriodMatch[3]) {
2987
+ const startDate = this.formatDateString(validPeriodMatch[1]);
2988
+ const endDate = /\d/.test(validPeriodMatch[3])
2989
+ ? this.formatDateString(validPeriodMatch[3])
2990
+ : "长期有效";
2991
+ info.validPeriod = `${startDate}-${endDate}`;
2992
+ }
2993
+ else {
2994
+ info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim();
2995
+ }
2996
+ }
2997
+ return info;
2998
+ }
2999
+ /**
3000
+ * 清除结果缓存
3001
+ */
3002
+ clearCache() {
3003
+ this.resultCache.clear();
3004
+ this.options.logger?.("OCR结果缓存已清除");
3005
+ }
3006
+ /**
3007
+ * 终止OCR引擎并释放资源
3008
+ *
3009
+ * @returns {Promise<void>} 终止完成的Promise
3010
+ */
3011
+ async terminate() {
3012
+ if (this.worker) {
3013
+ await this.worker.terminate();
3014
+ this.worker = null;
3015
+ }
3016
+ if (this.ocrWorker) {
3017
+ this.ocrWorker.terminate();
3018
+ this.ocrWorker = null;
3019
+ }
3020
+ this.initialized = false;
3021
+ this.options.logger?.("OCR引擎已终止");
3022
+ }
3023
+ /**
3024
+ * 释放资源
3025
+ */
3026
+ dispose() {
3027
+ return this.terminate();
3028
+ }
3029
+ }
3030
+
3031
+ /**
3032
+ * @file 身份证防伪检测模块
3033
+ * @description 提供身份证防伪特征识别功能,区分真假身份证
3034
+ * @module AntiFakeDetector
3035
+ * @version 1.3.2
3036
+ */
3037
+ /**
3038
+ * 身份证防伪特征检测器
3039
+ *
3040
+ * 基于图像分析技术检测身份证中的多种防伪特征,包括:
3041
+ * 1. 荧光油墨特征
3042
+ * 2. 微缩文字
3043
+ * 3. 光变图案
3044
+ * 4. 雕刻凹印
3045
+ * 5. 隐形图案
3046
+ *
3047
+ * @example
3048
+ * ```typescript
3049
+ * // 创建防伪检测器
3050
+ * const antiFakeDetector = new AntiFakeDetector({
3051
+ * sensitivity: 0.8,
3052
+ * enableCache: true
3053
+ * });
3054
+ *
3055
+ * // 分析身份证图像
3056
+ * const imageData = await ImageProcessor.createImageDataFromFile(idCardFile);
3057
+ * const result = await antiFakeDetector.detect(imageData);
3058
+ *
3059
+ * if (result.isAuthentic) {
3060
+ * console.log('身份证真实,检测到防伪特征:', result.detectedFeatures);
3061
+ * } else {
3062
+ * console.log('警告!', result.message);
3063
+ * }
3064
+ * ```
3065
+ */
3066
+ class AntiFakeDetector {
3067
+ /**
3068
+ * 创建身份证防伪检测器实例
3069
+ *
3070
+ * @param options 防伪检测器配置
3071
+ */
3072
+ constructor(options = {}) {
3073
+ this.options = {
3074
+ sensitivity: 0.7,
3075
+ enableCache: true,
3076
+ cacheSize: 50,
3077
+ logger: console.log,
3078
+ ...options,
3079
+ };
3080
+ // 初始化缓存
3081
+ this.resultCache = new LRUCache(this.options.cacheSize);
3082
+ }
3083
+ /**
3084
+ * 检测身份证图像的防伪特征
3085
+ *
3086
+ * @param imageData 身份证图像数据
3087
+ * @returns 防伪检测结果
3088
+ */
3089
+ async detect(imageData) {
3090
+ const startTime = performance.now();
3091
+ // 检查缓存
3092
+ if (this.options.enableCache) {
3093
+ const fingerprint = calculateImageFingerprint(imageData);
3094
+ const cachedResult = this.resultCache.get(fingerprint);
3095
+ if (cachedResult) {
3096
+ this.options.logger("使用缓存的防伪检测结果");
3097
+ return cachedResult;
3098
+ }
3099
+ }
3100
+ // 图像预处理增强防伪特征
3101
+ const enhancedImage = this.enhanceAntiFakeFeatures(imageData);
3102
+ // 执行多种防伪特征检测
3103
+ const featureResults = await Promise.all([
3104
+ this.detectUVInkFeatures(enhancedImage),
3105
+ this.detectMicroText(enhancedImage),
3106
+ this.detectOpticalVariable(enhancedImage),
3107
+ this.detectIntaglioPrinting(enhancedImage),
3108
+ this.detectGhostImage(enhancedImage),
3109
+ ]);
3110
+ // 汇总检测结果
3111
+ const detectedFeatures = [];
3112
+ let totalConfidence = 0;
3113
+ for (const [feature, detected, confidence] of featureResults) {
3114
+ if (detected && confidence > 0.5) {
3115
+ detectedFeatures.push(feature);
3116
+ totalConfidence += confidence;
3117
+ }
3118
+ }
3119
+ // 计算最终结果
3120
+ const normalizedConfidence = featureResults.length > 0 ? totalConfidence / featureResults.length : 0;
3121
+ // 根据敏感度和检测到的特征决定是否通过验证
3122
+ const isAuthentic = normalizedConfidence >= this.options.sensitivity &&
3123
+ detectedFeatures.length >= 2;
3124
+ // 生成结果消息
3125
+ let message = isAuthentic
3126
+ ? `身份证真实,检测到${detectedFeatures.length}个防伪特征`
3127
+ : detectedFeatures.length > 0
3128
+ ? `可疑身份证,仅检测到${detectedFeatures.length}个防伪特征,置信度不足`
3129
+ : "未检测到有效防伪特征,可能为伪造证件";
3130
+ const result = {
3131
+ isAuthentic,
3132
+ confidence: normalizedConfidence,
3133
+ detectedFeatures,
3134
+ message,
3135
+ processingTime: performance.now() - startTime,
3136
+ };
3137
+ // 缓存结果
3138
+ if (this.options.enableCache) {
3139
+ const fingerprint = calculateImageFingerprint(imageData);
3140
+ this.resultCache.set(fingerprint, result);
3141
+ }
3142
+ return result;
3143
+ }
3144
+ /**
3145
+ * 增强身份证图像中的防伪特征
3146
+ *
3147
+ * @param imageData 原始图像数据
3148
+ * @returns 增强后的图像数据
3149
+ * @private
3150
+ */
3151
+ enhanceAntiFakeFeatures(imageData) {
3152
+ // 应用特定的图像处理增强防伪特征
3153
+ return ImageProcessor.batchProcess(imageData, {
3154
+ contrast: 30, // 增强对比度
3155
+ brightness: 10, // 轻微提高亮度
3156
+ sharpen: true, // 锐化图像突出细节
3157
+ });
3158
+ }
3159
+ /**
3160
+ * 检测荧光油墨特征
3161
+ *
3162
+ * @param imageData 图像数据
3163
+ * @returns [特征名称, 是否检测到, 置信度]
3164
+ * @private
3165
+ */
3166
+ async detectUVInkFeatures(imageData) {
3167
+ // 在真实身份证上,荧光油墨会在特定反光条件下呈现特定颜色特征
3168
+ // 在普通可见光下,我们分析蓝色和紫外色通道分布特征
3169
+ // 1. 提取蓝色通道并增强对比度
3170
+ const blueChannel = this.extractColorChannel(imageData, "blue");
3171
+ // 2. 分析蓝色通道的分布特征
3172
+ const { peaks, variance } = this.analyzeChannelDistribution(blueChannel);
3173
+ // 3. 分析特定区域的颜色模式
3174
+ const patternScore = this.detectUVColorPattern(imageData);
3175
+ // 4. 计算综合得分
3176
+ // 特征分析:荧光油墨在蓝色通道通常有显著峰值,且分布更聚集
3177
+ let score = 0;
3178
+ // 过多的峰值表明可能是真实身份证上的荧光特征
3179
+ if (peaks > 3 && peaks < 10) {
3180
+ score += 0.4;
3181
+ }
3182
+ // 方差越大,表示颜色对比度越高,更可能有荧光特征
3183
+ if (variance > 1000) {
3184
+ score += 0.3;
3185
+ }
3186
+ // 颜色模式得分
3187
+ score += patternScore * 0.3;
3188
+ // 重要区域分析
3189
+ // 身份证头像区域通常不应具有荧光特征
3190
+ const hasPortraitAreaFeatures = this.analyzePortraitArea(imageData);
3191
+ if (hasPortraitAreaFeatures) {
3192
+ // 头像区域不应该有荧光特征,如果有可能是伪造的
3193
+ score -= 0.2;
3194
+ }
3195
+ // 求出最终分数并限制在[0,1]范围内
3196
+ const confidence = Math.max(0, Math.min(1, score));
3197
+ const detected = confidence > 0.55;
3198
+ return ["荧光油墨", detected, confidence];
3199
+ }
3200
+ /**
3201
+ * 从图像数据中提取指定颜色通道
3202
+ * @param imageData 原始图像数据
3203
+ * @param channel 通道名称(red, green, blue)
3204
+ */
3205
+ extractColorChannel(imageData, channel) {
3206
+ const { data, width, height } = imageData;
3207
+ const channelOffset = channel === "red" ? 0 : channel === "green" ? 1 : 2;
3208
+ const channelData = new Uint8ClampedArray(width * height);
3209
+ for (let i = 0; i < data.length; i += 4) {
3210
+ const pixelIndex = i / 4;
3211
+ channelData[pixelIndex] = data[i + channelOffset];
3212
+ }
3213
+ return channelData;
3214
+ }
3215
+ /**
3216
+ * 分析颜色通道分布特征
3217
+ * @param channelData 颜色通道数据
3218
+ */
3219
+ analyzeChannelDistribution(channelData) {
3220
+ // 计算直方图
3221
+ const histogram = new Array(256).fill(0);
3222
+ for (let i = 0; i < channelData.length; i++) {
3223
+ histogram[channelData[i]]++;
3224
+ }
3225
+ // 平滑直方图以减少噪声
3226
+ const smoothedHistogram = this.smoothHistogram(histogram, 3);
3227
+ // 计算峰值数量
3228
+ let peaks = 0;
3229
+ for (let i = 1; i < 255; i++) {
3230
+ if (smoothedHistogram[i] > smoothedHistogram[i - 1] &&
3231
+ smoothedHistogram[i] > smoothedHistogram[i + 1] &&
3232
+ smoothedHistogram[i] > channelData.length * 0.01) {
3233
+ // 只计算显著峰值
3234
+ peaks++;
3235
+ }
3236
+ }
3237
+ // 计算方差
3238
+ let mean = 0;
3239
+ for (let i = 0; i < channelData.length; i++) {
3240
+ mean += channelData[i];
3241
+ }
3242
+ mean /= channelData.length;
3243
+ let variance = 0;
3244
+ for (let i = 0; i < channelData.length; i++) {
3245
+ variance += Math.pow(channelData[i] - mean, 2);
3246
+ }
3247
+ variance /= channelData.length;
3248
+ return { peaks, variance };
3249
+ }
3250
+ /**
3251
+ * 平滑直方图以减少噪声
3252
+ */
3253
+ smoothHistogram(histogram, windowSize) {
3254
+ const result = new Array(histogram.length).fill(0);
3255
+ const halfWindow = Math.floor(windowSize / 2);
3256
+ for (let i = 0; i < histogram.length; i++) {
3257
+ let sum = 0;
3258
+ let count = 0;
3259
+ for (let j = Math.max(0, i - halfWindow); j <= Math.min(histogram.length - 1, i + halfWindow); j++) {
3260
+ sum += histogram[j];
3261
+ count++;
3262
+ }
3263
+ result[i] = sum / count;
3264
+ }
3265
+ return result;
3266
+ }
3267
+ /**
3268
+ * 检测图像中的荧光颜色模式
3269
+ */
3270
+ detectUVColorPattern(imageData) {
3271
+ // 分析特定组合颜色的出现频率,荧光油墨在可见光下也有特定的颜色特征
3272
+ const { data, width, height } = imageData;
3273
+ let uvColorCount = 0;
3274
+ // 寻找可能为荧光油墨的特定颜色模式
3275
+ // 这些颜色通常是特定的蓝紫色调和高对比度
3276
+ for (let i = 0; i < data.length; i += 4) {
3277
+ const r = data[i];
3278
+ const g = data[i + 1];
3279
+ const b = data[i + 2];
3280
+ // 检查是否是荧光油墨特有的颜色范围
3281
+ // 这里使用简化的追踪条件,实际应用中应使用更复杂的颜色模型
3282
+ if (b > 1.5 * r && b > 1.3 * g && b > 100) {
3283
+ uvColorCount++;
3284
+ }
3285
+ }
3286
+ // 计算荧光颜色像素占比
3287
+ const totalPixels = width * height;
3288
+ const uvColorRatio = uvColorCount / totalPixels;
3289
+ // 对于真实身份证,荧光颜色的占比应该在一定范围内
3290
+ // 如果占比过高或过低,可能是伪造的
3291
+ const idealRatio = 0.05; // 理想占比
3292
+ const deviation = Math.abs(uvColorRatio - idealRatio) / idealRatio;
3293
+ // 将差异转换为0-1的置信度分数
3294
+ return Math.max(0, 1 - Math.min(1, deviation * 2));
3295
+ }
3296
+ /**
3297
+ * 分析头像区域是否存在荧光特征
3298
+ * 这个方法用于检测伪造的身份证,因为头像区域不应该有荧光特征
3299
+ */
3300
+ analyzePortraitArea(imageData) {
3301
+ // 假设头像区域大约占据图片右上方四分之一的区域
3302
+ const { width, height, data } = imageData;
3303
+ const portraitX = Math.floor(width * 0.6);
3304
+ const portraitY = Math.floor(height * 0.2);
3305
+ const portraitWidth = Math.floor(width * 0.3);
3306
+ const portraitHeight = Math.floor(height * 0.3);
3307
+ let uvFeatureCount = 0;
3308
+ let totalPixels = 0;
3309
+ // 检查头像区域的荧光特征
3310
+ for (let y = portraitY; y < portraitY + portraitHeight; y++) {
3311
+ for (let x = portraitX; x < portraitX + portraitWidth; x++) {
3312
+ if (x >= 0 && x < width && y >= 0 && y < height) {
3313
+ const i = (y * width + x) * 4;
3314
+ const r = data[i];
3315
+ const g = data[i + 1];
3316
+ const b = data[i + 2];
3317
+ // 使用与上面相同的荧光颜色检测标准
3318
+ if (b > 1.5 * r && b > 1.3 * g && b > 100) {
3319
+ uvFeatureCount++;
3320
+ }
3321
+ totalPixels++;
3322
+ }
3323
+ }
3324
+ }
3325
+ // 如果头像区域的荧光特征占比过高,可能是伪造的
3326
+ return totalPixels > 0 && uvFeatureCount / totalPixels > 0.1;
3327
+ }
3328
+ /**
3329
+ * 检测微缩文字
3330
+ *
3331
+ * @param imageData 图像数据
3332
+ * @returns [特征名称, 是否检测到, 置信度]
3333
+ * @private
3334
+ */
3335
+ async detectMicroText(imageData) {
3336
+ // 微缩文字检测 - 身份证上的微缩文字是重要的防伪特征
3337
+ // 这些文字很小,但会呈现规则的线条和高频组件
3338
+ // 1. 转换图像为灰度图
3339
+ const grayscale = ImageProcessor.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
3340
+ // 2. 执行边缘检测突出微缩文字
3341
+ const edgeData = ImageProcessor.detectEdges(grayscale, 40); // 强化的边缘检测
3342
+ // 3. 分析频率特征 - 微缩文字呈现高频的边缘过渡
3343
+ const frequencyFeatures = this.analyzeFrequencyFeatures(edgeData);
3344
+ // 4. 检测微缩文字的具体区域
3345
+ const microTextRegions = this.detectMicroTextRegions(edgeData);
3346
+ // 5. 综合分析结果计算置信度
3347
+ let score = 0;
3348
+ // 频率特征分数
3349
+ score += frequencyFeatures.score * 0.6;
3350
+ // 区域特征分数
3351
+ if (microTextRegions.count > 0) {
3352
+ // 过多的区域也可能表示噪声,因此有一个最佳范围
3353
+ const normalizedCount = Math.min(microTextRegions.count, 5) / 5;
3354
+ score += normalizedCount * 0.4;
3355
+ }
3356
+ // 对置信度进行最终调整
3357
+ const confidence = Math.max(0, Math.min(1, score));
3358
+ const detected = confidence > 0.5;
3359
+ return ["微缩文字", detected, confidence];
3360
+ }
3361
+ /**
3362
+ * 分析边缘图像的频率特征
3363
+ * 微缩文字呈现高频的边缘过渡
3364
+ */
3365
+ analyzeFrequencyFeatures(edgeData) {
3366
+ const { data, width, height } = edgeData;
3367
+ // 计算边缘像素的数量
3368
+ for (let i = 0; i < data.length; i += 4) {
3369
+ if (data[i] > 200) ;
3370
+ }
3371
+ // 计算高频边缘分布
3372
+ // 统计边缘过渡的变化频率
3373
+ let highFreqTransitions = 0;
3374
+ // 检测行方向的边缘变化
3375
+ for (let y = 0; y < height; y++) {
3376
+ let prevEdge = false;
3377
+ let transitions = 0;
3378
+ for (let x = 0; x < width; x++) {
3379
+ const i = (y * width + x) * 4;
3380
+ const isEdge = data[i] > 200;
3381
+ if (isEdge !== prevEdge) {
3382
+ transitions++;
3383
+ prevEdge = isEdge;
3384
+ }
3385
+ }
3386
+ // 每行的过渡频率
3387
+ if (transitions > width * 0.1) {
3388
+ // 高频过渡行
3389
+ highFreqTransitions++;
3390
+ }
3391
+ }
3392
+ // 计算列方向的边缘变化
3393
+ let colHighFreqTransitions = 0;
3394
+ for (let x = 0; x < width; x++) {
3395
+ let prevEdge = false;
3396
+ let transitions = 0;
3397
+ for (let y = 0; y < height; y++) {
3398
+ const i = (y * width + x) * 4;
3399
+ const isEdge = data[i] > 200;
3400
+ if (isEdge !== prevEdge) {
3401
+ transitions++;
3402
+ prevEdge = isEdge;
3403
+ }
3404
+ }
3405
+ // 每列的过渡频率
3406
+ if (transitions > height * 0.1) {
3407
+ // 高频过渡列
3408
+ colHighFreqTransitions++;
3409
+ }
3410
+ }
3411
+ // 综合计算高频特征比例
3412
+ const rowHighFreqRatio = highFreqTransitions / height;
3413
+ const colHighFreqRatio = colHighFreqTransitions / width;
3414
+ const highFreqRatio = (rowHighFreqRatio + colHighFreqRatio) / 2;
3415
+ // 计算最终分数
3416
+ // 真实的微缩文字应该有适度的高频特征,而不是极端的高或低
3417
+ const idealRatio = 0.15; // 理想的高频比例
3418
+ const deviationFactor = Math.abs(highFreqRatio - idealRatio) / idealRatio;
3419
+ const score = Math.max(0, 1 - Math.min(1, deviationFactor * 3));
3420
+ return { score, highFreqRatio };
3421
+ }
3422
+ /**
3423
+ * 检测微缩文字区域
3424
+ * 微缩文字通常呈现呈现规则的组合排列
3425
+ */
3426
+ detectMicroTextRegions(edgeData) {
3427
+ const { data, width, height } = edgeData;
3428
+ const visitedMap = new Array(width * height).fill(false);
3429
+ const regions = [];
3430
+ // 使用满足条件的连通区域寻找微缩文字区域
3431
+ for (let y = 0; y < height; y++) {
3432
+ for (let x = 0; x < width; x++) {
3433
+ const idx = y * width + x;
3434
+ const i = idx * 4;
3435
+ // 如果是边缘像素且未访问过
3436
+ if (data[i] > 200 && !visitedMap[idx]) {
3437
+ // 使用深度优先搜索找到连通的边缘区域
3438
+ const regionPoints = this.floodFillEdge(edgeData, x, y, visitedMap);
3439
+ // 分析区域
3440
+ if (regionPoints.length > 10) {
3441
+ // 小区域忽略
3442
+ const [minX, minY, maxX, maxY] = this.getBoundingBox(regionPoints);
3443
+ const regionWidth = maxX - minX + 1;
3444
+ const regionHeight = maxY - minY + 1;
3445
+ // 检查区域大小和纹理特征
3446
+ if (regionWidth > 5 &&
3447
+ regionHeight > 5 &&
3448
+ regionWidth < width * 0.2 &&
3449
+ regionHeight < height * 0.2) {
3450
+ // 计算区域密度
3451
+ const density = regionPoints.length / (regionWidth * regionHeight);
3452
+ // 检查并添加符合微缩文字特征的区域
3453
+ if (density > 0.1 && density < 0.5) {
3454
+ // 合适的密度范围
3455
+ regions.push({
3456
+ x: minX,
3457
+ y: minY,
3458
+ w: regionWidth,
3459
+ h: regionHeight,
3460
+ });
3461
+ }
3462
+ }
3463
+ }
3464
+ }
3465
+ }
3466
+ }
3467
+ return { count: regions.length, regions };
3468
+ }
3469
+ /**
3470
+ * 深度优先搜索连通的边缘区域
3471
+ */
3472
+ floodFillEdge(edgeData, startX, startY, visitedMap) {
3473
+ const { data, width, height } = edgeData;
3474
+ const stack = [];
3475
+ const points = [];
3476
+ const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
3477
+ const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
3478
+ // 起始点
3479
+ stack.push({ x: startX, y: startY });
3480
+ visitedMap[startY * width + startX] = true;
3481
+ while (stack.length > 0) {
3482
+ const { x, y } = stack.pop();
3483
+ points.push({ x, y });
3484
+ // 检查88个相邻方向
3485
+ for (let i = 0; i < 8; i++) {
3486
+ const nx = x + dx[i];
3487
+ const ny = y + dy[i];
3488
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
3489
+ const nidx = ny * width + nx;
3490
+ const ni = nidx * 4;
3491
+ if (data[ni] > 200 && !visitedMap[nidx]) {
3492
+ stack.push({ x: nx, y: ny });
3493
+ visitedMap[nidx] = true;
3494
+ }
3495
+ }
3496
+ }
3497
+ }
3498
+ return points;
3499
+ }
3500
+ /**
3501
+ * 获取点集的外接矩形
3502
+ */
3503
+ getBoundingBox(points) {
3504
+ let minX = Number.MAX_SAFE_INTEGER;
3505
+ let minY = Number.MAX_SAFE_INTEGER;
3506
+ let maxX = 0;
3507
+ let maxY = 0;
3508
+ for (const { x, y } of points) {
3509
+ minX = Math.min(minX, x);
3510
+ minY = Math.min(minY, y);
3511
+ maxX = Math.max(maxX, x);
3512
+ maxY = Math.max(maxY, y);
3513
+ }
3514
+ return [minX, minY, maxX, maxY];
3515
+ }
3516
+ /**
3517
+ * 检测光变图案
3518
+ *
3519
+ * @param imageData 图像数据
3520
+ * @returns [特征名称, 是否检测到, 置信度]
3521
+ * @private
3522
+ */
3523
+ async detectOpticalVariable(imageData) {
3524
+ // 提取特定区域并分析颜色变化
3525
+ // 在实际实现中需要定位光变图案区域并分析其特征
3526
+ // 这里使用模拟实现
3527
+ // 模拟检测: 65%的概率检测到,置信度0.6-0.9
3528
+ const detected = Math.random() > 0.35;
3529
+ const confidence = detected ? 0.6 + Math.random() * 0.3 : 0;
3530
+ return ["光变图案", detected, confidence];
3531
+ }
3532
+ /**
3533
+ * 检测凹印雕刻特征
3534
+ *
3535
+ * @param imageData 图像数据
3536
+ * @returns [特征名称, 是否检测到, 置信度]
3537
+ * @private
3538
+ */
3539
+ async detectIntaglioPrinting(imageData) {
3540
+ // 使用特定滤镜增强凹印效果
3541
+ // 在实际实现中应分析阴影和纹理模式
3542
+ // 这里使用模拟实现
3543
+ // 模拟检测: 75%的概率检测到,置信度0.65-0.9
3544
+ const detected = Math.random() > 0.25;
3545
+ const confidence = detected ? 0.65 + Math.random() * 0.25 : 0;
3546
+ return ["雕刻凹印", detected, confidence];
3547
+ }
3548
+ /**
3549
+ * 检测隐形图案(幽灵图像)
3550
+ *
3551
+ * @param imageData 图像数据
3552
+ * @returns [特征名称, 是否检测到, 置信度]
3553
+ * @private
3554
+ */
3555
+ async detectGhostImage(imageData) {
3556
+ // 调整对比度和亮度显现隐形图案
3557
+ // 在实际实现中应使用特定滤镜和图像处理算法
3558
+ // 这里使用模拟实现
3559
+ // 模拟检测: 60%的概率检测到,置信度0.55-0.85
3560
+ const detected = Math.random() > 0.4;
3561
+ const confidence = detected ? 0.55 + Math.random() * 0.3 : 0;
3562
+ return ["隐形图案", detected, confidence];
3563
+ }
3564
+ /**
3565
+ * 清除结果缓存
3566
+ */
3567
+ clearCache() {
3568
+ this.resultCache.clear();
3569
+ this.options.logger("防伪检测结果缓存已清除");
3570
+ }
3571
+ /**
3572
+ * 释放资源
3573
+ */
3574
+ dispose() {
3575
+ this.resultCache.clear();
3576
+ }
3577
+ }
3578
+
3579
+ /**
3580
+ * @file 身份证模块入口
3581
+ * @description 提供身份证识别和验证功能的模块入口
3582
+ * @module modules/id-card
3583
+ */
3584
+ /**
3585
+ * 身份证识别模块
3586
+ * 提供身份证检测、OCR识别、防伪检测等功能
3587
+ */
3588
+ class IDCardModule extends BaseModule {
3589
+ /**
3590
+ * 构造函数
3591
+ * @param options 模块配置选项
3592
+ */
3593
+ constructor(options = {}) {
3594
+ super();
3595
+ /** 模块名称 */
3596
+ this.name = 'id-card';
3597
+ this.options = {
3598
+ enabled: true,
3599
+ detector: {
3600
+ minConfidence: 0.7,
3601
+ enableOCR: true,
3602
+ enableAntiFake: false,
3603
+ ...options.detector
3604
+ },
3605
+ ocr: {
3606
+ useWorker: true,
3607
+ maxImageDimension: 1000,
3608
+ brightness: 10,
3609
+ contrast: 20,
3610
+ ...options.ocr
3611
+ },
3612
+ antiFake: {
3613
+ sensitivity: 0.8,
3614
+ minConfidence: 0.7,
3615
+ ...options.antiFake
3616
+ },
3617
+ ...options
3618
+ };
3619
+ // 创建检测器
3620
+ this.detector = new IDCardDetector({
3621
+ minConfidence: this.options.detector?.minConfidence,
3622
+ enableEdgeDetection: true,
3623
+ returnImage: true
3624
+ });
3625
+ }
3626
+ /**
3627
+ * 初始化模块
3628
+ */
3629
+ async initialize() {
3630
+ if (this._isInitialized) {
3631
+ return;
3632
+ }
3633
+ this.logger.debug(this.name, '初始化身份证模块');
3634
+ try {
3635
+ // 初始化检测器
3636
+ await this.detector.initialize();
3637
+ // 如果启用OCR,初始化OCR处理器
3638
+ if (this.options.detector?.enableOCR) {
3639
+ this.ocrProcessor = new OCRProcessor({
3640
+ useWorker: this.options.ocr?.useWorker,
3641
+ maxImageDimension: this.options.ocr?.maxImageDimension,
3642
+ brightness: this.options.ocr?.brightness,
3643
+ contrast: this.options.ocr?.contrast
3644
+ });
3645
+ await this.ocrProcessor.initialize();
3646
+ }
3647
+ // 如果启用防伪检测,初始化防伪检测器
3648
+ if (this.options.detector?.enableAntiFake) {
3649
+ this.antiFakeDetector = new AntiFakeDetector({
3650
+ sensitivity: this.options.antiFake?.sensitivity
3651
+ });
3652
+ // AntiFakeDetector 不需要初始化
3653
+ }
3654
+ this._isInitialized = true;
3655
+ this.emit('initialized');
3656
+ this.logger.debug(this.name, '身份证模块初始化完成');
3657
+ }
3658
+ catch (error) {
3659
+ this.logger.error(this.name, '身份证模块初始化失败', error instanceof Error ? error : new Error(String(error)));
3660
+ throw new Error(`身份证模块初始化失败: ${error instanceof Error ? error.message : String(error)}`);
3661
+ }
3662
+ }
3663
+ /**
3664
+ * 识别身份证图像
3665
+ * @param image 图像源
3666
+ * @returns 识别结果
3667
+ */
3668
+ async recognize(image) {
3669
+ this.ensureInitialized();
3670
+ try {
3671
+ // 检测身份证
3672
+ const detectionResult = await this.detector.processImage(image);
3673
+ if (!detectionResult.isSuccess() || !detectionResult.data) {
3674
+ throw new Error('未检测到身份证');
3675
+ }
3676
+ // 创建结果对象
3677
+ const idCardInfo = {
3678
+ type: detectionResult.data.type || IDCardType.FRONT,
3679
+ confidence: detectionResult.data.confidence
3680
+ };
3681
+ // 如果启用OCR且OCR处理器已初始化
3682
+ if (this.options.detector?.enableOCR && this.ocrProcessor) {
3683
+ // 裁剪并处理图像
3684
+ const processedImage = detectionResult.data.image || this.convertToImageData(image);
3685
+ // 识别文本信息
3686
+ const ocrResult = await this.ocrProcessor.processIDCard(processedImage);
3687
+ // 合并OCR结果
3688
+ Object.assign(idCardInfo, ocrResult);
3689
+ }
3690
+ // 如果启用防伪检测且防伪检测器已初始化
3691
+ if (this.options.detector?.enableAntiFake && this.antiFakeDetector) {
3692
+ const processedImage = this.convertToImageData(image);
3693
+ const antiFakeResult = await this.antiFakeDetector.detect(processedImage);
3694
+ // 转换防伪检测结果格式
3695
+ idCardInfo.antiFake = {
3696
+ passed: antiFakeResult.isAuthentic,
3697
+ score: antiFakeResult.confidence,
3698
+ features: {
3699
+ // 转换检测到的特征
3700
+ fluorescent: antiFakeResult.detectedFeatures.includes('荧光油墨特征'),
3701
+ microtext: antiFakeResult.detectedFeatures.includes('微缩文字'),
3702
+ opticalVariable: antiFakeResult.detectedFeatures.includes('光变图案'),
3703
+ texture: antiFakeResult.detectedFeatures.includes('雕刻凹印'),
3704
+ watermark: antiFakeResult.detectedFeatures.includes('隐形图案')
3705
+ }
3706
+ };
3707
+ }
3708
+ // 保存最后一次检测结果
3709
+ this.lastDetectionResult = idCardInfo;
3710
+ // 触发事件
3711
+ this.emit('recognized', { idCardInfo });
3712
+ return idCardInfo;
3713
+ }
3714
+ catch (error) {
3715
+ this.logger.error(this.name, '身份证识别失败', error instanceof Error ? error : new Error(String(error)));
3716
+ throw new Error(`身份证识别失败: ${error instanceof Error ? error.message : String(error)}`);
3717
+ }
3718
+ }
3719
+ /**
3720
+ * 验证身份证信息
3721
+ * @param idCardInfo 身份证信息
3722
+ * @returns 验证结果
3723
+ */
3724
+ verify(idCardInfo) {
3725
+ this.ensureInitialized();
3726
+ const result = {
3727
+ isValid: true,
3728
+ score: 1.0,
3729
+ details: {
3730
+ idNumberValid: true,
3731
+ issueDateValid: true,
3732
+ isExpired: false,
3733
+ antiFakePassed: true
3734
+ }
3735
+ };
3736
+ // 验证身份证号码
3737
+ if (idCardInfo.idNumber) {
3738
+ result.details.idNumberValid = this.validateIDNumber(idCardInfo.idNumber);
3739
+ if (!result.details.idNumberValid) {
3740
+ result.isValid = false;
3741
+ result.score -= 0.3;
3742
+ result.failureReason = '身份证号码无效';
3743
+ }
3744
+ }
3745
+ // 验证有效期
3746
+ if (idCardInfo.validTo) {
3747
+ result.details.isExpired = this.isIDCardExpired(idCardInfo.validTo);
3748
+ if (result.details.isExpired) {
3749
+ result.isValid = false;
3750
+ result.score -= 0.2;
3751
+ result.failureReason = '身份证已过期';
3752
+ }
3753
+ }
3754
+ // 验证防伪结果
3755
+ if (idCardInfo.antiFake) {
3756
+ result.details.antiFakePassed = idCardInfo.antiFake.passed;
3757
+ if (!result.details.antiFakePassed) {
3758
+ result.isValid = false;
3759
+ result.score -= 0.5;
3760
+ result.failureReason = '防伪检测未通过';
3761
+ }
3762
+ }
3763
+ return result;
3764
+ }
3765
+ /**
3766
+ * 获取最后一次识别结果
3767
+ */
3768
+ getLastRecognitionResult() {
3769
+ return this.lastDetectionResult;
3770
+ }
3771
+ /**
3772
+ * 释放模块资源
3773
+ */
3774
+ async dispose() {
3775
+ if (!this._isInitialized) {
3776
+ return;
3777
+ }
3778
+ this.logger.debug(this.name, '释放身份证模块资源');
3779
+ try {
3780
+ // 释放检测器资源
3781
+ await this.detector.dispose();
3782
+ // 释放OCR处理器资源
3783
+ if (this.ocrProcessor) {
3784
+ await this.ocrProcessor.terminate();
3785
+ }
3786
+ // 释放防伪检测器资源
3787
+ if (this.antiFakeDetector) {
3788
+ await this.antiFakeDetector.dispose();
3789
+ }
3790
+ // 调用基类的dispose方法
3791
+ await super.dispose();
3792
+ }
3793
+ catch (error) {
3794
+ this.logger.error(this.name, '身份证模块资源释放失败', error instanceof Error ? error : new Error(String(error)));
3795
+ throw new Error(`身份证模块资源释放失败: ${error instanceof Error ? error.message : String(error)}`);
3796
+ }
3797
+ }
3798
+ /**
3799
+ * 验证身份证号码是否有效
3800
+ * @param idNumber 身份证号码
3801
+ * @returns 是否有效
3802
+ */
3803
+ validateIDNumber(idNumber) {
3804
+ // 基本格式验证
3805
+ if (!idNumber || !/^\d{17}[\dX]$/.test(idNumber)) {
3806
+ return false;
3807
+ }
3808
+ // 验证出生日期
3809
+ const year = parseInt(idNumber.substring(6, 10));
3810
+ const month = parseInt(idNumber.substring(10, 12));
3811
+ const day = parseInt(idNumber.substring(12, 14));
3812
+ if (year < 1900 || year > new Date().getFullYear() ||
3813
+ month < 1 || month > 12 ||
3814
+ day < 1 || day > 31) {
3815
+ return false;
3816
+ }
3817
+ // 验证校验位
3818
+ const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
3819
+ const validationCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
3820
+ let sum = 0;
3821
+ for (let i = 0; i < 17; i++) {
3822
+ sum += parseInt(idNumber[i]) * weights[i];
3823
+ }
3824
+ const validationCode = validationCodes[sum % 11];
3825
+ return validationCode === idNumber[17].toUpperCase();
3826
+ }
3827
+ /**
3828
+ * 检查身份证是否过期
3829
+ * @param validTo 有效期截止日期
3830
+ * @returns 是否过期
3831
+ */
3832
+ isIDCardExpired(validTo) {
3833
+ // 如果是"长期",则视为未过期
3834
+ if (validTo === '长期' || validTo === '长期有效') {
3835
+ return false;
3836
+ }
3837
+ try {
3838
+ // 解析日期字符串
3839
+ const parts = validTo.split('-');
3840
+ if (parts.length !== 3) {
3841
+ return true; // 格式错误,视为过期
3842
+ }
3843
+ const year = parseInt(parts[0]);
3844
+ const month = parseInt(parts[1]) - 1; // 月份从0开始
3845
+ const day = parseInt(parts[2]);
3846
+ const expiryDate = new Date(year, month, day);
3847
+ const today = new Date();
3848
+ // 设置时间为当天结束
3849
+ today.setHours(0, 0, 0, 0);
3850
+ return expiryDate < today;
3851
+ }
3852
+ catch {
3853
+ return true; // 解析错误,视为过期
3854
+ }
3855
+ }
3856
+ /**
3857
+ * 检测身份证
3858
+ * @param image 图像源
3859
+ * @returns 检测结果
3860
+ */
3861
+ async detect(image) {
3862
+ this.ensureInitialized();
3863
+ try {
3864
+ // 调用检测器处理图像
3865
+ const result = await this.detector.processImage(image);
3866
+ if (!result.isSuccess() || !result.data) {
3867
+ return { success: false, confidence: 0 };
3868
+ }
3869
+ return {
3870
+ success: true,
3871
+ type: result.data.type,
3872
+ confidence: result.data.confidence || 0,
3873
+ croppedImage: result.data.image
3874
+ };
3875
+ }
3876
+ catch (error) {
3877
+ this.logger.error(this.name, '身份证检测失败', error instanceof Error ? error : new Error(String(error)));
3878
+ return { success: false, confidence: 0 };
3879
+ }
3880
+ }
3881
+ /**
3882
+ * 将图像转换为 ImageData
3883
+ * @param image 图像源
3884
+ * @returns ImageData 对象
3885
+ */
3886
+ convertToImageData(image) {
3887
+ // 如果已经是 ImageData,直接返回
3888
+ if (image instanceof ImageData) {
3889
+ return image;
3890
+ }
3891
+ // 创建 Canvas 用于转换
3892
+ const canvas = document.createElement('canvas');
3893
+ const width = image instanceof HTMLImageElement ? image.naturalWidth : image.width;
3894
+ const height = image instanceof HTMLImageElement ? image.naturalHeight : image.height;
3895
+ canvas.width = width;
3896
+ canvas.height = height;
3897
+ const ctx = canvas.getContext('2d');
3898
+ if (!ctx) {
3899
+ throw new Error('无法创建 Canvas 上下文');
3900
+ }
3901
+ // 绘制图像到 Canvas
3902
+ ctx.drawImage(image, 0, 0);
3903
+ // 返回 ImageData
3904
+ return ctx.getImageData(0, 0, width, height);
3905
+ }
3906
+ /**
3907
+ * 确保模块已初始化
3908
+ * @protected
3909
+ */
3910
+ ensureInitialized() {
3911
+ if (!this._isInitialized) {
3912
+ throw new Error('身份证模块尚未初始化');
3913
+ }
3914
+ }
3915
+ }
3916
+
3917
+ /**
3918
+ * @file 二维码扫描器
3919
+ * @description 提供二维码检测和解析功能
3920
+ * @module modules/qrcode/qr-code-scanner
3921
+ */
3922
+ /**
3923
+ * 二维码扫描器类
3924
+ */
3925
+ class QRCodeScanner extends EventEmitter {
3926
+ /**
3927
+ * 构造函数
3928
+ * @param options 配置选项
3929
+ */
3930
+ constructor(options = {}) {
3931
+ super();
3932
+ this.initialized = false;
3933
+ this.options = {
3934
+ minConfidence: 0.6,
3935
+ returnImage: false,
3936
+ imageProcess: {
3937
+ preprocess: true,
3938
+ enhanceContrast: true,
3939
+ threshold: 128,
3940
+ ...options.imageProcess
3941
+ },
3942
+ ...options
3943
+ };
3944
+ this.logger = Logger.getInstance();
3945
+ }
3946
+ /**
3947
+ * 初始化扫描器
3948
+ */
3949
+ async initialize() {
3950
+ if (this.initialized) {
3951
+ return;
3952
+ }
3953
+ this.logger.debug('QRCodeScanner', '初始化二维码扫描器');
3954
+ // 验证jsQR是否可用
3955
+ if (typeof jsQR !== 'function') {
3956
+ throw new Error('jsQR库未加载,请确保已安装jsqr依赖');
3957
+ }
3958
+ this.initialized = true;
3959
+ this.logger.debug('QRCodeScanner', '二维码扫描器初始化完成');
3960
+ }
3961
+ /**
3962
+ * 扫描图像中的二维码
3963
+ * @param image 图像源
3964
+ * @returns 二维码扫描结果
3965
+ */
3966
+ async scan(image) {
3967
+ if (!this.initialized) {
3968
+ await this.initialize();
3969
+ }
3970
+ // 将输入转换为ImageData
3971
+ const imageData = this.getImageData(image);
3972
+ // 图像预处理
3973
+ const processedImage = this.options.imageProcess?.preprocess
3974
+ ? this.preprocessImage(imageData)
3975
+ : imageData;
3976
+ // 使用jsQR进行扫描
3977
+ const code = jsQR(processedImage.data, processedImage.width, processedImage.height, {
3978
+ inversionAttempts: 'dontInvert'
3979
+ });
3980
+ if (!code) {
3981
+ return undefined;
3982
+ }
3983
+ // 构建结果
3984
+ const result = {
3985
+ data: code.data,
3986
+ boundingBox: {
3987
+ topLeft: code.location.topLeftCorner,
3988
+ topRight: code.location.topRightCorner,
3989
+ bottomRight: code.location.bottomRightCorner,
3990
+ bottomLeft: code.location.bottomLeftCorner
3991
+ },
3992
+ center: {
3993
+ x: Math.round((code.location.topLeftCorner.x + code.location.bottomRightCorner.x) / 2),
3994
+ y: Math.round((code.location.topLeftCorner.y + code.location.bottomRightCorner.y) / 2)
3995
+ },
3996
+ confidence: 1.0 // jsQR不提供置信度,默认为1.0
3997
+ };
3998
+ // 如果需要返回原始图像
3999
+ if (this.options.returnImage) {
4000
+ result.image = imageData;
4001
+ }
4002
+ this.logger.debug('QRCodeScanner', `扫描到二维码: ${result.data.substring(0, 20)}${result.data.length > 20 ? '...' : ''}`);
4003
+ return result;
4004
+ }
4005
+ /**
4006
+ * 将各种图像源转换为ImageData
4007
+ * @param image 图像源
4008
+ * @returns ImageData
4009
+ */
4010
+ getImageData(image) {
4011
+ // 如果已经是ImageData,直接返回
4012
+ if (image instanceof ImageData) {
4013
+ return image;
4014
+ }
4015
+ // 创建canvas并获取2D上下文
4016
+ const canvas = document.createElement('canvas');
4017
+ const ctx = canvas.getContext('2d');
4018
+ if (!ctx) {
4019
+ throw new Error('无法创建Canvas上下文');
4020
+ }
4021
+ // 设置canvas尺寸
4022
+ canvas.width = image.width;
4023
+ canvas.height = image.height;
4024
+ // 绘制图像
4025
+ ctx.drawImage(image, 0, 0);
4026
+ // 获取ImageData
4027
+ return ctx.getImageData(0, 0, canvas.width, canvas.height);
4028
+ }
4029
+ /**
4030
+ * 图像预处理
4031
+ * @param imageData 原始图像数据
4032
+ * @returns 处理后的图像数据
4033
+ */
4034
+ preprocessImage(imageData) {
4035
+ // 创建canvas并获取2D上下文
4036
+ const canvas = document.createElement('canvas');
4037
+ const ctx = canvas.getContext('2d');
4038
+ if (!ctx) {
4039
+ return imageData;
4040
+ }
4041
+ // 设置canvas尺寸
4042
+ canvas.width = imageData.width;
4043
+ canvas.height = imageData.height;
4044
+ // 绘制原始图像
4045
+ ctx.putImageData(imageData, 0, 0);
4046
+ // 增强对比度
4047
+ if (this.options.imageProcess?.enhanceContrast) {
4048
+ ctx.filter = 'contrast(150%)';
4049
+ ctx.drawImage(canvas, 0, 0);
4050
+ ctx.filter = 'none';
4051
+ }
4052
+ // 应用二值化
4053
+ const threshold = this.options.imageProcess?.threshold || 128;
4054
+ const processedData = ctx.getImageData(0, 0, canvas.width, canvas.height);
4055
+ const data = processedData.data;
4056
+ for (let i = 0; i < data.length; i += 4) {
4057
+ // 计算灰度值
4058
+ const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
4059
+ // 二值化
4060
+ const value = gray < threshold ? 0 : 255;
4061
+ data[i] = data[i + 1] = data[i + 2] = value;
4062
+ }
4063
+ return processedData;
4064
+ }
4065
+ /**
4066
+ * 释放资源
4067
+ */
4068
+ async dispose() {
4069
+ if (!this.initialized) {
4070
+ return;
4071
+ }
4072
+ this.logger.debug('QRCodeScanner', '释放二维码扫描器资源');
4073
+ // 移除所有事件监听器
4074
+ this.removeAllListeners();
4075
+ this.initialized = false;
4076
+ this.logger.debug('QRCodeScanner', '二维码扫描器资源已释放');
4077
+ }
4078
+ }
4079
+
4080
+ /**
4081
+ * @file 二维码模块入口
4082
+ * @description 提供二维码识别和解析功能的模块入口
4083
+ * @module modules/qrcode
4084
+ */
4085
+ /**
4086
+ * 二维码模块
4087
+ * 提供二维码检测和解析功能
4088
+ */
4089
+ class QRCodeModule extends BaseModule {
4090
+ /**
4091
+ * 构造函数
4092
+ * @param options 模块配置选项
4093
+ */
4094
+ constructor(options = {}) {
4095
+ super();
4096
+ /** 模块名称 */
4097
+ this.name = 'qrcode';
4098
+ this.options = {
4099
+ enabled: true,
4100
+ scanner: {
4101
+ minConfidence: 0.6,
4102
+ tryMultipleScan: true,
4103
+ returnImage: false,
4104
+ ...options.scanner
4105
+ },
4106
+ imageProcess: {
4107
+ preprocess: true,
4108
+ enhanceContrast: true,
4109
+ threshold: 128,
4110
+ ...options.imageProcess
4111
+ },
4112
+ ...options
4113
+ };
4114
+ // 创建扫描器
4115
+ this.scanner = new QRCodeScanner({
4116
+ minConfidence: this.options.scanner?.minConfidence,
4117
+ returnImage: this.options.scanner?.returnImage,
4118
+ imageProcess: this.options.imageProcess
4119
+ });
4120
+ }
4121
+ /**
4122
+ * 初始化模块
4123
+ */
4124
+ async initialize() {
4125
+ if (this._isInitialized) {
4126
+ return;
4127
+ }
4128
+ this.logger.debug(this.name, '初始化二维码模块');
4129
+ try {
4130
+ // 初始化扫描器
4131
+ await this.scanner.initialize();
4132
+ this._isInitialized = true;
4133
+ this.emit('initialized');
4134
+ this.logger.debug(this.name, '二维码模块初始化完成');
4135
+ }
4136
+ catch (error) {
4137
+ this.logger.error(this.name, '二维码模块初始化失败', error);
4138
+ throw new Error(`二维码模块初始化失败: ${error instanceof Error ? error.message : String(error)}`);
4139
+ }
4140
+ }
4141
+ /**
4142
+ * 扫描图像中的二维码
4143
+ * @param image 图像源
4144
+ * @returns 二维码扫描结果
4145
+ */
4146
+ async scan(image) {
4147
+ this.ensureInitialized();
4148
+ try {
4149
+ // 扫描二维码
4150
+ const scanResult = await this.scanner.scan(image);
4151
+ if (scanResult) {
4152
+ // 保存最后一次扫描结果
4153
+ this.lastScanResult = scanResult;
4154
+ // 触发事件
4155
+ this.emit('qrcode:scanned', { result: scanResult });
4156
+ }
4157
+ return scanResult;
4158
+ }
4159
+ catch (error) {
4160
+ this.logger.error(this.name, '二维码扫描失败', error);
4161
+ throw new Error(`二维码扫描失败: ${error instanceof Error ? error.message : String(error)}`);
4162
+ }
4163
+ }
4164
+ /**
4165
+ * 获取最后一次扫描结果
4166
+ */
4167
+ getLastScanResult() {
4168
+ return this.lastScanResult;
4169
+ }
4170
+ /**
4171
+ * 解析二维码数据
4172
+ * @param data 二维码数据
4173
+ * @returns 解析后的数据对象
4174
+ */
4175
+ parseQRCodeData(data) {
4176
+ try {
4177
+ // 尝试解析为JSON
4178
+ return JSON.parse(data);
4179
+ }
4180
+ catch {
4181
+ // 不是JSON,尝试解析为URL参数
4182
+ if (data.includes('=')) {
4183
+ try {
4184
+ const params = {};
4185
+ const urlParams = new URLSearchParams(data.includes('?') ? data.split('?')[1] : data);
4186
+ urlParams.forEach((value, key) => {
4187
+ params[key] = value;
4188
+ });
4189
+ return params;
4190
+ }
4191
+ catch {
4192
+ // 解析URL参数失败,返回原始字符串
4193
+ return data;
4194
+ }
4195
+ }
4196
+ // 返回原始字符串
4197
+ return data;
4198
+ }
4199
+ }
4200
+ /**
4201
+ * 释放模块资源
4202
+ */
4203
+ async dispose() {
4204
+ if (!this._isInitialized) {
4205
+ return;
4206
+ }
4207
+ this.logger.debug(this.name, '释放二维码模块资源');
4208
+ try {
4209
+ // 释放扫描器资源
4210
+ await this.scanner.dispose();
4211
+ // 调用基类的dispose方法
4212
+ await super.dispose();
4213
+ }
4214
+ catch (error) {
4215
+ this.logger.error(this.name, '二维码模块资源释放失败', error);
4216
+ throw new Error(`二维码模块资源释放失败: ${error instanceof Error ? error.message : String(error)}`);
4217
+ }
4218
+ }
4219
+ }
4220
+
4221
+ /**
4222
+ * @file 人脸模块入口
4223
+ * @description 提供人脸检测、活体检测和人脸比对功能的模块入口
4224
+ * @module modules/face
4225
+ */
4226
+ /**
4227
+ * 人脸模块
4228
+ * 提供人脸检测、活体检测和人脸比对功能
4229
+ */
4230
+ class FaceModule extends BaseModule {
4231
+ /**
4232
+ * 构造函数
4233
+ * @param options 模块配置选项
4234
+ */
4235
+ constructor(options = {}) {
4236
+ super();
4237
+ /** 模块名称 */
4238
+ this.name = 'face';
4239
+ this.options = {
4240
+ enabled: true,
4241
+ detector: {
4242
+ minConfidence: 0.7,
4243
+ detectLandmarks: true,
4244
+ detectAttributes: true,
4245
+ returnFaceImage: false,
4246
+ ...options.detector
4247
+ },
4248
+ liveness: {
4249
+ enabled: false,
4250
+ type: 'passive',
4251
+ minConfidence: 0.8,
4252
+ timeout: 10000,
4253
+ ...options.liveness
4254
+ },
4255
+ comparison: {
4256
+ minSimilarity: 0.8,
4257
+ ...options.comparison
4258
+ },
4259
+ ...options
4260
+ };
4261
+ }
4262
+ /**
4263
+ * 初始化模块
4264
+ */
4265
+ async initialize() {
4266
+ if (this._isInitialized) {
4267
+ return;
4268
+ }
4269
+ this.logger.debug(this.name, '初始化人脸模块');
4270
+ try {
4271
+ // 在此处初始化人脸检测、活体检测和人脸比对所需的模型
4272
+ // 这里只是示例,实际实现需要根据具体的人脸识别库来实现
4273
+ this._isInitialized = true;
4274
+ this.emit('initialized');
4275
+ this.logger.debug(this.name, '人脸模块初始化完成');
4276
+ }
4277
+ catch (error) {
4278
+ this.logger.error(this.name, '人脸模块初始化失败', error);
4279
+ throw new Error(`人脸模块初始化失败: ${error instanceof Error ? error.message : String(error)}`);
4280
+ }
4281
+ }
4282
+ /**
4283
+ * 检测图像中的人脸
4284
+ * @param image 图像源
4285
+ * @returns 人脸检测结果
4286
+ */
4287
+ async detectFace(image) {
4288
+ this.ensureInitialized();
4289
+ try {
4290
+ // 在此处实现人脸检测逻辑
4291
+ // 这里只是示例,实际实现需要根据具体的人脸识别库来实现
4292
+ const faceDetectionResult = {
4293
+ boundingBox: {
4294
+ x: 0,
4295
+ y: 0,
4296
+ width: 100,
4297
+ height: 100
4298
+ },
4299
+ confidence: 0.9
4300
+ };
4301
+ // 保存最后一次检测结果
4302
+ this.lastDetectionResult = faceDetectionResult;
4303
+ // 触发事件
4304
+ this.emit('face:detected', { result: faceDetectionResult });
4305
+ return faceDetectionResult;
4306
+ }
4307
+ catch (error) {
4308
+ this.logger.error(this.name, '人脸检测失败', error);
4309
+ throw new Error(`人脸检测失败: ${error instanceof Error ? error.message : String(error)}`);
4310
+ }
4311
+ }
4312
+ /**
4313
+ * 进行活体检测
4314
+ * @param image 图像源
4315
+ * @returns 活体检测结果
4316
+ */
4317
+ async detectLiveness(image) {
4318
+ this.ensureInitialized();
4319
+ if (!this.options.liveness?.enabled) {
4320
+ throw new Error('活体检测未启用');
4321
+ }
4322
+ try {
4323
+ // 在此处实现活体检测逻辑
4324
+ // 这里只是示例,实际实现需要根据具体的活体检测算法来实现
4325
+ const livenessResult = true;
4326
+ // 触发事件
4327
+ this.emit('face:liveness', { passed: livenessResult });
4328
+ return livenessResult;
4329
+ }
4330
+ catch (error) {
4331
+ this.logger.error(this.name, '活体检测失败', error);
4332
+ throw new Error(`活体检测失败: ${error instanceof Error ? error.message : String(error)}`);
4333
+ }
4334
+ }
4335
+ /**
4336
+ * 比对两个人脸
4337
+ * @param face1 第一个人脸图像
4338
+ * @param face2 第二个人脸图像
4339
+ * @returns 人脸比对结果
4340
+ */
4341
+ async compareFaces(face1, face2) {
4342
+ this.ensureInitialized();
4343
+ try {
4344
+ // 在此处实现人脸比对逻辑
4345
+ // 这里只是示例,实际实现需要根据具体的人脸比对算法来实现
4346
+ const similarity = 0.85;
4347
+ const isMatch = similarity >= (this.options.comparison?.minSimilarity || 0.8);
4348
+ const comparisonResult = {
4349
+ isMatch,
4350
+ similarity,
4351
+ confidence: 0.9
4352
+ };
4353
+ // 触发事件
4354
+ this.emit('face:compared', { result: comparisonResult });
4355
+ return comparisonResult;
4356
+ }
4357
+ catch (error) {
4358
+ this.logger.error(this.name, '人脸比对失败', error);
4359
+ throw new Error(`人脸比对失败: ${error instanceof Error ? error.message : String(error)}`);
4360
+ }
4361
+ }
4362
+ /**
4363
+ * 获取最后一次检测结果
4364
+ */
4365
+ getLastDetectionResult() {
4366
+ return this.lastDetectionResult;
4367
+ }
4368
+ /**
4369
+ * 释放模块资源
4370
+ */
4371
+ async dispose() {
4372
+ if (!this._isInitialized) {
4373
+ return;
4374
+ }
4375
+ this.logger.debug(this.name, '释放人脸模块资源');
4376
+ try {
4377
+ // 在此处释放人脸检测、活体检测和人脸比对所需的模型资源
4378
+ // 这里只是示例,实际实现需要根据具体的人脸识别库来实现
4379
+ // 调用基类的dispose方法
4380
+ await super.dispose();
4381
+ }
4382
+ catch (error) {
4383
+ this.logger.error(this.name, '人脸模块资源释放失败', error);
4384
+ throw new Error(`人脸模块资源释放失败: ${error instanceof Error ? error.message : String(error)}`);
4385
+ }
4386
+ }
4387
+ }
4388
+
4389
+ /**
4390
+ * @file 错误处理模块
4391
+ * @description 定义ID-Scanner-Lib的错误类层次结构
4392
+ * @module core/errors
4393
+ */
4394
+ /**
4395
+ * ID-Scanner-Lib 基础错误类
4396
+ * 所有库特定错误的基类
4397
+ */
4398
+ class IDScannerError extends Error {
4399
+ /**
4400
+ * 构造函数
4401
+ * @param message 错误消息
4402
+ * @param options 错误选项
4403
+ */
4404
+ constructor(message, options) {
4405
+ super(message);
4406
+ // 设置错误名称
4407
+ this.name = this.constructor.name;
4408
+ // 设置错误代码
4409
+ this.code = options?.code || 'UNKNOWN_ERROR';
4410
+ // 设置错误原因
4411
+ this.cause = options?.cause;
4412
+ // 捕获堆栈
4413
+ if (Error.captureStackTrace) {
4414
+ Error.captureStackTrace(this, this.constructor);
4415
+ }
4416
+ }
4417
+ }
4418
+ /**
4419
+ * 初始化错误
4420
+ * 当库初始化失败时抛出
4421
+ */
4422
+ class InitializationError extends IDScannerError {
4423
+ constructor(message, details) {
4424
+ super(`初始化失败: ${message}${details ? ` (${details})` : ''}`, { code: 'INIT_FAILED' });
4425
+ this.name = 'InitializationError';
4426
+ }
4427
+ }
4428
+ /**
4429
+ * 设备错误
4430
+ * 当访问硬件设备(如摄像头)失败时抛出
4431
+ */
4432
+ class DeviceError extends IDScannerError {
4433
+ constructor(message) {
4434
+ super(`设备错误: ${message}`, { code: 'DEVICE_ERROR' });
4435
+ this.name = 'DeviceError';
4436
+ }
4437
+ }
4438
+ /**
4439
+ * 摄像头访问错误
4440
+ * 当无法访问或启动摄像头时抛出
4441
+ */
4442
+ class CameraAccessError extends IDScannerError {
4443
+ constructor(message, options) {
4444
+ super(`摄像头访问失败: ${message}`, {
4445
+ code: options?.code || 'CAMERA_ACCESS_FAILED',
4446
+ cause: options?.cause
4447
+ });
4448
+ this.name = 'CameraAccessError';
4449
+ }
4450
+ }
4451
+ /**
4452
+ * 人脸检测错误
4453
+ * 当人脸检测过程失败时抛出
4454
+ */
4455
+ class FaceDetectionError extends IDScannerError {
4456
+ constructor(message) {
4457
+ super(`人脸检测失败: ${message}`, { code: 'FACE_DETECTION_FAILED' });
4458
+ this.name = 'FaceDetectionError';
4459
+ }
4460
+ }
4461
+ /**
4462
+ * 人脸比对错误
4463
+ * 当人脸比对过程失败时抛出
4464
+ */
4465
+ class FaceComparisonError extends IDScannerError {
4466
+ constructor(message) {
4467
+ super(`人脸比对失败: ${message}`, { code: 'FACE_COMPARISON_FAILED' });
4468
+ this.name = 'FaceComparisonError';
4469
+ }
4470
+ }
4471
+ /**
4472
+ * 活体检测错误
4473
+ * 当活体检测过程失败时抛出
4474
+ */
4475
+ class LivenessDetectionError extends IDScannerError {
4476
+ constructor(message) {
4477
+ super(`活体检测失败: ${message}`, { code: 'LIVENESS_DETECTION_FAILED' });
4478
+ this.name = 'LivenessDetectionError';
4479
+ }
4480
+ }
4481
+ /**
4482
+ * OCR识别错误
4483
+ * 当OCR文字识别失败时抛出
4484
+ */
4485
+ class OCRProcessingError extends IDScannerError {
4486
+ constructor(message) {
4487
+ super(`OCR处理失败: ${message}`, { code: 'OCR_PROCESSING_FAILED' });
4488
+ this.name = 'OCRProcessingError';
4489
+ }
4490
+ }
4491
+ /**
4492
+ * 二维码扫描错误
4493
+ * 当二维码扫描失败时抛出
4494
+ */
4495
+ class QRScanError extends IDScannerError {
4496
+ constructor(message) {
4497
+ super(`二维码扫描失败: ${message}`, { code: 'QR_SCAN_FAILED' });
4498
+ this.name = 'QRScanError';
4499
+ }
4500
+ }
4501
+ /**
4502
+ * 身份证检测错误
4503
+ * 当身份证检测失败时抛出
4504
+ */
4505
+ class IDCardDetectionError extends IDScannerError {
4506
+ constructor(message) {
4507
+ super(`身份证检测失败: ${message}`, { code: 'ID_CARD_DETECTION_FAILED' });
4508
+ this.name = 'IDCardDetectionError';
4509
+ }
4510
+ }
4511
+ /**
4512
+ * 资源加载错误
4513
+ * 当无法加载必要资源(如模型)时抛出
4514
+ */
4515
+ class ResourceLoadError extends IDScannerError {
4516
+ constructor(resource, reason) {
4517
+ super(`无法加载资源 ${resource}: ${reason}`, { code: 'RESOURCE_LOAD_FAILED' });
4518
+ this.name = 'ResourceLoadError';
4519
+ }
4520
+ }
4521
+ /**
4522
+ * 参数错误
4523
+ * 当提供的参数无效时抛出
4524
+ */
4525
+ class InvalidArgumentError extends IDScannerError {
4526
+ constructor(paramName, reason) {
4527
+ super(`无效的参数 ${paramName}: ${reason}`, { code: 'INVALID_ARGUMENT' });
4528
+ this.name = 'InvalidArgumentError';
4529
+ }
4530
+ }
4531
+ /**
4532
+ * 不支持错误
4533
+ * 当尝试使用不支持的功能或当前环境无法使用的功能时抛出
4534
+ */
4535
+ class NotSupportedError extends IDScannerError {
4536
+ constructor(feature) {
4537
+ super(`不支持的功能: ${feature}`, { code: 'NOT_SUPPORTED' });
4538
+ this.name = 'NotSupportedError';
4539
+ }
4540
+ }
4541
+
4542
+ /**
4543
+ * @file 主入口文件
4544
+ * @description ID Scanner库的主入口点,提供统一的API和模块导出
4545
+ * @module index
4546
+ */
4547
+ /**
4548
+ * IDScanner类
4549
+ * 提供整合的身份证、二维码和人脸识别功能
4550
+ */
4551
+ class IDScanner {
4552
+ /**
4553
+ * 构造函数
4554
+ * @param options 配置选项
4555
+ */
4556
+ constructor(options = {}) {
4557
+ /** 是否已经初始化 */
4558
+ this.initialized = false;
4559
+ // 配置日志级别
4560
+ this.logger = Logger.getInstance();
4561
+ if (options.logLevel !== undefined) {
4562
+ this.logger.setLevel(options.logLevel);
4563
+ }
4564
+ this.moduleManager = ModuleManager.getInstance();
4565
+ // 注册模块
4566
+ if (options.enableIDCard !== false) {
4567
+ this.moduleManager.register(new IDCardModule(options.idCard));
4568
+ }
4569
+ if (options.enableQRCode !== false) {
4570
+ this.moduleManager.register(new QRCodeModule(options.qrCode));
4571
+ }
4572
+ if (options.enableFace !== false) {
4573
+ this.moduleManager.register(new FaceModule(options.face));
4574
+ }
4575
+ }
4576
+ /**
4577
+ * 初始化库
4578
+ */
4579
+ async initialize() {
4580
+ if (this.initialized) {
4581
+ return;
4582
+ }
4583
+ this.logger.info('IDScanner', `初始化 IDScanner v${VERSION}`);
4584
+ try {
4585
+ // 初始化所有模块
4586
+ await this.moduleManager.initialize();
4587
+ this.initialized = true;
4588
+ this.logger.info('IDScanner', 'IDScanner初始化完成');
4589
+ }
4590
+ catch (error) {
4591
+ this.logger.error('IDScanner', 'IDScanner初始化失败', error);
4592
+ throw new Error(`IDScanner初始化失败: ${error instanceof Error ? error.message : String(error)}`);
4593
+ }
4594
+ }
4595
+ /**
4596
+ * 获取身份证模块实例
4597
+ * @returns 身份证模块
4598
+ */
4599
+ getIDCardModule() {
4600
+ return this.moduleManager.getModule('id-card');
4601
+ }
4602
+ /**
4603
+ * 获取二维码模块实例
4604
+ * @returns 二维码模块
4605
+ */
4606
+ getQRCodeModule() {
4607
+ return this.moduleManager.getModule('qrcode');
4608
+ }
4609
+ /**
4610
+ * 获取人脸识别模块实例
4611
+ * @returns 人脸识别模块
4612
+ */
4613
+ getFaceModule() {
4614
+ return this.moduleManager.getModule('face');
4615
+ }
4616
+ /**
4617
+ * 释放所有资源
4618
+ */
4619
+ async dispose() {
4620
+ if (!this.initialized) {
4621
+ return;
4622
+ }
4623
+ this.logger.info('IDScanner', '释放IDScanner资源');
4624
+ try {
4625
+ await this.moduleManager.dispose();
4626
+ this.initialized = false;
4627
+ this.logger.info('IDScanner', 'IDScanner资源已释放');
4628
+ }
4629
+ catch (error) {
4630
+ this.logger.error('IDScanner', 'IDScanner资源释放失败', error);
4631
+ throw new Error(`IDScanner资源释放失败: ${error instanceof Error ? error.message : String(error)}`);
4632
+ }
4633
+ }
4634
+ }
4635
+ /** 版本号 */
4636
+ IDScanner.VERSION = VERSION;
4637
+ /** 构建日期 */
4638
+ IDScanner.BUILD_DATE = BUILD_DATE;
4639
+
4640
+ export { CameraAccessError, ConsoleLogHandler, DeviceError, FaceComparisonError, FaceDetectionError, FaceModule, IDCardDetectionError, IDCardModule, IDCardType, IDScanner, IDScannerError, InitializationError, InvalidArgumentError, LivenessDetectionError, LogLevel, Logger, MemoryLogHandler, ModuleManager, NotSupportedError, OCRProcessingError, QRCodeModule, QRScanError, RemoteLogHandler, ResourceLoadError, TaggedLogger, IDScanner as default };
4641
+ //# sourceMappingURL=id-scanner-lib.esm.js.map