id-scanner-lib 1.6.7 → 2.0.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 (48) hide show
  1. package/dist/id-scanner-lib.esm.js +994 -1139
  2. package/dist/id-scanner-lib.esm.js.map +1 -1
  3. package/dist/id-scanner-lib.js +995 -1144
  4. package/dist/id-scanner-lib.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/compat/index.ts +7 -0
  7. package/src/compat/v1-adapter.ts +84 -0
  8. package/src/core/camera-manager.ts +43 -76
  9. package/src/core/camera-stream-manager.ts +318 -0
  10. package/src/core/config.ts +113 -267
  11. package/src/core/errors.ts +68 -117
  12. package/src/core/logger.ts +158 -81
  13. package/src/core/resource-manager.ts +150 -0
  14. package/src/core/scanner.ts +109 -0
  15. package/src/core/utils/browser.ts +7 -0
  16. package/src/core/utils/canvas-pool.ts +171 -0
  17. package/src/core/utils/canvas.ts +7 -0
  18. package/src/core/utils/image.ts +7 -0
  19. package/src/core/utils/index.ts +9 -0
  20. package/src/core/utils/resource-manager.ts +155 -0
  21. package/src/core/utils/validate.ts +7 -0
  22. package/src/core/utils/worker.ts +130 -0
  23. package/src/modules/face/comparator/comparator.ts +45 -0
  24. package/src/modules/face/comparator/index.ts +1 -0
  25. package/src/modules/face/detector/detector.ts +83 -0
  26. package/src/modules/face/detector/index.ts +2 -0
  27. package/src/modules/face/detector/types.ts +80 -0
  28. package/src/modules/face/face-comparator.ts +150 -0
  29. package/src/modules/face/face-detector-options.ts +104 -0
  30. package/src/modules/face/face-detector.ts +121 -376
  31. package/src/modules/face/face-detector.ts.bak +991 -0
  32. package/src/modules/face/face-model-loader.ts +222 -0
  33. package/src/modules/face/face-result-converter.ts +225 -0
  34. package/src/modules/face/face-tracker.ts +207 -0
  35. package/src/modules/face/liveness/index.ts +7 -0
  36. package/src/modules/face/liveness-detector.ts +2 -2
  37. package/src/modules/face/tracker/index.ts +7 -0
  38. package/src/modules/id-card/anti-fake/index.ts +7 -0
  39. package/src/modules/id-card/detector/index.ts +7 -0
  40. package/src/modules/id-card/id-card-text-parser.ts +151 -0
  41. package/src/modules/id-card/ocr-processor.ts +20 -257
  42. package/src/modules/id-card/ocr-worker.ts +2 -183
  43. package/src/modules/id-card/parser/index.ts +7 -0
  44. package/src/modules/qr/scanner/index.ts +7 -0
  45. package/src/utils/canvas-pool.ts +273 -0
  46. package/src/utils/edge-detector.ts +232 -0
  47. package/src/utils/image-processing.ts +92 -419
  48. package/src/utils/index.ts +1 -0
