id-scanner-lib 1.6.3 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "id-scanner-lib",
3
- "version": "1.6.3",
3
+ "version": "1.6.5",
4
4
  "description": "Browser-based ID card, QR code, and face recognition scanner with liveness detection",
5
5
  "main": "dist/id-scanner-lib.js",
6
6
  "module": "dist/id-scanner-lib.esm.js",
@@ -18,11 +18,38 @@ export class ConfigManager {
18
18
  /** 配置变更回调 */
19
19
  private changeCallbacks: Map<string, Array<(value: any, oldValue: any) => void>> = new Map();
20
20
 
21
+ /** 初始化状态 */
22
+ private initialized = false;
23
+
21
24
  /**
22
25
  * 私有构造函数
23
26
  */
24
27
  private constructor() {
25
28
  // 设置默认配置
29
+ this._resetDefaults();
30
+ }
31
+
32
+ /**
33
+ * 获取单例实例
34
+ */
35
+ public static getInstance(): ConfigManager {
36
+ if (!ConfigManager.instance) {
37
+ ConfigManager.instance = new ConfigManager();
38
+ }
39
+ return ConfigManager.instance;
40
+ }
41
+
42
+ /**
43
+ * 重置单例实例(主要用于测试)
44
+ */
45
+ public static resetInstance(): void {
46
+ ConfigManager.instance = undefined as any;
47
+ }
48
+
49
+ /**
50
+ * 重置为默认配置
51
+ */
52
+ private _resetDefaults(): void {
26
53
  this.config = {
27
54
  debug: false,
28
55
  logLevel: 'info',
@@ -38,16 +65,7 @@ export class ConfigManager {
38
65
  useCache: true
39
66
  }
40
67
  };
41
- }
42
-
43
- /**
44
- * 获取单例实例
45
- */
46
- public static getInstance(): ConfigManager {
47
- if (!ConfigManager.instance) {
48
- ConfigManager.instance = new ConfigManager();
49
- }
50
- return ConfigManager.instance;
68
+ this.initialized = true;
51
69
  }
52
70
 
53
71
  /**
@@ -95,7 +113,7 @@ export class ConfigManager {
95
113
  reset(): void {
96
114
  const oldConfig = { ...this.config };
97
115
 
98
- // 重新创建默认配置
116
+ // 使用私有 reset 方法重建默认配置
99
117
  this.config = {
100
118
  debug: false,
101
119
  logLevel: 'info',
@@ -200,6 +200,18 @@ export class RemoteLogHandler implements LogHandler {
200
200
  const entriesToSend = [...this.queue];
201
201
  this.queue = [];
202
202
 
203
+ // 防止在 fetch 失败时无限重试
204
+ const sendCount = (this as any)._sendCount || 0;
205
+ (this as any)._sendCount = sendCount + 1;
206
+
207
+ // 如果发送次数过多,停止发送以防止无限循环
208
+ if (sendCount > 10) {
209
+ console.warn('RemoteLogHandler: Too many failed sends, stopping. Clear queue.');
210
+ this.queue = [];
211
+ (this as any)._sendCount = 0;
212
+ return;
213
+ }
214
+
203
215
  try {
204
216
  fetch(this.endpoint, {
205
217
  method: 'POST',
@@ -207,14 +219,26 @@ export class RemoteLogHandler implements LogHandler {
207
219
  'Content-Type': 'application/json'
208
220
  },
209
221
  body: JSON.stringify(entriesToSend),
210
- // 不等待响应,避免阻塞
211
222
  keepalive: true
212
- }).catch(err => {
223
+ }).catch((err: Error) => {
213
224
  console.error('Failed to send logs to remote server:', err);
225
+
226
+ // 防止无限重试 - 如果失败次数过多,丢弃日志
227
+ if ((this as any)._sendCount > 10) {
228
+ console.warn('RemoteLogHandler: Max retry exceeded, discarding logs');
229
+ this.queue = []; // 清空队列,避免内存泄漏
230
+ (this as any)._sendCount = 0;
231
+ return;
232
+ }
233
+
214
234
  // 失败时把日志放回队列,但防止无限增长
215
235
  if (this.queue.length < this.maxQueueSize) {
216
- this.queue = [...entriesToSend.slice(0, this.maxQueueSize - this.queue.length), ...this.queue];
236
+ const maxReturn = Math.min(entriesToSend.length, this.maxQueueSize - this.queue.length);
237
+ const returnedEntries = entriesToSend.slice(0, maxReturn);
238
+ this.queue = [...returnedEntries, ...this.queue];
217
239
  }
240
+
241
+ (this as any)._sendCount = 0;
218
242
  });
219
243
  } catch (error) {
220
244
  console.error('Error sending logs:', error);
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  /**
2
3
  * @file 模块管理器
3
4
  * @description 统一管理库的各功能模块,提供模块的注册、初始化和卸载功能
@@ -49,6 +50,7 @@ export class ModuleManager extends EventEmitter {
49
50
  private modules: Map<string, Module> = new Map();
50
51
  private logger: Logger;
51
52
  private initialized = false;
53
+ private initPromise: Promise<void> | null = null;
52
54
 
53
55
  /**
54
56
  * 获取模块管理器单例
@@ -59,6 +61,13 @@ export class ModuleManager extends EventEmitter {
59
61
  }
60
62
  return ModuleManager.instance;
61
63
  }
64
+
65
+ /**
66
+ * 重置单例实例(主要用于测试)
67
+ */
68
+ public static resetInstance(): void {
69
+ ModuleManager.instance = undefined as any;
70
+ }
62
71
 
63
72
  /**
64
73
  * 私有构造函数,确保单例模式
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  /**
2
3
  * @file 主入口文件
3
4
  * @description ID Scanner库的主入口点,提供统一的API和模块导出
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  /**
2
3
  * @file 人脸检测模块
3
4
  * @description 提供人脸检测、跟踪和分析功能
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  /**
2
3
  * @file 人脸模块入口
3
4
  * @description 提供人脸检测、活体检测和人脸比对功能的模块入口
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  /**
2
3
  * @file 活体检测模块
3
4
  * @description 提供人脸活体检测功能
@@ -63,6 +63,10 @@ export class IDCardDetector extends EventEmitter {
63
63
  ocr?: any;
64
64
  antiFake?: any;
65
65
  } = {};
66
+
67
+ /** 重用的 Canvas 元素,用于减少内存分配 */
68
+ private reusableCanvas: HTMLCanvasElement | null = null;
69
+ private reusableContext: CanvasRenderingContext2D | null = null;
66
70
 
67
71
  /**
68
72
  * 构造函数
@@ -187,6 +191,30 @@ export class IDCardDetector extends EventEmitter {
187
191
  return Result.failure(new Error('身份证检测器未初始化'));
188
192
  }
189
193
 
194
+ // 输入验证
195
+ if (!image) {
196
+ return Result.failure(new Error('图像源不能为空'));
197
+ }
198
+
199
+ // 验证 HTMLImageElement 是否已加载
200
+ if (image instanceof HTMLImageElement && !image.complete) {
201
+ return Result.failure(new Error('图像尚未加载完成'));
202
+ }
203
+
204
+ // 验证 ImageData 尺寸
205
+ if (image instanceof ImageData) {
206
+ if (image.width === 0 || image.height === 0) {
207
+ return Result.failure(new Error('图像尺寸无效'));
208
+ }
209
+ }
210
+
211
+ // 验证 Canvas 尺寸
212
+ if (image instanceof HTMLCanvasElement) {
213
+ if (image.width === 0 || image.height === 0) {
214
+ return Result.failure(new Error('Canvas尺寸无效'));
215
+ }
216
+ }
217
+
190
218
  try {
191
219
  this.logger.debug('IDCardDetector', '开始处理图像');
192
220
 
@@ -240,9 +268,7 @@ export class IDCardDetector extends EventEmitter {
240
268
  idCardInfo.image = context.getImageData(0, 0, image.width, image.height);
241
269
  }
242
270
  } else if (image instanceof HTMLImageElement && image.complete) {
243
- const canvas = document.createElement('canvas');
244
- canvas.width = image.naturalWidth;
245
- canvas.height = image.naturalHeight;
271
+ const canvas = this.getReusableCanvas(image.naturalWidth, image.naturalHeight);
246
272
  const context = canvas.getContext('2d');
247
273
  if (context) {
248
274
  context.drawImage(image, 0, 0);
@@ -261,6 +287,29 @@ export class IDCardDetector extends EventEmitter {
261
287
  }
262
288
  }
263
289
 
290
+ /**
291
+ * 获取可重用的 Canvas 元素
292
+ * @param width 宽度
293
+ * @param height 高度
294
+ * @returns CanvasRenderingContext2D
295
+ */
296
+ private getReusableCanvas(width: number, height: number): HTMLCanvasElement {
297
+ // 如果存在可重用的 canvas 且尺寸匹配,直接返回
298
+ if (this.reusableCanvas &&
299
+ this.reusableCanvas.width === width &&
300
+ this.reusableCanvas.height === height) {
301
+ return this.reusableCanvas;
302
+ }
303
+
304
+ // 创建新的 canvas
305
+ this.reusableCanvas = document.createElement('canvas');
306
+ this.reusableCanvas.width = width;
307
+ this.reusableCanvas.height = height;
308
+ this.reusableContext = this.reusableCanvas.getContext('2d');
309
+
310
+ return this.reusableCanvas;
311
+ }
312
+
264
313
  /**
265
314
  * 预处理图像
266
315
  * @param image 图像源
@@ -272,7 +321,6 @@ export class IDCardDetector extends EventEmitter {
272
321
  image: ImageData | HTMLImageElement | HTMLCanvasElement,
273
322
  options: ImageProcessOptions
274
323
  ): Promise<ImageData> {
275
- // 实际项目中,这里应该对图像进行预处理
276
324
  this.logger.debug('IDCardDetector', '预处理图像');
277
325
 
278
326
  // 创建ImageData对象
@@ -281,13 +329,10 @@ export class IDCardDetector extends EventEmitter {
281
329
  if (image instanceof ImageData) {
282
330
  imageData = image;
283
331
  } else {
284
- const canvas = document.createElement('canvas');
285
332
  const width = image instanceof HTMLImageElement ? image.naturalWidth : image.width;
286
333
  const height = image instanceof HTMLImageElement ? image.naturalHeight : image.height;
287
334
 
288
- canvas.width = width;
289
- canvas.height = height;
290
-
335
+ const canvas = this.getReusableCanvas(width, height);
291
336
  const context = canvas.getContext('2d');
292
337
  if (!context) {
293
338
  throw new Error('无法获取Canvas上下文');
@@ -344,21 +389,19 @@ export class IDCardDetector extends EventEmitter {
344
389
  * @private
345
390
  */
346
391
  private async cropAndAlign(image: ImageData, edge: IDCardEdge): Promise<ImageData> {
347
- // 实际项目中,这里应该进行透视变换以校正图像
348
392
  this.logger.debug('IDCardDetector', '裁剪并校正图像');
349
393
 
350
- // 创建Canvas
351
- const canvas = document.createElement('canvas');
352
394
  // 设置标准身份证尺寸比例
353
- canvas.width = 428;
354
- canvas.height = 270;
395
+ const standardWidth = 428;
396
+ const standardHeight = 270;
355
397
 
398
+ const canvas = this.getReusableCanvas(standardWidth, standardHeight);
356
399
  const context = canvas.getContext('2d');
357
400
  if (!context) {
358
401
  throw new Error('无法获取Canvas上下文');
359
402
  }
360
403
 
361
- // 创建临时Canvas
404
+ // 创建临时Canvas用于源图像
362
405
  const tempCanvas = document.createElement('canvas');
363
406
  tempCanvas.width = image.width;
364
407
  tempCanvas.height = image.height;
@@ -383,34 +426,42 @@ export class IDCardDetector extends EventEmitter {
383
426
  edge.bottomLeft.y - edge.topLeft.y,
384
427
  0,
385
428
  0,
386
- canvas.width,
387
- canvas.height
429
+ standardWidth,
430
+ standardHeight
388
431
  );
389
432
 
390
- return context.getImageData(0, 0, canvas.width, canvas.height);
433
+ return context.getImageData(0, 0, standardWidth, standardHeight);
391
434
  }
392
435
 
393
436
  /**
394
437
  * 识别文字
438
+ *
439
+ * @note 此方法返回模拟数据,用于框架开发和测试
440
+ * 实际使用时需要替换为真实的 OCR 模型集成
441
+ *
395
442
  * @param image 图像数据
396
443
  * @param type 身份证类型
397
444
  * @returns 识别结果
398
445
  * @private
399
446
  */
400
447
  private async recognizeText(image: ImageData, type: IDCardType): Promise<Partial<IDCardInfo>> {
401
- // 实际项目中,这里应该调用OCR模型进行文字识别
402
448
  this.logger.debug('IDCardDetector', '识别文字');
403
449
 
404
450
  // 模拟OCR结果
405
- // 在实际应用中,这里应该使用OCR模型进行文字识别
451
+ // 注意:这是框架的占位实现,真实场景需要接入实际的 OCR 服务
452
+ // 可选的方案包括:
453
+ // - TensorFlow.js + 自定义 OCR 模型
454
+ // - 第三方 OCR API (如百度OCR、腾讯OCR)
455
+ // - Tesseract.js WASM 版本
456
+ //
406
457
  if (type === IDCardType.FRONT) {
407
458
  return {
408
- name: '张三',
409
- gender: '男',
410
- ethnicity: '汉',
411
- birthDate: '1990-01-01',
412
- address: '北京市朝阳区某某街道某某社区1号楼1单元101',
413
- idNumber: '110101199001010001',
459
+ name: '张三', // TODO: 替换为真实OCR结果
460
+ gender: '男', // TODO: 替换为真实OCR结果
461
+ ethnicity: '汉', // TODO: 替换为真实OCR结果
462
+ birthDate: '1990-01-01', // TODO: 替换为真实OCR结果
463
+ address: '北京市朝阳区某某街道某某社区1号楼1单元101', // TODO: 替换为真实OCR结果
464
+ idNumber: '110101199001010001', // TODO: 替换为真实OCR结果
414
465
  photoRegion: {
415
466
  x: 300,
416
467
  y: 40,
@@ -420,9 +471,9 @@ export class IDCardDetector extends EventEmitter {
420
471
  };
421
472
  } else if (type === IDCardType.BACK) {
422
473
  return {
423
- issueAuthority: '北京市公安局朝阳分局',
424
- validFrom: '2015-01-01',
425
- validTo: '2035-01-01'
474
+ issueAuthority: '北京市公安局朝阳分局', // TODO: 替换为真实OCR结果
475
+ validFrom: '2015-01-01', // TODO: 替换为真实OCR结果
476
+ validTo: '2035-01-01' // TODO: 替换为真实OCR结果
426
477
  };
427
478
  }
428
479
 
@@ -431,6 +482,10 @@ export class IDCardDetector extends EventEmitter {
431
482
 
432
483
  /**
433
484
  * 检测防伪特征
485
+ *
486
+ * @note 此方法返回模拟数据,用于框架开发和测试
487
+ * 实际使用时需要替换为真实的防伪检测模型
488
+ *
434
489
  * @param image 图像数据
435
490
  * @param detectionResult 检测结果
436
491
  * @returns 防伪检测结果
@@ -440,20 +495,24 @@ export class IDCardDetector extends EventEmitter {
440
495
  image: ImageData,
441
496
  detectionResult: { type: IDCardType; edge: IDCardEdge; confidence: number }
442
497
  ): Promise<IDCardInfo['antiFake']> {
443
- // 实际项目中,这里应该调用防伪模型进行特征检测
444
498
  this.logger.debug('IDCardDetector', '检测防伪特征');
445
499
 
446
500
  // 模拟防伪检测结果
447
- // 在实际应用中,这里应该使用机器学习模型检测防伪特征
501
+ // 注意:这是框架的占位实现,真实场景需要接入实际的防伪检测模型
502
+ // 可选的方案包括:
503
+ // - 紫外光特征检测
504
+ // - 红外光特征检测
505
+ // - 微缩文字检测
506
+ // - 光学变色特征检测
448
507
  return {
449
508
  passed: true,
450
509
  score: 0.92,
451
510
  features: {
452
- fluorescent: true,
453
- microtext: true,
454
- opticalVariable: true,
455
- texture: true,
456
- watermark: true
511
+ fluorescent: true, // TODO: 替换为真实检测结果
512
+ microtext: true, // TODO: 替换为真实检测结果
513
+ opticalVariable: true, // TODO: 替换为真实检测结果
514
+ texture: true, // TODO: 替换为真实检测结果
515
+ watermark: true // TODO: 替换为真实检测结果
457
516
  }
458
517
  };
459
518
  }
@@ -468,6 +527,10 @@ export class IDCardDetector extends EventEmitter {
468
527
  this.models = {};
469
528
  this.initialized = false;
470
529
 
530
+ // 清理可重用的 Canvas
531
+ this.reusableCanvas = null;
532
+ this.reusableContext = null;
533
+
471
534
  // 清理事件监听
472
535
  this.removeAllListeners();
473
536
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  /**
2
3
  * @file 身份证模块入口
3
4
  * @description 提供身份证识别和验证功能的模块入口
@@ -140,7 +140,7 @@ export class OCRProcessor implements Disposable {
140
140
  * @param imageData 要处理的身份证图像数据
141
141
  * @returns 提取的身份证信息
142
142
  */
143
- async processIDCard(imageData: ImageData): Promise<IDCardInfo> {
143
+ async processIDCard(imageData: ImageData): Promise<IDCardInfo | null> {
144
144
  if (!this.initialized) {
145
145
  await this.initialize()
146
146
  }
@@ -249,8 +249,8 @@ export class OCRProcessor implements Disposable {
249
249
 
250
250
  this.options.logger?.(`OCR识别错误: ${errorMessage}`);
251
251
 
252
- // 返回空对象,避免完全失败
253
- return {} as IDCardInfo
252
+ // 返回 null,让调用方知道识别失败
253
+ return null;
254
254
  }
255
255
  }
256
256
 
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  /**
2
3
  * @file 二维码模块入口
3
4
  * @description 提供二维码识别和解析功能的模块入口
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  /**
2
3
  * @file 相机工具类
3
4
  * @description 提供访问和控制设备摄像头的功能
@@ -104,15 +105,28 @@ export class Camera {
104
105
  // 绑定到视频元素
105
106
  if (this.videoElement) {
106
107
  this.videoElement.srcObject = this.stream;
107
- await new Promise<void>((resolve) => {
108
+
109
+ // 添加超时保护,防止永久挂起
110
+ const timeoutPromise = new Promise<never>((_, reject) => {
111
+ setTimeout(() => reject(new Error('摄像头初始化超时')), 10000); // 10秒超时
112
+ });
113
+
114
+ const playPromise = new Promise<void>((resolve, reject) => {
108
115
  if (this.videoElement) {
109
116
  this.videoElement.onloadedmetadata = () => {
110
117
  if (this.videoElement) {
111
- this.videoElement.play().then(() => resolve());
118
+ this.videoElement.play()
119
+ .then(() => resolve())
120
+ .catch(err => reject(err));
112
121
  }
113
122
  };
123
+ this.videoElement.onerror = () => reject(new Error('视频加载失败'));
124
+ } else {
125
+ reject(new Error('视频元素未初始化'));
114
126
  }
115
127
  });
128
+
129
+ await Promise.race([playPromise, timeoutPromise]);
116
130
  }
117
131
  } catch (error) {
118
132
  const logger = Logger.getInstance();
@@ -86,7 +86,7 @@ export class ResourceManager {
86
86
  */
87
87
  export function createDisposable<T>(
88
88
  factory: () => T,
89
- disposeCallback: (resource: T) => Promise<void> | void
89
+ disposeCallback: (_resource: T) => Promise<void> | void
90
90
  ): T & Disposable {
91
91
  const resource = factory();
92
92
 
@@ -106,7 +106,7 @@ export function createDisposable<T>(
106
106
  */
107
107
  export async function using<T extends Disposable, R>(
108
108
  resource: T,
109
- callback: (resource: T) => Promise<R> | R
109
+ callback: (_resource: T) => Promise<R> | R
110
110
  ): Promise<R> {
111
111
  try {
112
112
  const result = await callback(resource);
@@ -75,7 +75,7 @@ export function createWorker<TInput, TOutput>(
75
75
 
76
76
  // 监听Worker消息
77
77
  worker.addEventListener('message', (event) => {
78
- const { messageId, success, result, error } = event.data;
78
+ const { messageId, success, result, error: _error } = event.data;
79
79
 
80
80
  // 查找对应的Promise
81
81
  const promiseHandlers = promiseMap.get(messageId);
@@ -83,7 +83,7 @@ export function createWorker<TInput, TOutput>(
83
83
  if (success) {
84
84
  promiseHandlers.resolve(result);
85
85
  } else {
86
- promiseHandlers.reject(new Error(error));
86
+ promiseHandlers.reject(new Error(_error));
87
87
  }
88
88
 
89
89
  // 删除Promise映射
@@ -113,8 +113,8 @@ export function createWorker<TInput, TOutput>(
113
113
  URL.revokeObjectURL(url);
114
114
 
115
115
  // 拒绝所有未完成的Promise
116
- for (const [, { reject }] of promiseMap) {
117
- reject(new Error('Worker已终止'));
116
+ for (const [, { reject: _reject }] of promiseMap) {
117
+ _reject(new Error('Worker已终止'));
118
118
  }
119
119
 
120
120
  // 清空Promise映射