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