@@ -5,251 +5,95 @@
5
5
  })(this, (function (exports, tesseract_js, imageCompression, jsQR) { 'use strict';
6
6
 
7
7
  /**
8
- * @file 配置管理器
9
- * @description 提供全局配置管理功能
8
+ * @file 配置管理器 + Scanner 配置
9
+ * @description 提供全局配置管理功能 + Scanner v2.0 配置接口
10
10
  * @module core/config
11
11
  */
12
- /**
13
- * 配置管理器
14
- * 负责存储和管理应用程序的配置
15
- */
12
+ // ============================================================
13
+ // 原有 ConfigManager(保持向后兼容)
14
+ // ============================================================
16
15
  class ConfigManager {
17
- /**
18
- * 私有构造函数
19
- */
20
16
  constructor() {
21
- /** 配置存储 */
22
17
  this.config = {};
23
- /** 配置变更回调 */
24
18
  this.changeCallbacks = new Map();
25
- /** 初始化状态 */
26
19
  this.initialized = false;
27
- // 设置默认配置
28
20
  this._resetDefaults();
29
21
  }
30
- /**
31
- * 获取单例实例
32
- */
33
22
  static getInstance() {
34
23
  if (!ConfigManager.instance) {
35
24
  ConfigManager.instance = new ConfigManager();
36
25
  }
37
26
  return ConfigManager.instance;
38
27
  }
39
- /**
40
- * 重置单例实例(主要用于测试)
41
- */
42
28
  static resetInstance() {
43
29
  ConfigManager.instance = undefined;
44
30
  }
45
- /**
46
- * 重置为默认配置
47
- */
48
31
  _resetDefaults() {
49
32
  this.config = {
50
33
  debug: false,
51
34
  logLevel: 'info',
52
- camera: {
53
- resolution: {
54
- width: 1280,
55
- height: 720
56
- },
57
- frameRate: 30,
58
- facingMode: 'environment'
35
+ modelPath: '/models',
36
+ maxRetries: 3,
37
+ retryDelay: 1000,
38
+ detectionInterval: 100,
39
+ face: {
40
+ enabled: true,
41
+ confidenceThreshold: 0.5,
42
+ maxFaces: 10,
43
+ },
44
+ idCard: {
45
+ enabled: false,
46
+ },
47
+ qr: {
48
+ enabled: false,
59
49
  },
60
- performance: {
61
- useCache: true
62
- }
63
50
  };
64
51
  this.initialized = true;
65
52
  }
66
- /**
67
- * 获取配置值
68
- * @param key 配置键,支持点号分隔的路径
69
- * @param defaultValue 默认值
70
- */
71
53
  get(key, defaultValue) {
72
- const value = this.getNestedValue(this.config, key);
73
- return (value !== undefined) ? value : defaultValue;
54
+ const keys = key.split('.');
55
+ let value = this.config;
56
+ for (const k of keys) {
57
+ value = value?.[k];
58
+ }
59
+ return (value !== undefined ? value : defaultValue);
74
60
  }
75
- /**
76
- * 设置配置值
77
- * @param key 配置键,支持点号分隔的路径
78
- * @param value 配置值
79
- */
80
61
  set(key, value) {
81
- const oldValue = this.get(key);
82
- // 如果值相同,不做任何事
83
- if (oldValue === value) {
84
- return;
62
+ const keys = key.split('.');
63
+ let target = this.config;
64
+ for (let i = 0; i < keys.length - 1; i++) {
65
+ if (!target[keys[i]])
66
+ target[keys[i]] = {};
67
+ target = target[keys[i]];
68
+ }
69
+ const oldValue = target[keys[keys.length - 1]];
70
+ target[keys[keys.length - 1]] = value;
71
+ const callbacks = this.changeCallbacks.get(key);
72
+ if (callbacks) {
73
+ callbacks.forEach(cb => cb(value, oldValue));
85
74
  }
86
- this.setNestedValue(this.config, key, value);
87
- // 触发变更回调
88
- this.triggerChangeCallbacks(key, value, oldValue);
89
- }
90
- /**
91
- * 批量更新配置
92
- * @param config 配置对象
93
- */
94
- updateConfig(config) {
95
- Object.entries(config).forEach(([key, value]) => {
96
- this.set(key, value);
97
- });
98
- }
99
- /**
100
- * 重置为默认配置
101
- */
102
- reset() {
103
- const oldConfig = { ...this.config };
104
- // 使用私有 reset 方法重建默认配置
105
- this.config = {
106
- debug: false,
107
- logLevel: 'info',
108
- camera: {
109
- resolution: {
110
- width: 1280,
111
- height: 720
112
- },
113
- frameRate: 30,
114
- facingMode: 'environment'
115
- },
116
- performance: {
117
- useCache: true
118
- }
119
- };
120
- // 触发所有回调
121
- Object.keys(oldConfig).forEach(key => {
122
- this.triggerChangeCallbacks(key, this.get(key), oldConfig[key]);
123
- });
124
75
  }
125
- /**
126
- * 注册配置变更回调
127
- * @param key 配置键
128
- * @param callback 回调函数
129
- */
130
- onConfigChange(key, callback) {
76
+ onChange(key, callback) {
131
77
  if (!this.changeCallbacks.has(key)) {
132
78
  this.changeCallbacks.set(key, []);
133
79
  }
134
80
  this.changeCallbacks.get(key).push(callback);
135
81
  }
136
- /**
137
- * 移除配置变更回调
138
- * @param key 配置键
139
- * @param callback 特定回调函数,如不提供则移除所有
140
- */
141
- offConfigChange(key, callback) {
142
- if (!this.changeCallbacks.has(key)) {
143
- return;
144
- }
145
- if (callback) {
146
- // 移除特定回调
147
- const callbacks = this.changeCallbacks.get(key);
82
+ /** @deprecated Use onChange instead */
83
+ onConfigChange(key, callback) {
84
+ this.onChange(key, callback);
85
+ }
86
+ removeChangeCallback(key, callback) {
87
+ const callbacks = this.changeCallbacks.get(key);
88
+ if (callbacks) {
148
89
  const index = callbacks.indexOf(callback);
149
- if (index !== -1) {
90
+ if (index > -1)
150
91
  callbacks.splice(index, 1);
151
- }
152
- // 如果没有回调,删除键
153
- if (callbacks.length === 0) {
154
- this.changeCallbacks.delete(key);
155
- }
156
- }
157
- else {
158
- // 移除所有回调
159
- this.changeCallbacks.delete(key);
160
- }
161
- }
162
- /**
163
- * 获取嵌套值
164
- * @param obj 对象
165
- * @param path 路径
166
- */
167
- getNestedValue(obj, path) {
168
- // 处理根路径
169
- if (!path) {
170
- return obj;
171
- }
172
- // 处理嵌套路径
173
- const parts = path.split('.');
174
- let current = obj;
175
- for (const part of parts) {
176
- if (current === undefined || current === null) {
177
- return undefined;
178
- }
179
- current = current[part];
180
- }
181
- return current;
182
- }
183
- /**
184
- * 设置嵌套值
185
- * @param obj 对象
186
- * @param path 路径
187
- * @param value 值
188
- */
189
- setNestedValue(obj, path, value) {
190
- // 处理根路径
191
- if (!path) {
192
- return;
193
- }
194
- // 处理嵌套路径
195
- const parts = path.split('.');
196
- let current = obj;
197
- // 遍历路径,直到倒数第二部分
198
- for (let i = 0; i < parts.length - 1; i++) {
199
- const part = parts[i];
200
- // 如果不存在,创建新对象
201
- if (current[part] === undefined || current[part] === null || typeof current[part] !== 'object') {
202
- current[part] = {};
203
- }
204
- current = current[part];
205
- }
206
- // 设置最终值
207
- current[parts[parts.length - 1]] = value;
208
- }
209
- /**
210
- * 触发变更回调
211
- * @param key 配置键
212
- * @param value 新值
213
- * @param oldValue 旧值
214
- */
215
- triggerChangeCallbacks(key, value, oldValue) {
216
- // 触发特定键的回调
217
- if (this.changeCallbacks.has(key)) {
218
- const callbacks = this.changeCallbacks.get(key);
219
- callbacks.forEach(callback => {
220
- try {
221
- callback(value, oldValue);
222
- }
223
- catch (error) {
224
- console.error(`Error in config change callback for key ${key}:`, error);
225
- }
226
- });
227
- }
228
- // 触发父路径的回调
229
- const parts = key.split('.');
230
- while (parts.length > 1) {
231
- parts.pop();
232
- const parentKey = parts.join('.');
233
- if (this.changeCallbacks.has(parentKey)) {
234
- const parentValue = this.get(parentKey);
235
- this.changeCallbacks.get(parentKey).forEach(callback => {
236
- try {
237
- callback(parentValue, parentValue);
238
- }
239
- catch (error) {
240
- console.error(`Error in config change callback for parent key ${parentKey}:`, error);
241
- }
242
- });
243
- }
244
92
  }
245
93
  }
246
- /**
247
- * 检查模块是否启用
248
- * @param moduleName 模块名称
249
- */
250
- isModuleEnabled(moduleName) {
251
- const key = `modules.${moduleName}.enabled`;
252
- return this.get(key) ?? false;
94
+ reset() {
95
+ this.config = {};
96
+ this._resetDefaults();
253
97
  }
254
98
  }
255
99
 
@@ -284,18 +128,20 @@
284
128
  handle(entry) {
285
129
  const timestamp = new Date(entry.timestamp).toISOString();
286
130
  const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
131
+ // 优先使用 error,其次使用 data
132
+ const extra = entry.error || entry.data || '';
287
133
  switch (entry.level) {
288
134
  case exports.LoggerLevel.DEBUG:
289
- console.debug(prefix, entry.message, entry.error || '');
135
+ console.debug(prefix, entry.message, extra);
290
136
  break;
291
137
  case exports.LoggerLevel.INFO:
292
- console.info(prefix, entry.message, entry.error || '');
138
+ console.info(prefix, entry.message, extra);
293
139
  break;
294
140
  case exports.LoggerLevel.WARN:
295
- console.warn(prefix, entry.message, entry.error || '');
141
+ console.warn(prefix, entry.message, extra);
296
142
  break;
297
143
  case exports.LoggerLevel.ERROR:
298
- console.error(prefix, entry.message, entry.error || '');
144
+ console.error(prefix, entry.message, extra);
299
145
  break;
300
146
  // 输出什么也不做
301
147
  }
@@ -369,10 +215,13 @@
369
215
  this.queue = [];
370
216
  /** 定时发送的计时器ID */
371
217
  this.timerId = null;
218
+ /** 当前连续失败计数 */
219
+ this.consecutiveFailures = 0;
372
220
  this.endpoint = endpoint;
373
221
  this.maxQueueSize = maxQueueSize;
374
222
  this.flushInterval = flushInterval;
375
223
  this.isBrowser = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
224
+ this.maxConsecutiveFailures = 10;
376
225
  // 设置定时发送
377
226
  this.startTimer();
378
227
  // 页面卸载前尝试发送剩余日志
@@ -402,47 +251,55 @@
402
251
  flush() {
403
252
  if (this.queue.length === 0)
404
253
  return;
405
- const entriesToSend = [...this.queue];
406
- this.queue = [];
407
- // 防止在 fetch 失败时无限重试
408
- const sendCount = this._sendCount || 0;
409
- this._sendCount = sendCount + 1;
410
- // 如果发送次数过多,停止发送以防止无限循环
411
- if (sendCount > 10) {
412
- console.warn('RemoteLogHandler: Too many failed sends, stopping. Clear queue.');
254
+ // 如果连续失败次数过多,停止发送以防止无限循环
255
+ if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
256
+ console.warn('RemoteLogHandler: Too many consecutive failures, stopping. Clear queue.');
413
257
  this.queue = [];
414
- this._sendCount = 0;
258
+ this.consecutiveFailures = 0;
415
259
  return;
416
260
  }
417
- try {
418
- fetch(this.endpoint, {
419
- method: 'POST',
420
- headers: {
421
- 'Content-Type': 'application/json'
422
- },
423
- body: JSON.stringify(entriesToSend),
424
- keepalive: true
425
- }).catch((err) => {
426
- console.error('Failed to send logs to remote server:', err);
427
- // 防止无限重试 - 如果失败次数过多,丢弃日志
428
- if (this._sendCount > 10) {
429
- console.warn('RemoteLogHandler: Max retry exceeded, discarding logs');
430
- this.queue = []; // 清空队列,避免内存泄漏
431
- this._sendCount = 0;
432
- return;
433
- }
434
- // 失败时把日志放回队列,但防止无限增长
435
- if (this.queue.length < this.maxQueueSize) {
436
- const maxReturn = Math.min(entriesToSend.length, this.maxQueueSize - this.queue.length);
437
- const returnedEntries = entriesToSend.slice(0, maxReturn);
438
- this.queue = [...returnedEntries, ...this.queue];
439
- }
440
- this._sendCount = 0;
441
- });
442
- }
443
- catch (error) {
444
- console.error('Error sending logs:', error);
445
- }
261
+ const entriesToSend = [...this.queue];
262
+ this.queue = [];
263
+ this.sendLogEntries(entriesToSend);
264
+ }
265
+ /**
266
+ * 发送日志条目到远程服务器
267
+ * @param entries 日志条目数组
268
+ */
269
+ sendLogEntries(entries) {
270
+ if (entries.length === 0)
271
+ return;
272
+ const controller = new AbortController();
273
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s 超时
274
+ fetch(this.endpoint, {
275
+ method: 'POST',
276
+ headers: {
277
+ 'Content-Type': 'application/json'
278
+ },
279
+ body: JSON.stringify(entries),
280
+ keepalive: true,
281
+ signal: controller.signal
282
+ }).then(() => {
283
+ clearTimeout(timeoutId);
284
+ this.consecutiveFailures = 0; // 发送成功,重置失败计数
285
+ }).catch((err) => {
286
+ clearTimeout(timeoutId);
287
+ console.error('Failed to send logs to remote server:', err);
288
+ this.consecutiveFailures++;
289
+ // 如果失败次数过多,丢弃日志防止内存泄漏
290
+ if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
291
+ console.warn('RemoteLogHandler: Max consecutive failures exceeded, discarding logs');
292
+ this.queue = [];
293
+ this.consecutiveFailures = 0;
294
+ return;
295
+ }
296
+ // 失败时把日志放回队列,但防止无限增长
297
+ const maxReturn = Math.min(entries.length, this.maxQueueSize - this.queue.length);
298
+ if (maxReturn > 0) {
299
+ const returnedEntries = entries.slice(0, maxReturn);
300
+ this.queue = [...returnedEntries, ...this.queue];
301
+ }
302
+ });
446
303
  }
447
304
  /**
448
305
  * 开始定时发送
@@ -452,7 +309,7 @@
452
309
  return;
453
310
  if (this.timerId !== null)
454
311
  return;
455
- this.timerId = window.setInterval(() => {
312
+ this.timerId = setInterval(() => {
456
313
  this.flush();
457
314
  }, this.flushInterval);
458
315
  }
@@ -461,7 +318,7 @@
461
318
  */
462
319
  stopTimer() {
463
320
  if (this.timerId !== null) {
464
- window.clearInterval(this.timerId);
321
+ clearInterval(this.timerId);
465
322
  this.timerId = null;
466
323
  }
467
324
  }
@@ -538,37 +395,57 @@
538
395
  * 记录调试级别日志
539
396
  * @param tag 标签
540
397
  * @param message 消息
541
- * @param error 错误
398
+ * @param errorOrData 错误对象或结构化数据
542
399
  */
543
- debug(tag, message, error) {
544
- this.log(exports.LoggerLevel.DEBUG, tag, message, error);
400
+ debug(tag, message, errorOrData) {
401
+ if (errorOrData instanceof Error) {
402
+ this.log(exports.LoggerLevel.DEBUG, tag, message, errorOrData);
403
+ }
404
+ else {
405
+ this.logWithData(exports.LoggerLevel.DEBUG, tag, message, errorOrData);
406
+ }
545
407
  }
546
408
  /**
547
409
  * 记录信息级别日志
548
410
  * @param tag 标签
549
411
  * @param message 消息
550
- * @param error 错误
412
+ * @param errorOrData 错误对象或结构化数据
551
413
  */
552
- info(tag, message, error) {
553
- this.log(exports.LoggerLevel.INFO, tag, message, error);
414
+ info(tag, message, errorOrData) {
415
+ if (errorOrData instanceof Error) {
416
+ this.log(exports.LoggerLevel.INFO, tag, message, errorOrData);
417
+ }
418
+ else {
419
+ this.logWithData(exports.LoggerLevel.INFO, tag, message, errorOrData);
420
+ }
554
421
  }
555
422
  /**
556
423
  * 记录警告级别日志
557
424
  * @param tag 标签
558
425
  * @param message 消息
559
- * @param error 错误
426
+ * @param errorOrData 错误对象或结构化数据
560
427
  */
561
- warn(tag, message, error) {
562
- this.log(exports.LoggerLevel.WARN, tag, message, error);
428
+ warn(tag, message, errorOrData) {
429
+ if (errorOrData instanceof Error) {
430
+ this.log(exports.LoggerLevel.WARN, tag, message, errorOrData);
431
+ }
432
+ else {
433
+ this.logWithData(exports.LoggerLevel.WARN, tag, message, errorOrData);
434
+ }
563
435
  }
564
436
  /**
565
437
  * 记录错误级别日志
566
438
  * @param tag 标签
567
439
  * @param message 消息
568
- * @param error 错误
440
+ * @param errorOrData 错误对象或结构化数据
569
441
  */
570
- error(tag, message, error) {
571
- this.log(exports.LoggerLevel.ERROR, tag, message, error);
442
+ error(tag, message, errorOrData) {
443
+ if (errorOrData instanceof Error) {
444
+ this.log(exports.LoggerLevel.ERROR, tag, message, errorOrData);
445
+ }
446
+ else {
447
+ this.logWithData(exports.LoggerLevel.ERROR, tag, message, errorOrData);
448
+ }
572
449
  }
573
450
  /**
574
451
  * 创建标记了特定标签的日志记录器
@@ -613,6 +490,42 @@
613
490
  this.consoleOutput(entry);
614
491
  }
615
492
  }
493
+ /**
494
+ * 记录日志(支持结构化数据)
495
+ * @param level 日志级别
496
+ * @param tag 标签
497
+ * @param message 消息
498
+ * @param data 结构化数据
499
+ */
500
+ logWithData(level, tag, message, data) {
501
+ // 检查日志级别
502
+ const levelValue = this.getLevelValue(level);
503
+ const currentLevelValue = this.getLevelValue(this.logLevel);
504
+ if (levelValue < currentLevelValue) {
505
+ return;
506
+ }
507
+ // 创建日志条目
508
+ const entry = {
509
+ timestamp: Date.now(),
510
+ level: level,
511
+ tag: tag || this.defaultTag,
512
+ message,
513
+ data
514
+ };
515
+ // 分发到所有处理程序
516
+ for (const handler of this.handlers) {
517
+ try {
518
+ handler.handle(entry);
519
+ }
520
+ catch (handlerError) {
521
+ console.error(`[Logger] 处理程序错误:`, handlerError);
522
+ }
523
+ }
524
+ // 如果没有处理程序,使用控制台
525
+ if (this.handlers.length === 0) {
526
+ this.consoleOutput(entry);
527
+ }
528
+ }
616
529
  /**
617
530
  * 控制台输出
618
531
  * @param entry 日志条目
@@ -620,18 +533,20 @@
620
533
  consoleOutput(entry) {
621
534
  const timestamp = new Date(entry.timestamp).toISOString();
622
535
  const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.tag}]`;
536
+ // 构造日志内容:错误对象或数据对象
537
+ const extra = entry.error || entry.data || '';
623
538
  switch (entry.level) {
624
539
  case exports.LoggerLevel.DEBUG:
625
- console.debug(`${prefix} ${entry.message}`, entry.error || '');
540
+ console.debug(`${prefix} ${entry.message}`, extra);
626
541
  break;
627
542
  case exports.LoggerLevel.INFO:
628
- console.info(`${prefix} ${entry.message}`, entry.error || '');
543
+ console.info(`${prefix} ${entry.message}`, extra);
629
544
  break;
630
545
  case exports.LoggerLevel.WARN:
631
- console.warn(`${prefix} ${entry.message}`, entry.error || '');
546
+ console.warn(`${prefix} ${entry.message}`, extra);
632
547
  break;
633
548
  case exports.LoggerLevel.ERROR:
634
- console.error(`${prefix} ${entry.message}`, entry.error || '');
549
+ console.error(`${prefix} ${entry.message}`, extra);
635
550
  break;
636
551
  }
637
552
  }
@@ -1598,93 +1513,685 @@
1598
1513
  }
1599
1514
 
1600
1515
  /**
1601
- * @file 图像处理工具类
1602
- * @description 提供图像预处理功能,用于提高OCR识别率
1603
- * @module ImageProcessor
1604
- * @version 1.3.2
1516
+ * 格式化日期字符串为标准格式 (YYYY-MM-DD)
1605
1517
  */
1518
+ function formatDateString(dateStr) {
1519
+ const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/);
1520
+ if (dateMatch) {
1521
+ const year = dateMatch[1];
1522
+ const month = dateMatch[2].padStart(2, "0");
1523
+ const day = dateMatch[3].padStart(2, "0");
1524
+ return `${year}-${month}-${day}`;
1525
+ }
1526
+ if (/^\d{8}$/.test(dateStr)) {
1527
+ const year = dateStr.substring(0, 4);
1528
+ const month = dateStr.substring(4, 6);
1529
+ const day = dateStr.substring(6, 8);
1530
+ return `${year}-${month}-${day}`;
1531
+ }
1532
+ return dateStr;
1533
+ }
1606
1534
  /**
1607
- * 图像处理工具类
1608
- *
1609
- * 提供各种图像处理功能,用于优化识别效果
1535
+ * IDCardTextParser - 统一解析身份证OCR文本
1536
+ * 提取 ocr-processor.ts 和 ocr-worker.ts 中的解析逻辑
1610
1537
  */
1611
- class ImageProcessor {
1538
+ class IDCardTextParser {
1612
1539
  /**
1613
- * 将ImageData转换为Canvas元素
1614
- *
1615
- * @param {ImageData} imageData - 要转换的图像数据
1616
- * @returns {HTMLCanvasElement} 包含图像的Canvas元素
1540
+ * 解析身份证文本
1541
+ * @param text OCR识别的原始文本
1542
+ * @returns 解析后的身份证信息
1617
1543
  */
1618
- static imageDataToCanvas(imageData) {
1619
- const canvas = document.createElement("canvas");
1620
- canvas.width = imageData.width;
1621
- canvas.height = imageData.height;
1622
- const ctx = canvas.getContext("2d");
1623
- if (ctx) {
1624
- ctx.putImageData(imageData, 0, 0);
1544
+ static parse(text) {
1545
+ const info = {};
1546
+ const processedText = text.replace(/\s+/g, " ").trim();
1547
+ const lines = processedText.split("\n").filter((line) => line.trim());
1548
+ // 1. 解析身份证号码
1549
+ const idNumberRegex = /(\d{17}[\dX])/;
1550
+ const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/;
1551
+ const basicMatch = processedText.match(idNumberRegex);
1552
+ const prefixMatch = processedText.match(idNumberWithPrefixRegex);
1553
+ if (prefixMatch && prefixMatch[1]) {
1554
+ info.idNumber = prefixMatch[1];
1625
1555
  }
1626
- return canvas;
1627
- }
1628
- /**
1629
- * 将Canvas转换为ImageData
1630
- *
1631
- * @param {HTMLCanvasElement} canvas - 要转换的Canvas元素
1632
- * @returns {ImageData|null} Canvas的图像数据,如果获取失败则返回null
1633
- */
1634
- static canvasToImageData(canvas) {
1635
- const ctx = canvas.getContext("2d");
1636
- return ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
1637
- }
1638
- /**
1639
- * 调整图像亮度和对比度
1640
- *
1641
- * @param imageData 原始图像数据
1642
- * @param brightness 亮度调整值 (-100到100)
1643
- * @param contrast 对比度调整值 (-100到100)
1644
- * @returns 处理后的图像数据
1645
- */
1646
- static adjustBrightnessContrast(imageData, brightness = 0, contrast = 0) {
1647
- // 将亮度和对比度范围限制在 -100 到 100 之间
1648
- brightness = Math.max(-100, Math.min(100, brightness));
1649
- contrast = Math.max(-100, Math.min(100, contrast));
1650
- // 将范围转换为适合计算的值
1651
- const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
1652
- const briAdjust = (brightness / 100) * 255;
1653
- const data = imageData.data;
1654
- const length = data.length;
1655
- for (let i = 0; i < length; i += 4) {
1656
- // 分别处理 RGB 三个通道
1657
- for (let j = 0; j < 3; j++) {
1658
- // 应用亮度和对比度调整公式
1659
- const newValue = factor * (data[i + j] + briAdjust - 128) + 128;
1660
- data[i + j] = Math.max(0, Math.min(255, newValue));
1661
- }
1662
- // Alpha 通道保持不变
1556
+ else if (basicMatch && basicMatch[1]) {
1557
+ info.idNumber = basicMatch[1];
1663
1558
  }
1664
- return imageData;
1665
- }
1666
- /**
1667
- * 将图像转换为灰度图
1668
- *
1669
- * @param imageData 原始图像数据
1670
- * @returns 灰度图像数据
1671
- */
1672
- static toGrayscale(imageData) {
1673
- const data = imageData.data;
1674
- const length = data.length;
1675
- for (let i = 0; i < length; i += 4) {
1676
- // 使用加权平均法将 RGB 转换为灰度值
1677
- const gray = data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11;
1678
- data[i] = data[i + 1] = data[i + 2] = gray;
1559
+ // 2. 解析姓名
1560
+ const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/;
1561
+ const nameMatch = processedText.match(nameWithLabelRegex);
1562
+ if (nameMatch && nameMatch[1]) {
1563
+ info.name = nameMatch[1].trim();
1679
1564
  }
1680
- return imageData;
1681
- }
1682
- /**
1683
- * 锐化图像
1684
- *
1685
- * @param imageData 原始图像数据
1686
- * @param amount 锐化程度,默认为2
1687
- * @returns 锐化后的图像数据
1565
+ else {
1566
+ for (const line of lines) {
1567
+ if (line.length >= 2 && line.length <= 5 && /^[一-龥]+$/.test(line) &&
1568
+ !/性别|民族|住址|公民|签发|有效/.test(line)) {
1569
+ info.name = line.trim();
1570
+ break;
1571
+ }
1572
+ }
1573
+ }
1574
+ // 3. 解析性别和民族
1575
+ const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/;
1576
+ const genderOnlyRegex = /性别[\s\:]*([男女])/;
1577
+ const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/;
1578
+ const genderNationalityMatch = processedText.match(genderAndNationalityRegex);
1579
+ const genderOnlyMatch = processedText.match(genderOnlyRegex);
1580
+ const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex);
1581
+ if (genderNationalityMatch) {
1582
+ info.gender = genderNationalityMatch[1];
1583
+ info.ethnicity = genderNationalityMatch[2];
1584
+ }
1585
+ else {
1586
+ if (genderOnlyMatch)
1587
+ info.gender = genderOnlyMatch[1];
1588
+ if (nationalityOnlyMatch)
1589
+ info.ethnicity = nationalityOnlyMatch[1];
1590
+ }
1591
+ // 4. 判断身份证类型
1592
+ if (processedText.includes('出生') || processedText.includes('公民身份号码')) {
1593
+ info.type = exports.IDCardType.FRONT;
1594
+ }
1595
+ else if (processedText.includes('签发机关') || processedText.includes('有效期')) {
1596
+ info.type = exports.IDCardType.BACK;
1597
+ }
1598
+ // 5. 解析出生日期
1599
+ const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/;
1600
+ const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/;
1601
+ const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/;
1602
+ const birthDateMatch = processedText.match(birthDateRegex1) || processedText.match(birthDateRegex2) || processedText.match(birthDateRegex3);
1603
+ if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
1604
+ const year = info.idNumber.substring(6, 10);
1605
+ const month = info.idNumber.substring(10, 12);
1606
+ const day = info.idNumber.substring(12, 14);
1607
+ info.birthDate = `${year}-${month}-${day}`;
1608
+ }
1609
+ else if (birthDateMatch) {
1610
+ const year = birthDateMatch[1];
1611
+ const month = birthDateMatch[2].padStart(2, "0");
1612
+ const day = birthDateMatch[3].padStart(2, "0");
1613
+ info.birthDate = `${year}-${month}-${day}`;
1614
+ }
1615
+ // 6. 解析地址
1616
+ const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/;
1617
+ const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/;
1618
+ const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2);
1619
+ if (addressMatch && addressMatch[1]) {
1620
+ info.address = addressMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim();
1621
+ if (info.address.length > 70)
1622
+ info.address = info.address.substring(0, 70);
1623
+ if (!/[一-龥]/.test(info.address))
1624
+ info.address = '';
1625
+ }
1626
+ // 7. 解析签发机关
1627
+ const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/;
1628
+ const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/;
1629
+ const authorityMatch = processedText.match(authorityRegex1) || processedText.match(authorityRegex2);
1630
+ if (authorityMatch && authorityMatch[1]) {
1631
+ info.issueAuthority = authorityMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim();
1632
+ }
1633
+ // 8. 解析有效期限
1634
+ 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}[日]*|[永久长期]*)/;
1635
+ const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/;
1636
+ const validPeriodMatch = processedText.match(validPeriodRegex1) || processedText.match(validPeriodRegex2);
1637
+ if (validPeriodMatch && validPeriodMatch[1] && validPeriodMatch[3]) {
1638
+ const startDate = formatDateString(validPeriodMatch[1]);
1639
+ const endDate = /\d/.test(validPeriodMatch[3]) ? formatDateString(validPeriodMatch[3]) : '长期有效';
1640
+ info.validFrom = startDate;
1641
+ info.validTo = endDate;
1642
+ info.validPeriod = `${startDate}-${endDate}`;
1643
+ }
1644
+ else if (validPeriodMatch) {
1645
+ info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim();
1646
+ }
1647
+ return info;
1648
+ }
1649
+ }
1650
+
1651
+ /**
1652
+ * @file Canvas 对象池
1653
+ * @description 提供 Canvas 元素的复用机制,减少内存分配和 GC 压力
1654
+ * @module utils/canvas-pool
1655
+ */
1656
+ /**
1657
+ * Canvas 对象池
1658
+ *
1659
+ * 复用 Canvas 元素,避免频繁创建和销毁导致的内存抖动
1660
+ *
1661
+ * @example
1662
+ * ```typescript
1663
+ * const pool = CanvasPool.getInstance();
1664
+ * const { canvas, context } = pool.acquire(100, 200);
1665
+ * // 使用 canvas 进行绘制...
1666
+ * pool.release(canvas);
1667
+ * ```
1668
+ */
1669
+ class CanvasPool {
1670
+ /**
1671
+ * 获取单例实例
1672
+ */
1673
+ static getInstance() {
1674
+ if (!CanvasPool.instance) {
1675
+ CanvasPool.instance = new CanvasPool();
1676
+ }
1677
+ return CanvasPool.instance;
1678
+ }
1679
+ /**
1680
+ * 重置单例实例(主要用于测试)
1681
+ */
1682
+ static resetInstance() {
1683
+ if (CanvasPool.instance) {
1684
+ CanvasPool.instance.dispose();
1685
+ CanvasPool.instance = null;
1686
+ }
1687
+ }
1688
+ /**
1689
+ * 私有构造函数
1690
+ */
1691
+ constructor() {
1692
+ /** Canvas 池存储 */
1693
+ this.pool = new Map();
1694
+ /** 已借出的 Canvas */
1695
+ this.borrowed = new Map();
1696
+ /** 最大池大小(每个尺寸) */
1697
+ this.maxPoolSize = 4;
1698
+ /** Canvas 尺寸容差(允许一定范围的尺寸复用) */
1699
+ this.sizeTolerance = 10;
1700
+ // 页面卸载前清理
1701
+ if (typeof window !== 'undefined') {
1702
+ window.addEventListener('beforeunload', () => this.dispose());
1703
+ }
1704
+ }
1705
+ /**
1706
+ * 生成尺寸键
1707
+ * @param width 宽度
1708
+ * @param height 高度
1709
+ */
1710
+ getSizeKey(width, height) {
1711
+ return `${width}x${height}`;
1712
+ }
1713
+ /**
1714
+ * 查找匹配的尺寸键(考虑容差)
1715
+ * @param width 宽度
1716
+ * @param height 高度
1717
+ */
1718
+ findMatchingSizeKey(width, height) {
1719
+ for (const [key, items] of this.pool.entries()) {
1720
+ const [w, h] = key.split('x').map(Number);
1721
+ if (Math.abs(w - width) <= this.sizeTolerance &&
1722
+ Math.abs(h - height) <= this.sizeTolerance) {
1723
+ // 找到可用的
1724
+ const available = items.filter(item => !item.inUse);
1725
+ if (available.length > 0) {
1726
+ return key;
1727
+ }
1728
+ }
1729
+ }
1730
+ return null;
1731
+ }
1732
+ /**
1733
+ * 从池中获取 Canvas
1734
+ *
1735
+ * @param width 宽度
1736
+ * @param height 高度
1737
+ * @returns Canvas 和其上下文
1738
+ */
1739
+ acquire(width, height) {
1740
+ // 先尝试精确匹配
1741
+ let sizeKey = this.getSizeKey(width, height);
1742
+ let items = this.pool.get(sizeKey);
1743
+ // 如果没有精确匹配,尝试模糊匹配
1744
+ if (!items || items.every(item => item.inUse)) {
1745
+ const matchedKey = this.findMatchingSizeKey(width, height);
1746
+ if (matchedKey) {
1747
+ sizeKey = matchedKey;
1748
+ items = this.pool.get(sizeKey);
1749
+ }
1750
+ }
1751
+ // 如果没有可用的,创建一个新的
1752
+ if (!items || items.every(item => item.inUse)) {
1753
+ const canvas = document.createElement('canvas');
1754
+ canvas.width = width;
1755
+ canvas.height = height;
1756
+ const context = canvas.getContext('2d');
1757
+ const item = {
1758
+ canvas,
1759
+ context,
1760
+ inUse: true,
1761
+ lastUsed: Date.now(),
1762
+ sizeKey: this.getSizeKey(width, height)
1763
+ };
1764
+ // 如果池已满,移除最老的
1765
+ if (!items) {
1766
+ items = [];
1767
+ this.pool.set(sizeKey, items);
1768
+ }
1769
+ else if (items.length >= this.maxPoolSize) {
1770
+ // 找到最老的未使用项并移除
1771
+ let oldestIdx = 0;
1772
+ let oldestTime = Infinity;
1773
+ items.forEach((item, idx) => {
1774
+ if (!item.inUse && item.lastUsed < oldestTime) {
1775
+ oldestTime = item.lastUsed;
1776
+ oldestIdx = idx;
1777
+ }
1778
+ });
1779
+ const removed = items.splice(oldestIdx, 1)[0];
1780
+ this.borrowed.delete(removed.canvas);
1781
+ }
1782
+ items.push(item);
1783
+ this.borrowed.set(canvas, item);
1784
+ return { canvas, context };
1785
+ }
1786
+ // 找到一个空闲的
1787
+ const available = items.find(item => !item.inUse);
1788
+ available.inUse = true;
1789
+ available.lastUsed = Date.now();
1790
+ // 如果尺寸变化,更新 canvas
1791
+ if (available.canvas.width !== width || available.canvas.height !== height) {
1792
+ available.canvas.width = width;
1793
+ available.canvas.height = height;
1794
+ available.sizeKey = sizeKey;
1795
+ }
1796
+ // 清除之前的上下文状态
1797
+ available.context.setTransform(1, 0, 0, 1, 0, 0);
1798
+ available.context.clearRect(0, 0, width, height);
1799
+ this.borrowed.set(available.canvas, available);
1800
+ return { canvas: available.canvas, context: available.context };
1801
+ }
1802
+ /**
1803
+ * 释放 Canvas 回池中
1804
+ *
1805
+ * @param canvas 要释放的 Canvas
1806
+ */
1807
+ release(canvas) {
1808
+ const item = this.borrowed.get(canvas);
1809
+ if (!item) {
1810
+ // 不属于我们管理的 Canvas,忽略
1811
+ return;
1812
+ }
1813
+ item.inUse = false;
1814
+ item.lastUsed = Date.now();
1815
+ this.borrowed.delete(canvas);
1816
+ }
1817
+ /**
1818
+ * 批量释放所有借出的 Canvas
1819
+ */
1820
+ releaseAll() {
1821
+ for (const [, item] of this.borrowed) {
1822
+ item.inUse = false;
1823
+ item.lastUsed = Date.now();
1824
+ }
1825
+ this.borrowed.clear();
1826
+ }
1827
+ /**
1828
+ * 预热池(预创建指定尺寸的 Canvas)
1829
+ *
1830
+ * @param sizes 尺寸数组,每项为 [width, height]
1831
+ */
1832
+ warmup(sizes) {
1833
+ for (const [width, height] of sizes) {
1834
+ this.acquire(width, height);
1835
+ // 立即释放,让它们进入池中
1836
+ const sizeKey = this.getSizeKey(width, height);
1837
+ const items = this.pool.get(sizeKey);
1838
+ if (items && items.length > 0) {
1839
+ const item = items[items.length - 1];
1840
+ item.inUse = false;
1841
+ this.borrowed.delete(item.canvas);
1842
+ }
1843
+ }
1844
+ }
1845
+ /**
1846
+ * 获取池统计信息
1847
+ */
1848
+ getStats() {
1849
+ let totalItems = 0;
1850
+ let borrowedCount = 0;
1851
+ const poolSizes = {};
1852
+ for (const [key, items] of this.pool.entries()) {
1853
+ totalItems += items.length;
1854
+ borrowedCount += items.filter(i => i.inUse).length;
1855
+ poolSizes[key] = {
1856
+ total: items.length,
1857
+ available: items.filter(i => !i.inUse).length
1858
+ };
1859
+ }
1860
+ return { totalItems, borrowedCount, poolSizes };
1861
+ }
1862
+ /**
1863
+ * 清理并释放所有资源
1864
+ */
1865
+ dispose() {
1866
+ this.pool.clear();
1867
+ this.borrowed.clear();
1868
+ }
1869
+ }
1870
+ /** 单例实例 */
1871
+ CanvasPool.instance = null;
1872
+
1873
+ /**
1874
+ * @file 边缘检测器
1875
+ * @description 提供边缘检测算法(Sobel、Canny等)
1876
+ * @module utils/edge-detector
1877
+ */
1878
+ /**
1879
+ * 边缘检测器类
1880
+ * 提供各种边缘检测算法用于图像处理
1881
+ */
1882
+ class EdgeDetector {
1883
+ /**
1884
+ * 使用Sobel算子进行边缘检测
1885
+ * @param imageData 灰度图像数据
1886
+ * @param threshold 边缘阈值,默认为30
1887
+ * @returns 检测到边缘的图像数据
1888
+ */
1889
+ static detectEdges(imageData, threshold = 30) {
1890
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
1891
+ const width = grayscaleImage.width;
1892
+ const height = grayscaleImage.height;
1893
+ const inputData = grayscaleImage.data;
1894
+ const outputData = new Uint8ClampedArray(inputData.length);
1895
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
1896
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
1897
+ for (let y = 1; y < height - 1; y++) {
1898
+ for (let x = 1; x < width - 1; x++) {
1899
+ let gx = 0, gy = 0;
1900
+ for (let ky = -1; ky <= 1; ky++) {
1901
+ for (let kx = -1; kx <= 1; kx++) {
1902
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
1903
+ const pixelVal = inputData[pixelPos];
1904
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
1905
+ gx += pixelVal * sobelX[kernelIdx];
1906
+ gy += pixelVal * sobelY[kernelIdx];
1907
+ }
1908
+ }
1909
+ let magnitude = Math.sqrt(gx * gx + gy * gy);
1910
+ magnitude = magnitude > threshold ? 255 : 0;
1911
+ const pos = (y * width + x) * 4;
1912
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
1913
+ outputData[pos + 3] = 255;
1914
+ }
1915
+ }
1916
+ // 处理边缘
1917
+ for (let i = 0; i < width * 4; i++) {
1918
+ outputData[i] = 0;
1919
+ outputData[(height - 1) * width * 4 + i] = 0;
1920
+ }
1921
+ for (let i = 0; i < height; i++) {
1922
+ const leftPos = i * width * 4;
1923
+ const rightPos = (i * width + width - 1) * 4;
1924
+ for (let j = 0; j < 4; j++) {
1925
+ outputData[leftPos + j] = 0;
1926
+ outputData[rightPos + j] = 0;
1927
+ }
1928
+ }
1929
+ return new ImageData(outputData, width, height);
1930
+ }
1931
+ /**
1932
+ * 卡尼-德里奇边缘检测
1933
+ */
1934
+ static cannyEdgeDetection(imageData, lowThreshold = 20, highThreshold = 50) {
1935
+ const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
1936
+ const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
1937
+ const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
1938
+ const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
1939
+ const thresholdResult = this.hysteresisThresholding(nonMaxSuppressed, blurredImage.width, blurredImage.height, lowThreshold, highThreshold);
1940
+ const outputData = new Uint8ClampedArray(imageData.data.length);
1941
+ for (let i = 0; i < thresholdResult.length; i++) {
1942
+ const pos = i * 4;
1943
+ const value = thresholdResult[i] ? 255 : 0;
1944
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
1945
+ outputData[pos + 3] = 255;
1946
+ }
1947
+ return new ImageData(outputData, blurredImage.width, blurredImage.height);
1948
+ }
1949
+ static toGrayscale(imageData) {
1950
+ const srcData = imageData.data;
1951
+ const destData = new Uint8ClampedArray(srcData);
1952
+ for (let i = 0; i < srcData.length; i += 4) {
1953
+ const gray = srcData[i] * 0.3 + srcData[i + 1] * 0.59 + srcData[i + 2] * 0.11;
1954
+ destData[i] = destData[i + 1] = destData[i + 2] = gray;
1955
+ destData[i + 3] = srcData[i + 3];
1956
+ }
1957
+ return new ImageData(destData, imageData.width, imageData.height);
1958
+ }
1959
+ static gaussianBlur(imageData, sigma = 1.5) {
1960
+ const width = imageData.width, height = imageData.height;
1961
+ const inputData = imageData.data, outputData = new Uint8ClampedArray(inputData.length);
1962
+ const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
1963
+ const halfKernel = Math.floor(kernelSize / 2);
1964
+ const kernel = this.generateGaussianKernel(kernelSize, sigma);
1965
+ for (let y = 0; y < height; y++) {
1966
+ for (let x = 0; x < width; x++) {
1967
+ let sum = 0, weightSum = 0;
1968
+ for (let ky = -halfKernel; ky <= halfKernel; ky++) {
1969
+ for (let kx = -halfKernel; kx <= halfKernel; kx++) {
1970
+ const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
1971
+ const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
1972
+ const pixelPos = (pixelY * width + pixelX) * 4;
1973
+ const kernelY = ky + halfKernel, kernelX = kx + halfKernel;
1974
+ const weight = kernel[kernelY * kernelSize + kernelX];
1975
+ sum += inputData[pixelPos] * weight;
1976
+ weightSum += weight;
1977
+ }
1978
+ }
1979
+ const pos = (y * width + x) * 4;
1980
+ const value = Math.round(sum / weightSum);
1981
+ outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
1982
+ outputData[pos + 3] = 255;
1983
+ }
1984
+ }
1985
+ return new ImageData(outputData, width, height);
1986
+ }
1987
+ static generateGaussianKernel(size, sigma) {
1988
+ const kernel = new Array(size * size);
1989
+ const center = Math.floor(size / 2);
1990
+ let sum = 0;
1991
+ for (let y = 0; y < size; y++) {
1992
+ for (let x = 0; x < size; x++) {
1993
+ const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
1994
+ kernel[y * size + x] = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
1995
+ sum += kernel[y * size + x];
1996
+ }
1997
+ }
1998
+ for (let i = 0; i < kernel.length; i++)
1999
+ kernel[i] /= sum;
2000
+ return kernel;
2001
+ }
2002
+ static computeGradients(imageData) {
2003
+ const width = imageData.width, height = imageData.height;
2004
+ const inputData = imageData.data;
2005
+ const gradientMagnitude = new Array(width * height);
2006
+ const gradientDirection = new Array(width * height);
2007
+ const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
2008
+ const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
2009
+ for (let y = 1; y < height - 1; y++) {
2010
+ for (let x = 1; x < width - 1; x++) {
2011
+ let gx = 0, gy = 0;
2012
+ for (let ky = -1; ky <= 1; ky++) {
2013
+ for (let kx = -1; kx <= 1; kx++) {
2014
+ const pixelPos = ((y + ky) * width + (x + kx)) * 4;
2015
+ const pixelVal = inputData[pixelPos];
2016
+ const kernelIdx = (ky + 1) * 3 + (kx + 1);
2017
+ gx += pixelVal * sobelX[kernelIdx];
2018
+ gy += pixelVal * sobelY[kernelIdx];
2019
+ }
2020
+ }
2021
+ const idx = y * width + x;
2022
+ gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
2023
+ gradientDirection[idx] = Math.atan2(gy, gx);
2024
+ }
2025
+ }
2026
+ return { gradientMagnitude, gradientDirection };
2027
+ }
2028
+ static nonMaxSuppression(gradientMagnitude, gradientDirection, width, height) {
2029
+ const result = new Array(width * height).fill(0);
2030
+ for (let y = 1; y < height - 1; y++) {
2031
+ for (let x = 1; x < width - 1; x++) {
2032
+ const idx = y * width + x;
2033
+ const magnitude = gradientMagnitude[idx];
2034
+ const direction = gradientDirection[idx];
2035
+ const degrees = (direction * 180 / Math.PI + 180) % 180;
2036
+ let neighbor1Idx, neighbor2Idx;
2037
+ if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
2038
+ neighbor1Idx = idx - 1;
2039
+ neighbor2Idx = idx + 1;
2040
+ }
2041
+ else if (degrees >= 22.5 && degrees < 67.5) {
2042
+ neighbor1Idx = (y - 1) * width + (x + 1);
2043
+ neighbor2Idx = (y + 1) * width + (x - 1);
2044
+ }
2045
+ else if (degrees >= 67.5 && degrees < 112.5) {
2046
+ neighbor1Idx = (y - 1) * width + x;
2047
+ neighbor2Idx = (y + 1) * width + x;
2048
+ }
2049
+ else {
2050
+ neighbor1Idx = (y - 1) * width + (x - 1);
2051
+ neighbor2Idx = (y + 1) * width + (x + 1);
2052
+ }
2053
+ if (magnitude >= gradientMagnitude[neighbor1Idx] && magnitude >= gradientMagnitude[neighbor2Idx]) {
2054
+ result[idx] = magnitude;
2055
+ }
2056
+ }
2057
+ }
2058
+ return result;
2059
+ }
2060
+ static hysteresisThresholding(nonMaxSuppressed, width, height, lowThreshold, highThreshold) {
2061
+ const result = new Array(width * height).fill(false);
2062
+ const visited = new Array(width * height).fill(false);
2063
+ const stack = [];
2064
+ for (let i = 0; i < nonMaxSuppressed.length; i++) {
2065
+ if (nonMaxSuppressed[i] >= highThreshold) {
2066
+ result[i] = true;
2067
+ stack.push(i);
2068
+ visited[i] = true;
2069
+ }
2070
+ }
2071
+ const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
2072
+ const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
2073
+ while (stack.length > 0) {
2074
+ const currentIdx = stack.pop();
2075
+ const currentX = currentIdx % width;
2076
+ const currentY = Math.floor(currentIdx / width);
2077
+ for (let i = 0; i < 8; i++) {
2078
+ const newX = currentX + dx[i];
2079
+ const newY = currentY + dy[i];
2080
+ if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
2081
+ const newIdx = newY * width + newX;
2082
+ if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
2083
+ result[newIdx] = true;
2084
+ stack.push(newIdx);
2085
+ visited[newIdx] = true;
2086
+ }
2087
+ }
2088
+ }
2089
+ }
2090
+ return result;
2091
+ }
2092
+ }
2093
+
2094
+ /**
2095
+ * @file 图像处理工具类
2096
+ * @description 提供图像预处理功能,用于提高OCR识别率
2097
+ * @module ImageProcessor
2098
+ * @version 1.4.0
2099
+ */
2100
+ /**
2101
+ * 图像处理工具类
2102
+ *
2103
+ * 提供各种图像处理功能,用于优化识别效果
2104
+ */
2105
+ class ImageProcessor {
2106
+ /**
2107
+ * 将ImageData转换为Canvas元素
2108
+ *
2109
+ * @param {ImageData} imageData - 要转换的图像数据
2110
+ * @returns {HTMLCanvasElement} 包含图像的Canvas元素
2111
+ */
2112
+ static imageDataToCanvas(imageData, usePool = true) {
2113
+ let canvas;
2114
+ let context;
2115
+ if (usePool) {
2116
+ ({ canvas, context } = CanvasPool.getInstance().acquire(imageData.width, imageData.height));
2117
+ }
2118
+ else {
2119
+ canvas = document.createElement("canvas");
2120
+ canvas.width = imageData.width;
2121
+ canvas.height = imageData.height;
2122
+ context = canvas.getContext("2d");
2123
+ }
2124
+ context.putImageData(imageData, 0, 0);
2125
+ if (usePool) {
2126
+ // 立即释放回池中,用户保留 canvas 引用即可
2127
+ CanvasPool.getInstance().release(canvas);
2128
+ }
2129
+ return canvas;
2130
+ }
2131
+ /**
2132
+ * 将Canvas转换为ImageData
2133
+ *
2134
+ * @param {HTMLCanvasElement} canvas - 要转换的Canvas元素
2135
+ * @returns {ImageData|null} Canvas的图像数据,如果获取失败则返回null
2136
+ */
2137
+ static canvasToImageData(canvas) {
2138
+ const ctx = canvas.getContext("2d");
2139
+ return ctx ? ctx.getImageData(0, 0, canvas.width, canvas.height) : null;
2140
+ }
2141
+ /**
2142
+ * 调整图像亮度和对比度
2143
+ *
2144
+ * @param imageData 原始图像数据
2145
+ * @param brightness 亮度调整值 (-100到100)
2146
+ * @param contrast 对比度调整值 (-100到100)
2147
+ * @returns 处理后的图像数据
2148
+ */
2149
+ static adjustBrightnessContrast(imageData, brightness = 0, contrast = 0) {
2150
+ // 将亮度和对比度范围限制在 -100 到 100 之间
2151
+ brightness = Math.max(-100, Math.min(100, brightness));
2152
+ contrast = Math.max(-100, Math.min(100, contrast));
2153
+ // 将范围转换为适合计算的值
2154
+ const factor = (259 * (contrast + 255)) / (255 * (259 - contrast));
2155
+ const briAdjust = (brightness / 100) * 255;
2156
+ const data = imageData.data;
2157
+ const length = data.length;
2158
+ for (let i = 0; i < length; i += 4) {
2159
+ // 分别处理 RGB 三个通道
2160
+ for (let j = 0; j < 3; j++) {
2161
+ // 应用亮度和对比度调整公式
2162
+ const newValue = factor * (data[i + j] + briAdjust - 128) + 128;
2163
+ data[i + j] = Math.max(0, Math.min(255, newValue));
2164
+ }
2165
+ // Alpha 通道保持不变
2166
+ }
2167
+ return imageData;
2168
+ }
2169
+ /**
2170
+ * 将图像转换为灰度图(返回新 ImageData,不修改原图)
2171
+ *
2172
+ * @param imageData 原始图像数据
2173
+ * @returns 灰度图像数据(新对象)
2174
+ */
2175
+ static toGrayscale(imageData) {
2176
+ const srcData = imageData.data;
2177
+ const length = srcData.length;
2178
+ // 创建新数组,避免修改原图
2179
+ const destData = new Uint8ClampedArray(srcData);
2180
+ for (let i = 0; i < length; i += 4) {
2181
+ // 使用加权平均法将 RGB 转换为灰度值
2182
+ const gray = srcData[i] * 0.3 + srcData[i + 1] * 0.59 + srcData[i + 2] * 0.11;
2183
+ destData[i] = destData[i + 1] = destData[i + 2] = gray;
2184
+ // Alpha 通道保持不变
2185
+ destData[i + 3] = srcData[i + 3];
2186
+ }
2187
+ return new ImageData(destData, imageData.width, imageData.height);
2188
+ }
2189
+ /**
2190
+ * 锐化图像
2191
+ *
2192
+ * @param imageData 原始图像数据
2193
+ * @param amount 锐化程度,默认为2
2194
+ * @returns 锐化后的图像数据
1688
2195
  */
1689
2196
  static sharpen(imageData, amount = 2) {
1690
2197
  if (!imageData || !imageData.data)
@@ -1755,35 +2262,48 @@
1755
2262
  return new ImageData(outputData, width, height);
1756
2263
  }
1757
2264
  /**
1758
- * 对图像应用阈值操作,增强对比度
2265
+ * 对图像应用阈值操作,增强对比度(二值化)
1759
2266
  *
1760
2267
  * @param imageData 原始图像数据
1761
2268
  * @param threshold 阈值 (0-255)
1762
- * @returns 处理后的图像数据
2269
+ * @returns 处理后的图像数据(新对象,不修改原图)
1763
2270
  */
1764
2271
  static threshold(imageData, threshold = 128) {
1765
- // 先转换为灰度图(toGrayscale 内部已创建新 ImageData,无需外部拷贝)
2272
+ // 先转换为灰度图(返回新 ImageData,不修改原图)
1766
2273
  const grayscaleImage = this.toGrayscale(imageData);
1767
- const data = grayscaleImage.data;
1768
- for (let i = 0; i < data.length; i += 4) {
2274
+ const srcData = grayscaleImage.data;
2275
+ const length = srcData.length;
2276
+ // 创建新数组存储二值化结果
2277
+ const destData = new Uint8ClampedArray(length);
2278
+ for (let i = 0; i < length; i += 4) {
1769
2279
  // 二值化处理
1770
- const value = data[i] < threshold ? 0 : 255;
1771
- data[i] = data[i + 1] = data[i + 2] = value;
2280
+ const value = srcData[i] < threshold ? 0 : 255;
2281
+ destData[i] = destData[i + 1] = destData[i + 2] = value;
2282
+ destData[i + 3] = srcData[i + 3]; // 保持透明度
1772
2283
  }
1773
- return grayscaleImage;
2284
+ return new ImageData(destData, grayscaleImage.width, grayscaleImage.height);
1774
2285
  }
1775
2286
  /**
1776
- * 将图像转换为黑白图像(二值化)
2287
+ * 将图像转换为黑白图像(二值化,使用OTSU自动阈值)
1777
2288
  *
1778
2289
  * @param imageData 原始图像数据
1779
- * @returns 二值化后的图像数据
2290
+ * @returns 二值化后的图像数据(新对象,不修改原图)
1780
2291
  */
1781
2292
  static toBinaryImage(imageData) {
1782
- // 先转换为灰度图(toGrayscale 内部已创建新 ImageData,无需外部拷贝)
2293
+ // 先转换为灰度图(返回新 ImageData,不修改原图)
1783
2294
  const grayscaleImage = this.toGrayscale(imageData);
1784
2295
  // 使用OTSU算法自动确定阈值
1785
2296
  const threshold = this.getOtsuThreshold(grayscaleImage);
1786
- return this.threshold(grayscaleImage, threshold);
2297
+ // 直接对灰度图进行二值化,避免再次调用 toGrayscale
2298
+ const srcData = grayscaleImage.data;
2299
+ const length = srcData.length;
2300
+ const destData = new Uint8ClampedArray(length);
2301
+ for (let i = 0; i < length; i += 4) {
2302
+ const value = srcData[i] < threshold ? 0 : 255;
2303
+ destData[i] = destData[i + 1] = destData[i + 2] = value;
2304
+ destData[i + 3] = srcData[i + 3]; // 保持透明度
2305
+ }
2306
+ return new ImageData(destData, grayscaleImage.width, grayscaleImage.height);
1787
2307
  }
1788
2308
  /**
1789
2309
  * 使用OTSU算法计算最佳阈值
@@ -1901,24 +2421,20 @@
1901
2421
  const url = URL.createObjectURL(file);
1902
2422
  img.onload = () => {
1903
2423
  try {
1904
- // 创建canvas元素
1905
- const canvas = document.createElement("canvas");
1906
- const ctx = canvas.getContext("2d");
1907
- if (!ctx) {
1908
- reject(new Error("无法创建2D上下文"));
1909
- return;
1910
- }
1911
- canvas.width = img.width;
1912
- canvas.height = img.height;
2424
+ // 使用 Canvas 池获取 canvas
2425
+ const { canvas, context } = CanvasPool.getInstance().acquire(img.width, img.height);
1913
2426
  // 绘制图片到canvas
1914
- ctx.drawImage(img, 0, 0);
2427
+ context.drawImage(img, 0, 0);
1915
2428
  // 获取图像数据
1916
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
2429
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
2430
+ // 释放回池
2431
+ CanvasPool.getInstance().release(canvas);
1917
2432
  // 释放资源
1918
2433
  URL.revokeObjectURL(url);
1919
2434
  resolve(imageData);
1920
2435
  }
1921
2436
  catch (e) {
2437
+ URL.revokeObjectURL(url);
1922
2438
  reject(e);
1923
2439
  }
1924
2440
  };
@@ -1945,16 +2461,12 @@
1945
2461
  static async imageDataToFile(imageData, fileName = "image.jpg", fileType = "image/jpeg", quality = 0.8) {
1946
2462
  return new Promise((resolve, reject) => {
1947
2463
  try {
1948
- const canvas = document.createElement("canvas");
1949
- canvas.width = imageData.width;
1950
- canvas.height = imageData.height;
1951
- const ctx = canvas.getContext("2d");
1952
- if (!ctx) {
1953
- reject(new Error("无法创建2D上下文"));
1954
- return;
1955
- }
1956
- ctx.putImageData(imageData, 0, 0);
2464
+ // 使用 Canvas
2465
+ const { canvas, context } = CanvasPool.getInstance().acquire(imageData.width, imageData.height);
2466
+ context.putImageData(imageData, 0, 0);
1957
2467
  canvas.toBlob((blob) => {
2468
+ // 释放回池
2469
+ CanvasPool.getInstance().release(canvas);
1958
2470
  if (!blob) {
1959
2471
  reject(new Error("无法创建图片Blob"));
1960
2472
  return;
@@ -2003,312 +2515,52 @@
2003
2515
  newHeight = Math.round(height * (maxWidth / width));
2004
2516
  newWidth = maxWidth;
2005
2517
  }
2006
- }
2007
- else {
2008
- if (height > maxHeight) {
2009
- newWidth = Math.round(width * (maxHeight / height));
2010
- newHeight = maxHeight;
2011
- }
2012
- }
2013
- }
2014
- else {
2015
- newWidth = Math.min(width, maxWidth);
2016
- newHeight = Math.min(height, maxHeight);
2017
- }
2018
- // 设置canvas尺寸
2019
- canvas.width = newWidth;
2020
- canvas.height = newHeight;
2021
- // 绘制调整后的图像
2022
- if (image instanceof ImageData) {
2023
- // 创建临时canvas存储ImageData
2024
- const tempCanvas = document.createElement('canvas');
2025
- const tempCtx = tempCanvas.getContext('2d');
2026
- if (!tempCtx) {
2027
- throw new Error('无法创建临时Canvas上下文');
2028
- }
2029
- tempCanvas.width = image.width;
2030
- tempCanvas.height = image.height;
2031
- tempCtx.putImageData(image, 0, 0);
2032
- // 绘制调整后的图像
2033
- ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, newWidth, newHeight);
2034
- }
2035
- else {
2036
- ctx.drawImage(image, 0, 0, width, height, 0, 0, newWidth, newHeight);
2037
- }
2038
- // 返回调整后的ImageData
2039
- return ctx.getImageData(0, 0, newWidth, newHeight);
2040
- }
2041
- /**
2042
- * 边缘检测算法,用于识别图像中的边缘
2043
- * 基于Sobel算子实现
2044
- *
2045
- * @param imageData 原始图像数据,应已转为灰度图
2046
- * @param threshold 边缘阈值,默认为30
2047
- * @returns 检测到边缘的图像数据
2048
- */
2049
- static detectEdges(imageData, threshold = 30) {
2050
- // 确保输入图像是灰度图
2051
- const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
2052
- const width = grayscaleImage.width;
2053
- const height = grayscaleImage.height;
2054
- const inputData = grayscaleImage.data;
2055
- const outputData = new Uint8ClampedArray(inputData.length);
2056
- // Sobel算子 - 水平和垂直方向
2057
- const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
2058
- const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
2059
- // 对每个像素应用Sobel算子
2060
- for (let y = 1; y < height - 1; y++) {
2061
- for (let x = 1; x < width - 1; x++) {
2062
- let gx = 0;
2063
- let gy = 0;
2064
- // 应用卷积
2065
- for (let ky = -1; ky <= 1; ky++) {
2066
- for (let kx = -1; kx <= 1; kx++) {
2067
- const pixelPos = ((y + ky) * width + (x + kx)) * 4;
2068
- const pixelVal = inputData[pixelPos]; // 灰度值
2069
- const kernelIdx = (ky + 1) * 3 + (kx + 1);
2070
- gx += pixelVal * sobelX[kernelIdx];
2071
- gy += pixelVal * sobelY[kernelIdx];
2072
- }
2073
- }
2074
- // 计算梯度强度
2075
- let magnitude = Math.sqrt(gx * gx + gy * gy);
2076
- // 应用阈值
2077
- magnitude = magnitude > threshold ? 255 : 0;
2078
- // 设置输出像素
2079
- const pos = (y * width + x) * 4;
2080
- outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
2081
- outputData[pos + 3] = 255; // 透明度保持完全不透明
2082
- }
2083
- }
2084
- // 处理边缘像素
2085
- for (let i = 0; i < width * 4; i++) {
2086
- // 顶部和底部行
2087
- outputData[i] = 0;
2088
- outputData[(height - 1) * width * 4 + i] = 0;
2089
- }
2090
- for (let i = 0; i < height; i++) {
2091
- // 左右两侧列
2092
- const leftPos = i * width * 4;
2093
- const rightPos = (i * width + width - 1) * 4;
2094
- for (let j = 0; j < 4; j++) {
2095
- outputData[leftPos + j] = 0;
2096
- outputData[rightPos + j] = 0;
2097
- }
2098
- }
2099
- return new ImageData(outputData, width, height);
2100
- }
2101
- /**
2102
- * 卡尼-德里奇边缘检测
2103
- * 相比Sobel更精确的边缘检测算法
2104
- *
2105
- * @param imageData 灰度图像数据
2106
- * @param lowThreshold 低阈值
2107
- * @param highThreshold 高阈值
2108
- * @returns 边缘检测结果
2109
- */
2110
- static cannyEdgeDetection(imageData, lowThreshold = 20, highThreshold = 50) {
2111
- const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
2112
- // 1. 高斯模糊
2113
- const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
2114
- // 2. 使用Sobel算子计算梯度
2115
- const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
2116
- // 3. 非极大值抛弃
2117
- const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
2118
- // 4. 双阈值处理
2119
- const thresholdResult = this.hysteresisThresholding(nonMaxSuppressed, blurredImage.width, blurredImage.height, lowThreshold, highThreshold);
2120
- // 创建输出图像
2121
- const outputData = new Uint8ClampedArray(imageData.data.length);
2122
- // 将结果转换为ImageData
2123
- for (let i = 0; i < thresholdResult.length; i++) {
2124
- const pos = i * 4;
2125
- const value = thresholdResult[i] ? 255 : 0;
2126
- outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
2127
- outputData[pos + 3] = 255;
2128
- }
2129
- return new ImageData(outputData, blurredImage.width, blurredImage.height);
2130
- }
2131
- /**
2132
- * 高斯模糊
2133
- */
2134
- static gaussianBlur(imageData, sigma = 1.5) {
2135
- const width = imageData.width;
2136
- const height = imageData.height;
2137
- const inputData = imageData.data;
2138
- const outputData = new Uint8ClampedArray(inputData.length);
2139
- // 生成高斯核
2140
- const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
2141
- const halfKernel = Math.floor(kernelSize / 2);
2142
- const kernel = this.generateGaussianKernel(kernelSize, sigma);
2143
- // 应用高斯核
2144
- for (let y = 0; y < height; y++) {
2145
- for (let x = 0; x < width; x++) {
2146
- let sum = 0;
2147
- let weightSum = 0;
2148
- for (let ky = -halfKernel; ky <= halfKernel; ky++) {
2149
- for (let kx = -halfKernel; kx <= halfKernel; kx++) {
2150
- const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
2151
- const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
2152
- const pixelPos = (pixelY * width + pixelX) * 4;
2153
- const kernelY = ky + halfKernel;
2154
- const kernelX = kx + halfKernel;
2155
- const weight = kernel[kernelY * kernelSize + kernelX];
2156
- sum += inputData[pixelPos] * weight;
2157
- weightSum += weight;
2158
- }
2159
- }
2160
- const pos = (y * width + x) * 4;
2161
- const value = Math.round(sum / weightSum);
2162
- outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
2163
- outputData[pos + 3] = 255;
2164
- }
2165
- }
2166
- return new ImageData(outputData, width, height);
2167
- }
2168
- /**
2169
- * 生成高斯核
2170
- */
2171
- static generateGaussianKernel(size, sigma) {
2172
- const kernel = new Array(size * size);
2173
- const center = Math.floor(size / 2);
2174
- let sum = 0;
2175
- for (let y = 0; y < size; y++) {
2176
- for (let x = 0; x < size; x++) {
2177
- const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
2178
- const value = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
2179
- kernel[y * size + x] = value;
2180
- sum += value;
2181
- }
2182
- }
2183
- // 归一化
2184
- for (let i = 0; i < kernel.length; i++) {
2185
- kernel[i] /= sum;
2186
- }
2187
- return kernel;
2188
- }
2189
- /**
2190
- * 计算梯度强度和方向
2191
- */
2192
- static computeGradients(imageData) {
2193
- const width = imageData.width;
2194
- const height = imageData.height;
2195
- const inputData = imageData.data;
2196
- const gradientMagnitude = new Array(width * height);
2197
- const gradientDirection = new Array(width * height);
2198
- // Sobel算子
2199
- const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
2200
- const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
2201
- for (let y = 1; y < height - 1; y++) {
2202
- for (let x = 1; x < width - 1; x++) {
2203
- let gx = 0;
2204
- let gy = 0;
2205
- for (let ky = -1; ky <= 1; ky++) {
2206
- for (let kx = -1; kx <= 1; kx++) {
2207
- const pixelPos = ((y + ky) * width + (x + kx)) * 4;
2208
- const pixelVal = inputData[pixelPos];
2209
- const kernelIdx = (ky + 1) * 3 + (kx + 1);
2210
- gx += pixelVal * sobelX[kernelIdx];
2211
- gy += pixelVal * sobelY[kernelIdx];
2212
- }
2518
+ }
2519
+ else {
2520
+ if (height > maxHeight) {
2521
+ newWidth = Math.round(width * (maxHeight / height));
2522
+ newHeight = maxHeight;
2213
2523
  }
2214
- const idx = y * width + x;
2215
- gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
2216
- gradientDirection[idx] = Math.atan2(gy, gx);
2217
2524
  }
2218
2525
  }
2219
- // 处理边界
2220
- for (let y = 0; y < height; y++) {
2221
- for (let x = 0; x < width; x++) {
2222
- if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
2223
- const idx = y * width + x;
2224
- gradientMagnitude[idx] = 0;
2225
- gradientDirection[idx] = 0;
2226
- }
2526
+ else {
2527
+ newWidth = Math.min(width, maxWidth);
2528
+ newHeight = Math.min(height, maxHeight);
2529
+ }
2530
+ // 设置canvas尺寸
2531
+ canvas.width = newWidth;
2532
+ canvas.height = newHeight;
2533
+ // 绘制调整后的图像
2534
+ if (image instanceof ImageData) {
2535
+ // 创建临时canvas存储ImageData
2536
+ const tempCanvas = document.createElement('canvas');
2537
+ const tempCtx = tempCanvas.getContext('2d');
2538
+ if (!tempCtx) {
2539
+ throw new Error('无法创建临时Canvas上下文');
2227
2540
  }
2541
+ tempCanvas.width = image.width;
2542
+ tempCanvas.height = image.height;
2543
+ tempCtx.putImageData(image, 0, 0);
2544
+ // 绘制调整后的图像
2545
+ ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, newWidth, newHeight);
2228
2546
  }
2229
- return { gradientMagnitude, gradientDirection };
2547
+ else {
2548
+ ctx.drawImage(image, 0, 0, width, height, 0, 0, newWidth, newHeight);
2549
+ }
2550
+ // 返回调整后的ImageData
2551
+ return ctx.getImageData(0, 0, newWidth, newHeight);
2230
2552
  }
2231
2553
  /**
2232
- * 非极大值抛弃
2554
+ * @deprecated 请使用 EdgeDetector.detectEdges()
2233
2555
  */
2234
- static nonMaxSuppression(gradientMagnitude, gradientDirection, width, height) {
2235
- const result = new Array(width * height).fill(0);
2236
- for (let y = 1; y < height - 1; y++) {
2237
- for (let x = 1; x < width - 1; x++) {
2238
- const idx = y * width + x;
2239
- const magnitude = gradientMagnitude[idx];
2240
- const direction = gradientDirection[idx];
2241
- // 将方向转化为角度
2242
- const degrees = (direction * 180 / Math.PI + 180) % 180;
2243
- // 获取相邻像素索引
2244
- let neighbor1Idx, neighbor2Idx;
2245
- // 将方向量化为四个方向: 0°, 45°, 90°, 135°
2246
- if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
2247
- // 水平方向
2248
- neighbor1Idx = idx - 1;
2249
- neighbor2Idx = idx + 1;
2250
- }
2251
- else if (degrees >= 22.5 && degrees < 67.5) {
2252
- // 45度方向
2253
- neighbor1Idx = (y - 1) * width + (x + 1);
2254
- neighbor2Idx = (y + 1) * width + (x - 1);
2255
- }
2256
- else if (degrees >= 67.5 && degrees < 112.5) {
2257
- // 垂直方向
2258
- neighbor1Idx = (y - 1) * width + x;
2259
- neighbor2Idx = (y + 1) * width + x;
2260
- }
2261
- else {
2262
- // 135度方向
2263
- neighbor1Idx = (y - 1) * width + (x - 1);
2264
- neighbor2Idx = (y + 1) * width + (x + 1);
2265
- }
2266
- // 检查当前像素是否是最大值
2267
- if (magnitude >= gradientMagnitude[neighbor1Idx] &&
2268
- magnitude >= gradientMagnitude[neighbor2Idx]) {
2269
- result[idx] = magnitude;
2270
- }
2271
- }
2272
- }
2273
- return result;
2556
+ static detectEdges(imageData, threshold = 30) {
2557
+ return EdgeDetector.detectEdges(imageData, threshold);
2274
2558
  }
2275
2559
  /**
2276
- * 双阈值处理
2560
+ * @deprecated 请使用 EdgeDetector.cannyEdgeDetection()
2277
2561
  */
2278
- static hysteresisThresholding(nonMaxSuppressed, width, height, lowThreshold, highThreshold) {
2279
- const result = new Array(width * height).fill(false);
2280
- const visited = new Array(width * height).fill(false);
2281
- const stack = [];
2282
- // 标记强边缘点
2283
- for (let i = 0; i < nonMaxSuppressed.length; i++) {
2284
- if (nonMaxSuppressed[i] >= highThreshold) {
2285
- result[i] = true;
2286
- stack.push(i);
2287
- visited[i] = true;
2288
- }
2289
- }
2290
- // 使用深度优先搜索连接弱边缘
2291
- const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
2292
- const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
2293
- while (stack.length > 0) {
2294
- const currentIdx = stack.pop();
2295
- const currentX = currentIdx % width;
2296
- const currentY = Math.floor(currentIdx / width);
2297
- // 检查88个相邻方向
2298
- for (let i = 0; i < 8; i++) {
2299
- const newX = currentX + dx[i];
2300
- const newY = currentY + dy[i];
2301
- if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
2302
- const newIdx = newY * width + newX;
2303
- if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
2304
- result[newIdx] = true;
2305
- stack.push(newIdx);
2306
- visited[newIdx] = true;
2307
- }
2308
- }
2309
- }
2310
- }
2311
- return result;
2562
+ static cannyEdgeDetection(imageData, lowThreshold = 20, highThreshold = 50) {
2563
+ return EdgeDetector.cannyEdgeDetection(imageData, lowThreshold, highThreshold);
2312
2564
  }
2313
2565
  }
2314
2566
 
@@ -2598,7 +2850,7 @@
2598
2850
  // 识别图像
2599
2851
  const { data } = await worker.recognize(input.imageBase64);
2600
2852
  // 解析身份证信息
2601
- const idCardInfo = parseIDCardText(data.text);
2853
+ const idCardInfo = IDCardTextParser.parse(data.text);
2602
2854
  // 释放Worker资源
2603
2855
  await worker.terminate();
2604
2856
  const processingTime = performance.now() - startTime;
@@ -2612,160 +2864,6 @@
2612
2864
  };
2613
2865
  }
2614
2866
  }
2615
- /**
2616
- * 解析身份证文本
2617
- * @param text OCR识别的文本
2618
- * @returns 解析后的身份证信息
2619
- */
2620
- function parseIDCardText(text) {
2621
- const info = {};
2622
- // 预处理文本,清除多余空白
2623
- const processedText = text.replace(/\s+/g, ' ').trim();
2624
- // 解析身份证号码
2625
- const idNumberRegex = /(\d{17}[\dX])/;
2626
- const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/;
2627
- const basicMatch = processedText.match(idNumberRegex);
2628
- const prefixMatch = processedText.match(idNumberWithPrefixRegex);
2629
- if (prefixMatch && prefixMatch[1]) {
2630
- info.idNumber = prefixMatch[1];
2631
- }
2632
- else if (basicMatch && basicMatch[1]) {
2633
- info.idNumber = basicMatch[1];
2634
- }
2635
- // 解析姓名
2636
- const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/;
2637
- const nameMatch = processedText.match(nameWithLabelRegex);
2638
- if (nameMatch && nameMatch[1]) {
2639
- info.name = nameMatch[1].trim();
2640
- }
2641
- else {
2642
- // 备用方案:查找短行且内容全是汉字
2643
- const lines = processedText.split('\n').filter(line => line.trim());
2644
- for (const line of lines) {
2645
- if (line.length >= 2 &&
2646
- line.length <= 5 &&
2647
- /^[一-龥]+$/.test(line) &&
2648
- !/性别|民族|住址|公民|签发|有效/.test(line)) {
2649
- info.name = line.trim();
2650
- break;
2651
- }
2652
- }
2653
- }
2654
- // 解析性别和民族
2655
- const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/;
2656
- const genderOnlyRegex = /性别[\s\:]*([男女])/;
2657
- const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/;
2658
- const genderNationalityMatch = processedText.match(genderAndNationalityRegex);
2659
- const genderOnlyMatch = processedText.match(genderOnlyRegex);
2660
- const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex);
2661
- if (genderNationalityMatch) {
2662
- info.gender = genderNationalityMatch[1];
2663
- info.ethnicity = genderNationalityMatch[2];
2664
- }
2665
- else {
2666
- if (genderOnlyMatch)
2667
- info.gender = genderOnlyMatch[1];
2668
- if (nationalityOnlyMatch)
2669
- info.ethnicity = nationalityOnlyMatch[1];
2670
- }
2671
- // 根据内容判断身份证类型
2672
- if (processedText.includes('出生') || processedText.includes('公民身份号码')) {
2673
- info.type = exports.IDCardType.FRONT; // 确保类型为枚举值而不是字符串
2674
- }
2675
- else if (processedText.includes('签发机关') || processedText.includes('有效期')) {
2676
- info.type = exports.IDCardType.BACK; // 确保类型为枚举值而不是字符串
2677
- }
2678
- // 解析出生日期
2679
- const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/;
2680
- const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/;
2681
- const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/;
2682
- const birthDateMatch = processedText.match(birthDateRegex1) ||
2683
- processedText.match(birthDateRegex2) ||
2684
- processedText.match(birthDateRegex3);
2685
- if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
2686
- const year = info.idNumber.substring(6, 10);
2687
- const month = info.idNumber.substring(10, 12);
2688
- const day = info.idNumber.substring(12, 14);
2689
- info.birthDate = `${year}-${month}-${day}`;
2690
- }
2691
- else if (birthDateMatch) {
2692
- const year = birthDateMatch[1];
2693
- const month = birthDateMatch[2].padStart(2, '0');
2694
- const day = birthDateMatch[3].padStart(2, '0');
2695
- info.birthDate = `${year}-${month}-${day}`;
2696
- }
2697
- // 解析地址
2698
- const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/;
2699
- const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/;
2700
- const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2);
2701
- if (addressMatch && addressMatch[1]) {
2702
- info.address = addressMatch[1]
2703
- .replace(/\s+/g, '')
2704
- .replace(/\n/g, '')
2705
- .trim();
2706
- if (info.address.length > 70) {
2707
- info.address = info.address.substring(0, 70);
2708
- }
2709
- if (!/[一-龥]/.test(info.address)) {
2710
- info.address = '';
2711
- }
2712
- }
2713
- // 解析签发机关
2714
- const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/;
2715
- const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/;
2716
- const authorityMatch = processedText.match(authorityRegex1) ||
2717
- processedText.match(authorityRegex2);
2718
- if (authorityMatch && authorityMatch[1]) {
2719
- info.issueAuthority = authorityMatch[1]
2720
- .replace(/\s+/g, '')
2721
- .replace(/\n/g, '')
2722
- .trim();
2723
- }
2724
- // 解析有效期限
2725
- 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}[日]*|[永久长期]*)/;
2726
- const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/;
2727
- const validPeriodMatch = processedText.match(validPeriodRegex1) ||
2728
- processedText.match(validPeriodRegex2);
2729
- if (validPeriodMatch) {
2730
- if (validPeriodMatch[1] && validPeriodMatch[3]) {
2731
- const startDate = formatDateString(validPeriodMatch[1]);
2732
- const endDate = /\d/.test(validPeriodMatch[3])
2733
- ? formatDateString(validPeriodMatch[3])
2734
- : '长期有效';
2735
- info.validFrom = startDate;
2736
- info.validTo = endDate;
2737
- info.validPeriod = `${startDate}-${endDate}`;
2738
- }
2739
- else {
2740
- info.validPeriod = validPeriodMatch[0].replace('有效期限', '').trim();
2741
- }
2742
- }
2743
- return info;
2744
- }
2745
- /**
2746
- * 格式化日期字符串
2747
- * @param dateStr 原始日期字符串
2748
- * @returns 格式化后的日期字符串
2749
- */
2750
- function formatDateString(dateStr) {
2751
- // 提取年月日
2752
- const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/);
2753
- if (dateMatch) {
2754
- const year = dateMatch[1];
2755
- const month = dateMatch[2].padStart(2, '0');
2756
- const day = dateMatch[3].padStart(2, '0');
2757
- return `${year}-${month}-${day}`;
2758
- }
2759
- // 纯数字格式如 20220101
2760
- if (/^\d{8}$/.test(dateStr)) {
2761
- const year = dateStr.substring(0, 4);
2762
- const month = dateStr.substring(4, 6);
2763
- const day = dateStr.substring(6, 8);
2764
- return `${year}-${month}-${day}`;
2765
- }
2766
- // 无法格式化,返回原始字符串
2767
- return dateStr;
2768
- }
2769
2867
 
2770
2868
  /**
2771
2869
  * @file OCR处理器
@@ -2817,7 +2915,7 @@
2817
2915
  /**
2818
2916
  * 初始化OCR引擎
2819
2917
  *
2820
- * 加载Tesseract OCR引擎和中文简体语言包,并设置适合身份证识别的参数
2918
+ * 加载Tesseract OCR引擎和中文简体语言包,并设置适合身份证识别的参数
2821
2919
  *
2822
2920
  * @returns {Promise<void>} 初始化完成的Promise
2823
2921
  */
@@ -2839,11 +2937,11 @@
2839
2937
  await this.worker.loadLanguage("chi_sim");
2840
2938
  await this.worker.initialize("chi_sim");
2841
2939
  await this.worker.setParameters({
2842
- tessedit_char_whitelist: "0123456789X年月日壹贰叁肆伍陆柒捌玖拾民族汉满回维吾尔藏苗彝壮朝鲜侗瑶白土家哈尼哈萨克傣黎傈僳佤高山拉祜水东乡纳西景颇柯尔克孜达斡尔仫佬羌布朗撒拉毛南仡佬锡伯阿昌普米塔吉克怒乌孜别克俄罗斯鄂温克德昂保安裕固京塔塔尔独龙鄂伦春赫哲门巴珞巴基诺男女住址出生公民身份号码签发机关有效期省市区县乡镇街道号楼单元室ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", // 优化字符白名单,增加常见地址字符,移除部分不常用汉字
2940
+ tessedit_char_whitelist: "0123456789X年月日壹贰叁肆伍陆柒捌玖拾民族汉满回维吾尔藏苗彝壮朝鲜侗瑶白土家哈尼哈萨克傣黎傈僳佤高山拉祜水东乡纳西景颇柯尔克孜达斡尔仫佬羌布朗撒拉毛南仡佬锡伯阿昌普米塔吉克怒乌孜别克俄罗斯鄂温克德昂保安裕固京塔塔尔独龙鄂伦春赫哲门巴珞巴基诺男女住址出生公民身份号码签发机关有效期省市区县乡镇街道号楼单元室ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", // 优化字符白名单,增加常见地址字符,移除部分不常用汉字
2843
2941
  });
2844
- // 增加一些针对性的参数,提高识别率
2942
+ // 增加一些针对性的参数,提高识别率
2845
2943
  await this.worker.setParameters({
2846
- tessedit_pageseg_mode: 7, // PSM_SINGLE_LINE,使用数字而不是字符串
2944
+ tessedit_pageseg_mode: 7, // PSM_SINGLE_LINE,使用数字而不是字符串
2847
2945
  preserve_interword_spaces: "1", // 保留单词间的空格
2848
2946
  });
2849
2947
  this.initialized = true;
@@ -2859,7 +2957,7 @@
2859
2957
  if (!this.initialized) {
2860
2958
  await this.initialize();
2861
2959
  }
2862
- // 计算图像指纹,用于缓存查找
2960
+ // 计算图像指纹,用于缓存查找
2863
2961
  if (this.options.enableCache) {
2864
2962
  const fingerprint = calculateImageFingerprint(imageData);
2865
2963
  // 检查缓存中是否有结果
@@ -2876,7 +2974,7 @@
2876
2974
  const enhancedImage = ImageProcessor.batchProcess(downsampledImage, {
2877
2975
  brightness: this.options.brightness !== undefined ? this.options.brightness : 10, // 调整默认亮度
2878
2976
  contrast: this.options.contrast !== undefined ? this.options.contrast : 20, // 调整默认对比度
2879
- sharpen: true, // 默认启用锐化,通常对OCR有益
2977
+ sharpen: true, // 默认启用锐化,通常对OCR有益
2880
2978
  });
2881
2979
  // 转换为base64供Tesseract处理
2882
2980
  // 创建一个canvas元素
@@ -2898,11 +2996,11 @@
2898
2996
  // 使用Worker线程处理
2899
2997
  const result = await this.ocrWorker.postMessage({
2900
2998
  imageBase64: base64Image,
2901
- // 不传递函数对象,避免DataCloneError
2999
+ // 不传递函数对象,避免DataCloneError
2902
3000
  tessWorkerOptions: {},
2903
3001
  });
2904
3002
  idCardInfo = result.idCardInfo;
2905
- this.options.logger?.(`OCR处理完成,用时: ${result.processingTime.toFixed(2)}ms`);
3003
+ this.options.logger?.(`OCR处理完成,用时: ${result.processingTime.toFixed(2)}ms`);
2906
3004
  }
2907
3005
  else {
2908
3006
  // 使用主线程处理
@@ -2915,9 +3013,9 @@
2915
3013
  }
2916
3014
  const { data } = (await this.worker.recognize(canvas));
2917
3015
  // 解析身份证信息
2918
- idCardInfo = this.parseIDCardText(data.text);
3016
+ idCardInfo = IDCardTextParser.parse(data.text);
2919
3017
  const processingTime = performance.now() - startTime;
2920
- this.options.logger?.(`OCR处理完成,用时: ${processingTime.toFixed(2)}ms`);
3018
+ this.options.logger?.(`OCR处理完成,用时: ${processingTime.toFixed(2)}ms`);
2921
3019
  }
2922
3020
  // 缓存结果
2923
3021
  if (this.options.enableCache) {
@@ -2934,7 +3032,7 @@
2934
3032
  ? JSON.stringify(error)
2935
3033
  : String(error);
2936
3034
  this.options.logger?.(`OCR识别错误: ${errorMessage}`);
2937
- // 返回 null,让调用方知道识别失败
3035
+ // 返回 null,让调用方知道识别失败
2938
3036
  return null;
2939
3037
  }
2940
3038
  }
@@ -2947,198 +3045,6 @@
2947
3045
  * @param {string} text - OCR识别到的文本
2948
3046
  * @returns {IDCardInfo} 提取到的身份证信息对象
2949
3047
  */
2950
- /**
2951
- * 格式化日期字符串为标准格式 (YYYY-MM-DD)
2952
- * @param dateStr 原始日期字符串
2953
- * @returns 格式化后的日期字符串
2954
- */
2955
- formatDateString(dateStr) {
2956
- // 先尝试提取年月日
2957
- const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/);
2958
- if (dateMatch) {
2959
- const year = dateMatch[1];
2960
- const month = dateMatch[2].padStart(2, "0");
2961
- const day = dateMatch[3].padStart(2, "0");
2962
- return `${year}-${month}-${day}`;
2963
- }
2964
- // 如果是纯数字格式如 20220101
2965
- if (/^\d{8}$/.test(dateStr)) {
2966
- const year = dateStr.substring(0, 4);
2967
- const month = dateStr.substring(4, 6);
2968
- const day = dateStr.substring(6, 8);
2969
- return `${year}-${month}-${day}`;
2970
- }
2971
- // 如果无法格式化,返回原始字符串
2972
- return dateStr;
2973
- }
2974
- /**
2975
- * 验证身份证号是否符合规则
2976
- * @param idNumber 身份证号
2977
- * @returns 是否有效
2978
- */
2979
- validateIDNumber(idNumber) {
2980
- // 基本验证,校验位有效性和长度
2981
- if (!idNumber || idNumber.length !== 18) {
2982
- return false;
2983
- }
2984
- // 检查格式,前17位必须为数字,最后一位可以是数字或'X'
2985
- const pattern = /^\d{17}[\dX]$/;
2986
- if (!pattern.test(idNumber)) {
2987
- return false;
2988
- }
2989
- // 检查日期部分
2990
- parseInt(idNumber.substr(6, 4));
2991
- const month = parseInt(idNumber.substr(10, 2));
2992
- const day = parseInt(idNumber.substr(12, 2));
2993
- if (month < 1 || month > 12 || day < 1 || day > 31) {
2994
- return false;
2995
- }
2996
- // 更详细的检查可以添加校验位的验证等逻辑...
2997
- return true;
2998
- }
2999
- parseIDCardText(text) {
3000
- const info = {};
3001
- // 预处理文本,清除多余空白
3002
- const processedText = text.replace(/\s+/g, " ").trim();
3003
- // 拆分为行,并过滤空行
3004
- const lines = processedText.split("\n").filter((line) => line.trim());
3005
- // 解析身份证号码 - 多种模式匹配
3006
- // 1. 普通18位身份证号模式
3007
- const idNumberRegex = /(\d{17}[\dX])/;
3008
- // 2. 带前缀的模式
3009
- const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/;
3010
- // 尝试所有模式
3011
- let idNumber = null;
3012
- const basicMatch = processedText.match(idNumberRegex);
3013
- const prefixMatch = processedText.match(idNumberWithPrefixRegex);
3014
- if (prefixMatch && prefixMatch[1]) {
3015
- idNumber = prefixMatch[1]; // 首选带前缀的匹配,因为最可靠
3016
- }
3017
- else if (basicMatch && basicMatch[1]) {
3018
- idNumber = basicMatch[1]; // 其次是常规匹配
3019
- }
3020
- if (idNumber) {
3021
- info.idNumber = idNumber;
3022
- }
3023
- // 解析姓名 - 使用多种策略
3024
- // 1. 直接匹配姓名标签近的内容
3025
- const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/;
3026
- const nameMatch = processedText.match(nameWithLabelRegex);
3027
- // 2. 分析行文本寻找姓名
3028
- if (nameMatch && nameMatch[1]) {
3029
- info.name = nameMatch[1].trim();
3030
- }
3031
- else {
3032
- // 备用方案:查找短行且内容全是汉字
3033
- for (const line of lines) {
3034
- if (line.length >= 2 &&
3035
- line.length <= 5 &&
3036
- /^[一-龥]+$/.test(line) &&
3037
- !/性别|民族|住址|公民|签发|有效/.test(line)) {
3038
- info.name = line.trim();
3039
- break;
3040
- }
3041
- }
3042
- }
3043
- // 解析性别和民族 - 多种模式匹配
3044
- // 1. 标准格式匹配
3045
- const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/;
3046
- const genderNationalityMatch = processedText.match(genderAndNationalityRegex);
3047
- // 2. 只匹配性别
3048
- const genderOnlyRegex = /性别[\s\:]*([男女])/;
3049
- const genderOnlyMatch = processedText.match(genderOnlyRegex);
3050
- // 3. 只匹配民族
3051
- const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/;
3052
- const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex);
3053
- if (genderNationalityMatch) {
3054
- info.gender = genderNationalityMatch[1];
3055
- info.nationality = genderNationalityMatch[2];
3056
- }
3057
- else {
3058
- // 分开获取
3059
- if (genderOnlyMatch)
3060
- info.gender = genderOnlyMatch[1];
3061
- if (nationalityOnlyMatch)
3062
- info.nationality = nationalityOnlyMatch[1];
3063
- }
3064
- // 解析出生日期 - 支持多种格式
3065
- // 1. 标准格式:YYYY年MM月DD日
3066
- const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/;
3067
- // 2. 美式日期格式:YYYY-MM-DD或YYYY/MM/DD
3068
- const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/;
3069
- // 3. 带前缀的格式
3070
- const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/;
3071
- let birthDateMatch = processedText.match(birthDateRegex1) ||
3072
- processedText.match(birthDateRegex2) ||
3073
- processedText.match(birthDateRegex3);
3074
- // 4. 从身份证号码中提取出生日期(如果上述方法失败)
3075
- if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
3076
- const year = info.idNumber.substring(6, 10);
3077
- const month = info.idNumber.substring(10, 12);
3078
- const day = info.idNumber.substring(12, 14);
3079
- info.birthDate = `${year}-${month}-${day}`;
3080
- }
3081
- else if (birthDateMatch) {
3082
- // 确保月份和日期是两位数
3083
- const year = birthDateMatch[1];
3084
- const month = birthDateMatch[2].padStart(2, "0");
3085
- const day = birthDateMatch[3].padStart(2, "0");
3086
- info.birthDate = `${year}-${month}-${day}`;
3087
- }
3088
- // 解析地址 - 改进的正则匹配
3089
- // 1. 常规模式
3090
- const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/;
3091
- // 2. 更宽松的模式
3092
- const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/;
3093
- const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2);
3094
- if (addressMatch && addressMatch[1]) {
3095
- // 清理地址中的常见错误和多余空格
3096
- info.address = addressMatch[1]
3097
- .replace(/\s+/g, "")
3098
- .replace(/\n/g, "")
3099
- .trim();
3100
- // 限制地址长度并判断地址合理性
3101
- if (info.address.length > 70) {
3102
- info.address = info.address.substring(0, 70);
3103
- }
3104
- // 确保地址是合理的(不仅仅包含符号或数字)
3105
- if (!/[一-龥]/.test(info.address)) {
3106
- info.address = ""; // 如果没有中文字符,可能不是有效地址
3107
- }
3108
- }
3109
- // 解析签发机关
3110
- const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/;
3111
- const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/;
3112
- const authorityMatch = processedText.match(authorityRegex1) ||
3113
- processedText.match(authorityRegex2);
3114
- if (authorityMatch && authorityMatch[1]) {
3115
- info.issuingAuthority = authorityMatch[1]
3116
- .replace(/\s+/g, "")
3117
- .replace(/\n/g, "")
3118
- .trim();
3119
- }
3120
- // 解析有效期限 - 支持多种格式
3121
- // 1. 常规格式:YYYY.MM.DD-YYYY.MM.DD
3122
- 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}[日]*|[永久长期]*)/;
3123
- // 2. 简化格式:YYYYMMDD-YYYYMMDD
3124
- const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/;
3125
- const validPeriodMatch = processedText.match(validPeriodRegex1) ||
3126
- processedText.match(validPeriodRegex2);
3127
- if (validPeriodMatch) {
3128
- // 格式化为统一的有效期限形式
3129
- if (validPeriodMatch[1] && validPeriodMatch[3]) {
3130
- const startDate = this.formatDateString(validPeriodMatch[1]);
3131
- const endDate = /\d/.test(validPeriodMatch[3])
3132
- ? this.formatDateString(validPeriodMatch[3])
3133
- : "长期有效";
3134
- info.validPeriod = `${startDate}-${endDate}`;
3135
- }
3136
- else {
3137
- info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim();
3138
- }
3139
- }
3140
- return info;
3141
- }
3142
3048
  /**
3143
3049
  * 清除结果缓存
3144
3050
  */
@@ -4714,157 +4620,106 @@
4714
4620
  }
4715
4621
 
4716
4622
  /**
4717
- * @file 错误处理模块
4718
- * @description 定义ID-Scanner-Lib的错误类层次结构
4623
+ * @file Scanner Errors
4624
+ * @description Error classes for Scanner
4719
4625
  * @module core/errors
4720
4626
  */
4721
4627
  /**
4722
- * ID-Scanner-Lib 基础错误类
4723
- * 所有库特定错误的基类
4628
+ * Scanner specific error
4724
4629
  */
4725
- class IDScannerError extends Error {
4630
+ class ScannerError extends Error {
4726
4631
  /**
4727
- * 构造函数
4728
- * @param message 错误消息
4729
- * @param options 错误选项
4632
+ * Create a ScannerError
4633
+ * @param message Error message
4634
+ * @param code Error code
4635
+ * @param module Module name
4730
4636
  */
4731
- constructor(message, options) {
4637
+ constructor(message, code, module) {
4732
4638
  super(message);
4733
- // 设置错误名称
4734
- this.name = this.constructor.name;
4735
- // 设置错误代码
4736
- this.code = options?.code || 'UNKNOWN_ERROR';
4737
- // 设置错误原因
4738
- this.cause = options?.cause;
4739
- // 捕获堆栈 (Node.js专有,浏览器环境忽略)
4740
- if (typeof Error.captureStackTrace === 'function') {
4741
- Error.captureStackTrace(this, this.constructor);
4742
- }
4639
+ this.name = 'ScannerError';
4640
+ this.code = code;
4641
+ this.module = module;
4743
4642
  }
4744
4643
  }
4745
4644
  /**
4746
- * 初始化错误
4747
- * 当库初始化失败时抛出
4645
+ * Camera access error
4748
4646
  */
4749
- class InitializationError extends IDScannerError {
4750
- constructor(message, details) {
4751
- super(`初始化失败: ${message}${details ? ` (${details})` : ''}`, { code: 'INIT_FAILED' });
4752
- this.name = 'InitializationError';
4647
+ class CameraAccessError extends Error {
4648
+ constructor(message = 'Camera access denied') {
4649
+ super(message);
4650
+ this.name = 'CameraAccessError';
4753
4651
  }
4754
4652
  }
4755
4653
  /**
4756
- * 设备错误
4757
- * 当访问硬件设备(如摄像头)失败时抛出
4654
+ * Device error
4758
4655
  */
4759
- class DeviceError extends IDScannerError {
4760
- constructor(message) {
4761
- super(`设备错误: ${message}`, { code: 'DEVICE_ERROR' });
4656
+ class DeviceError extends Error {
4657
+ constructor(message = 'Device error') {
4658
+ super(message);
4762
4659
  this.name = 'DeviceError';
4763
4660
  }
4764
4661
  }
4765
4662
  /**
4766
- * 摄像头访问错误
4767
- * 当无法访问或启动摄像头时抛出
4663
+ * Resource load error
4768
4664
  */
4769
- class CameraAccessError extends IDScannerError {
4770
- constructor(message, options) {
4771
- super(`摄像头访问失败: ${message}`, {
4772
- code: options?.code || 'CAMERA_ACCESS_FAILED',
4773
- cause: options?.cause
4774
- });
4775
- this.name = 'CameraAccessError';
4665
+ class ResourceLoadError extends Error {
4666
+ constructor(message = 'Failed to load resource', id) {
4667
+ super(message);
4668
+ this.name = 'ResourceLoadError';
4669
+ this.id = id;
4776
4670
  }
4777
4671
  }
4778
4672
  /**
4779
- * 人脸检测错误
4780
- * 当人脸检测过程失败时抛出
4673
+ * Face detection error
4781
4674
  */
4782
- class FaceDetectionError extends IDScannerError {
4783
- constructor(message) {
4784
- super(`人脸检测失败: ${message}`, { code: 'FACE_DETECTION_FAILED' });
4675
+ class FaceDetectionError extends Error {
4676
+ constructor(message = 'Face detection failed') {
4677
+ super(message);
4785
4678
  this.name = 'FaceDetectionError';
4786
4679
  }
4787
4680
  }
4788
4681
  /**
4789
- * 人脸比对错误
4790
- * 当人脸比对过程失败时抛出
4682
+ * Face comparison error
4791
4683
  */
4792
- class FaceComparisonError extends IDScannerError {
4793
- constructor(message) {
4794
- super(`人脸比对失败: ${message}`, { code: 'FACE_COMPARISON_FAILED' });
4684
+ class FaceComparisonError extends Error {
4685
+ constructor(message = 'Face comparison failed') {
4686
+ super(message);
4795
4687
  this.name = 'FaceComparisonError';
4796
4688
  }
4797
4689
  }
4798
4690
  /**
4799
- * 活体检测错误
4800
- * 当活体检测过程失败时抛出
4801
- */
4802
- class LivenessDetectionError extends IDScannerError {
4803
- constructor(message) {
4804
- super(`活体检测失败: ${message}`, { code: 'LIVENESS_DETECTION_FAILED' });
4805
- this.name = 'LivenessDetectionError';
4806
- }
4807
- }
4808
- /**
4809
- * OCR识别错误
4810
- * 当OCR文字识别失败时抛出
4811
- */
4812
- class OCRProcessingError extends IDScannerError {
4813
- constructor(message) {
4814
- super(`OCR处理失败: ${message}`, { code: 'OCR_PROCESSING_FAILED' });
4815
- this.name = 'OCRProcessingError';
4816
- }
4817
- }
4818
- /**
4819
- * 二维码扫描错误
4820
- * 当二维码扫描失败时抛出
4821
- */
4822
- class QRScanError extends IDScannerError {
4823
- constructor(message) {
4824
- super(`二维码扫描失败: ${message}`, { code: 'QR_SCAN_FAILED' });
4825
- this.name = 'QRScanError';
4826
- }
4827
- }
4828
- /**
4829
- * 身份证检测错误
4830
- * 当身份证检测失败时抛出
4831
- */
4832
- class IDCardDetectionError extends IDScannerError {
4833
- constructor(message) {
4834
- super(`身份证检测失败: ${message}`, { code: 'ID_CARD_DETECTION_FAILED' });
4835
- this.name = 'IDCardDetectionError';
4836
- }
4837
- }
4838
- /**
4839
- * 资源加载错误
4840
- * 当无法加载必要资源(如模型)时抛出
4691
+ * Initialization error
4841
4692
  */
4842
- class ResourceLoadError extends IDScannerError {
4843
- constructor(resource, reason) {
4844
- super(`无法加载资源 ${resource}: ${reason}`, { code: 'RESOURCE_LOAD_FAILED' });
4845
- this.name = 'ResourceLoadError';
4693
+ class InitializationError extends Error {
4694
+ constructor(message = 'Initialization failed') {
4695
+ super(message);
4696
+ this.name = 'InitializationError';
4846
4697
  }
4847
4698
  }
4848
4699
  /**
4849
- * 参数错误
4850
- * 当提供的参数无效时抛出
4700
+ * Liveness detection error
4851
4701
  */
4852
- class InvalidArgumentError extends IDScannerError {
4853
- constructor(paramName, reason) {
4854
- super(`无效的参数 ${paramName}: ${reason}`, { code: 'INVALID_ARGUMENT' });
4855
- this.name = 'InvalidArgumentError';
4702
+ class LivenessDetectionError extends Error {
4703
+ constructor(message = 'Liveness detection failed') {
4704
+ super(message);
4705
+ this.name = 'LivenessDetectionError';
4856
4706
  }
4857
4707
  }
4858
4708
  /**
4859
- * 不支持错误
4860
- * 当尝试使用不支持的功能或当前环境无法使用的功能时抛出
4709
+ * Error codes for Scanner
4861
4710
  */
4862
- class NotSupportedError extends IDScannerError {
4863
- constructor(feature) {
4864
- super(`不支持的功能: ${feature}`, { code: 'NOT_SUPPORTED' });
4865
- this.name = 'NotSupportedError';
4866
- }
4867
- }
4711
+ const ErrorCodes = {
4712
+ /** Scanner not initialized */
4713
+ NOT_INITIALIZED: 'SCANNER_001',
4714
+ /** Module not found */
4715
+ MODULE_NOT_FOUND: 'SCANNER_002',
4716
+ /** Detection failed */
4717
+ DETECTION_FAILED: 'SCANNER_003',
4718
+ /** Camera access denied */
4719
+ CAMERA_ACCESS_DENIED: 'CAMERA_001',
4720
+ /** Camera not available */
4721
+ CAMERA_NOT_AVAILABLE: 'CAMERA_002',
4722
+ };
4868
4723
 
4869
4724
  /* eslint-disable */
4870
4725
  /**
@@ -4969,27 +4824,23 @@
4969
4824
  exports.ConsoleLogHandler = ConsoleLogHandler;
4970
4825
  exports.DEFAULT_FORMATS = DEFAULT_FORMATS;
4971
4826
  exports.DeviceError = DeviceError;
4827
+ exports.ErrorCodes = ErrorCodes;
4972
4828
  exports.FaceComparisonError = FaceComparisonError;
4973
4829
  exports.FaceDetectionError = FaceDetectionError;
4974
4830
  exports.FaceModule = FaceModule;
4975
- exports.IDCardDetectionError = IDCardDetectionError;
4976
4831
  exports.IDCardModule = IDCardModule;
4977
4832
  exports.IDScanner = IDScanner;
4978
- exports.IDScannerError = IDScannerError;
4979
4833
  exports.InitializationError = InitializationError;
4980
- exports.InvalidArgumentError = InvalidArgumentError;
4981
4834
  exports.LivenessDetectionError = LivenessDetectionError;
4982
4835
  exports.LoadingStateManager = LoadingStateManager;
4983
4836
  exports.LogLevel = LogLevel;
4984
4837
  exports.Logger = Logger;
4985
4838
  exports.MemoryLogHandler = MemoryLogHandler;
4986
4839
  exports.ModuleManager = ModuleManager;
4987
- exports.NotSupportedError = NotSupportedError;
4988
- exports.OCRProcessingError = OCRProcessingError;
4989
4840
  exports.QRCodeModule = QRCodeModule;
4990
- exports.QRScanError = QRScanError;
4991
4841
  exports.RemoteLogHandler = RemoteLogHandler;
4992
4842
  exports.ResourceLoadError = ResourceLoadError;
4843
+ exports.ScannerError = ScannerError;
4993
4844
  exports.TaggedLogger = TaggedLogger;
4994
4845
  exports.createLoadingStateManager = createLoadingStateManager;
4995
4846
  exports.default = IDScanner;