id-scanner-lib 1.7.0 → 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.
@@ -1,174 +1,125 @@
1
1
  /**
2
- * @file 错误处理模块
3
- * @description 定义ID-Scanner-Lib的错误类层次结构
2
+ * @file Scanner Errors
3
+ * @description Error classes for Scanner
4
4
  * @module core/errors
5
5
  */
6
6
 
7
7
  /**
8
- * ID-Scanner-Lib 基础错误类
9
- * 所有库特定错误的基类
8
+ * Scanner specific error
10
9
  */
11
- export class IDScannerError extends Error {
12
- /** 错误代码 */
10
+ export class ScannerError extends Error {
11
+ /**
12
+ * Error code
13
+ */
13
14
  public code: string;
14
- /** 错误原因 */
15
- public cause?: Error;
15
+ /**
16
+ * Module that threw the error
17
+ */
18
+ public module?: string;
16
19
 
17
20
  /**
18
- * 构造函数
19
- * @param message 错误消息
20
- * @param options 错误选项
21
+ * Create a ScannerError
22
+ * @param message Error message
23
+ * @param code Error code
24
+ * @param module Module name
21
25
  */
22
- constructor(message: string, options?: { code?: string; cause?: Error }) {
26
+ constructor(message: string, code: string, module?: string) {
23
27
  super(message);
24
-
25
- // 设置错误名称
26
- this.name = this.constructor.name;
27
-
28
- // 设置错误代码
29
- this.code = options?.code || 'UNKNOWN_ERROR';
30
-
31
- // 设置错误原因
32
- this.cause = options?.cause;
33
-
34
- // 捕获堆栈 (Node.js专有,浏览器环境忽略)
35
- if (typeof (Error as any).captureStackTrace === 'function') {
36
- (Error as any).captureStackTrace(this, this.constructor);
37
- }
28
+ this.name = 'ScannerError';
29
+ this.code = code;
30
+ this.module = module;
38
31
  }
39
32
  }
40
33
 
41
34
  /**
42
- * 初始化错误
43
- * 当库初始化失败时抛出
35
+ * Camera access error
44
36
  */
45
- export class InitializationError extends IDScannerError {
46
- constructor(message: string, details?: string) {
47
- super(`初始化失败: ${message}${details ? ` (${details})` : ''}`, { code: 'INIT_FAILED' });
48
- this.name = 'InitializationError';
37
+ export class CameraAccessError extends Error {
38
+ constructor(message = 'Camera access denied') {
39
+ super(message);
40
+ this.name = 'CameraAccessError';
49
41
  }
50
42
  }
51
43
 
52
44
  /**
53
- * 设备错误
54
- * 当访问硬件设备(如摄像头)失败时抛出
45
+ * Device error
55
46
  */
56
- export class DeviceError extends IDScannerError {
57
- constructor(message: string) {
58
- super(`设备错误: ${message}`, { code: 'DEVICE_ERROR' });
47
+ export class DeviceError extends Error {
48
+ constructor(message = 'Device error') {
49
+ super(message);
59
50
  this.name = 'DeviceError';
60
51
  }
61
52
  }
62
53
 
63
54
  /**
64
- * 摄像头访问错误
65
- * 当无法访问或启动摄像头时抛出
55
+ * Resource load error
66
56
  */
67
- export class CameraAccessError extends IDScannerError {
68
- constructor(message: string, options?: { code?: string; cause?: Error }) {
69
- super(`摄像头访问失败: ${message}`, {
70
- code: options?.code || 'CAMERA_ACCESS_FAILED',
71
- cause: options?.cause
72
- });
73
- this.name = 'CameraAccessError';
57
+ export class ResourceLoadError extends Error {
58
+ public readonly id?: string;
59
+ constructor(message = 'Failed to load resource', id?: string) {
60
+ super(message);
61
+ this.name = 'ResourceLoadError';
62
+ this.id = id;
74
63
  }
75
64
  }
76
65
 
77
66
  /**
78
- * 人脸检测错误
79
- * 当人脸检测过程失败时抛出
67
+ * Face detection error
80
68
  */
81
- export class FaceDetectionError extends IDScannerError {
82
- constructor(message: string) {
83
- super(`人脸检测失败: ${message}`, { code: 'FACE_DETECTION_FAILED' });
69
+ export class FaceDetectionError extends Error {
70
+ constructor(message = 'Face detection failed') {
71
+ super(message);
84
72
  this.name = 'FaceDetectionError';
85
73
  }
86
74
  }
87
75
 
88
76
  /**
89
- * 人脸比对错误
90
- * 当人脸比对过程失败时抛出
77
+ * Face comparison error
91
78
  */
92
- export class FaceComparisonError extends IDScannerError {
93
- constructor(message: string) {
94
- super(`人脸比对失败: ${message}`, { code: 'FACE_COMPARISON_FAILED' });
79
+ export class FaceComparisonError extends Error {
80
+ constructor(message = 'Face comparison failed') {
81
+ super(message);
95
82
  this.name = 'FaceComparisonError';
96
83
  }
97
84
  }
98
85
 
99
86
  /**
100
- * 活体检测错误
101
- * 当活体检测过程失败时抛出
102
- */
103
- export class LivenessDetectionError extends IDScannerError {
104
- constructor(message: string) {
105
- super(`活体检测失败: ${message}`, { code: 'LIVENESS_DETECTION_FAILED' });
106
- this.name = 'LivenessDetectionError';
107
- }
108
- }
109
-
110
- /**
111
- * OCR识别错误
112
- * 当OCR文字识别失败时抛出
87
+ * Initialization error
113
88
  */
114
- export class OCRProcessingError extends IDScannerError {
115
- constructor(message: string) {
116
- super(`OCR处理失败: ${message}`, { code: 'OCR_PROCESSING_FAILED' });
117
- this.name = 'OCRProcessingError';
118
- }
119
- }
120
-
121
- /**
122
- * 二维码扫描错误
123
- * 当二维码扫描失败时抛出
124
- */
125
- export class QRScanError extends IDScannerError {
126
- constructor(message: string) {
127
- super(`二维码扫描失败: ${message}`, { code: 'QR_SCAN_FAILED' });
128
- this.name = 'QRScanError';
129
- }
130
- }
131
-
132
- /**
133
- * 身份证检测错误
134
- * 当身份证检测失败时抛出
135
- */
136
- export class IDCardDetectionError extends IDScannerError {
137
- constructor(message: string) {
138
- super(`身份证检测失败: ${message}`, { code: 'ID_CARD_DETECTION_FAILED' });
139
- this.name = 'IDCardDetectionError';
89
+ export class InitializationError extends Error {
90
+ constructor(message = 'Initialization failed') {
91
+ super(message);
92
+ this.name = 'InitializationError';
140
93
  }
141
94
  }
142
95
 
143
96
  /**
144
- * 资源加载错误
145
- * 当无法加载必要资源(如模型)时抛出
97
+ * Liveness detection error
146
98
  */
147
- export class ResourceLoadError extends IDScannerError {
148
- constructor(resource: string, reason: string) {
149
- super(`无法加载资源 ${resource}: ${reason}`, { code: 'RESOURCE_LOAD_FAILED' });
150
- this.name = 'ResourceLoadError';
99
+ export class LivenessDetectionError extends Error {
100
+ constructor(message = 'Liveness detection failed') {
101
+ super(message);
102
+ this.name = 'LivenessDetectionError';
151
103
  }
152
104
  }
153
105
 
154
106
  /**
155
- * 参数错误
156
- * 当提供的参数无效时抛出
107
+ * Error codes for Scanner
157
108
  */
158
- export class InvalidArgumentError extends IDScannerError {
159
- constructor(paramName: string, reason: string) {
160
- super(`无效的参数 ${paramName}: ${reason}`, { code: 'INVALID_ARGUMENT' });
161
- this.name = 'InvalidArgumentError';
162
- }
163
- }
109
+ export const ErrorCodes = {
110
+ /** Scanner not initialized */
111
+ NOT_INITIALIZED: 'SCANNER_001',
112
+ /** Module not found */
113
+ MODULE_NOT_FOUND: 'SCANNER_002',
114
+ /** Detection failed */
115
+ DETECTION_FAILED: 'SCANNER_003',
116
+ /** Camera access denied */
117
+ CAMERA_ACCESS_DENIED: 'CAMERA_001',
118
+ /** Camera not available */
119
+ CAMERA_NOT_AVAILABLE: 'CAMERA_002',
120
+ } as const;
164
121
 
165
122
  /**
166
- * 不支持错误
167
- * 当尝试使用不支持的功能或当前环境无法使用的功能时抛出
123
+ * Type for error codes
168
124
  */
169
- export class NotSupportedError extends IDScannerError {
170
- constructor(feature: string) {
171
- super(`不支持的功能: ${feature}`, { code: 'NOT_SUPPORTED' });
172
- this.name = 'NotSupportedError';
173
- }
174
- }
125
+ export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
@@ -10,6 +10,156 @@ import { ResourceLoadError } from './errors';
10
10
  import { EventEmitter } from './event-emitter';
11
11
  import { Result } from './result';
12
12
 
13
+ /**
14
+ * 模型分片信息
15
+ */
16
+ export interface ModelShard {
17
+ name: string; // e.g. 'face-detector'
18
+ version: string; // e.g. '1.0.0'
19
+ url: string; // 完整 URL
20
+ size: number; // bytes
21
+ hash?: string; // 完整性校验
22
+ }
23
+
24
+ /**
25
+ * 模型缓存信息
26
+ */
27
+ export interface ModelCacheInfo {
28
+ name: string;
29
+ version: string;
30
+ cachedAt: number;
31
+ size: number;
32
+ }
33
+
34
+ /**
35
+ * IndexedDB 模型缓存管理器
36
+ * 提供分片模型的持久化缓存功能
37
+ */
38
+ export class ModelCacheManager {
39
+ private _dbName = 'id-scanner-lib-models';
40
+ private _storeName = 'model-cache';
41
+ private _db: IDBDatabase | null = null;
42
+
43
+ /**
44
+ * 初始化缓存管理器(打开 IndexedDB)
45
+ */
46
+ async init(): Promise<void> {
47
+ return new Promise((resolve, reject) => {
48
+ const request = indexedDB.open(this._dbName, 1);
49
+ request.onerror = () => reject(new Error('Failed to open IndexedDB'));
50
+ request.onsuccess = () => {
51
+ this._db = request.result;
52
+ resolve();
53
+ };
54
+ request.onupgradeneeded = (event) => {
55
+ const db = (event.target as IDBOpenDBRequest).result;
56
+ if (!db.objectStoreNames.contains(this._storeName)) {
57
+ const store = db.createObjectStore(this._storeName, { keyPath: 'key' });
58
+ store.createIndex('name', 'name', { unique: false });
59
+ store.createIndex('version', 'version', { unique: false });
60
+ }
61
+ };
62
+ });
63
+ }
64
+
65
+ /**
66
+ * 生成缓存键
67
+ */
68
+ private _makeKey(model: ModelShard): string {
69
+ return `${model.name}@${model.version}`;
70
+ }
71
+
72
+ /**
73
+ * 检查模型是否已缓存
74
+ */
75
+ async has(model: ModelShard): Promise<boolean> {
76
+ if (!this._db) return false;
77
+ return new Promise((resolve, reject) => {
78
+ const tx = this._db!.transaction(this._storeName, 'readonly');
79
+ const store = tx.objectStore(this._storeName);
80
+ const req = store.get(this._makeKey(model));
81
+ req.onsuccess = () => resolve(!!req.result);
82
+ req.onerror = () => reject(req.error);
83
+ });
84
+ }
85
+
86
+ /**
87
+ * 存储模型数据
88
+ */
89
+ async store(model: ModelShard, data: ArrayBuffer): Promise<void> {
90
+ if (!this._db) throw new Error('ModelCacheManager not initialized');
91
+ return new Promise((resolve, reject) => {
92
+ const tx = this._db!.transaction(this._storeName, 'readwrite');
93
+ const store = tx.objectStore(this._storeName);
94
+ const record = {
95
+ key: this._makeKey(model),
96
+ name: model.name,
97
+ version: model.version,
98
+ url: model.url,
99
+ size: model.size,
100
+ data,
101
+ cachedAt: Date.now(),
102
+ };
103
+ const req = store.put(record);
104
+ req.onsuccess = () => resolve();
105
+ req.onerror = () => reject(req.error);
106
+ });
107
+ }
108
+
109
+ /**
110
+ * 加载模型数据
111
+ */
112
+ async load(model: ModelShard): Promise<ArrayBuffer | null> {
113
+ if (!this._db) throw new Error('ModelCacheManager not initialized');
114
+ return new Promise((resolve, reject) => {
115
+ const tx = this._db!.transaction(this._storeName, 'readonly');
116
+ const store = tx.objectStore(this._storeName);
117
+ const req = store.get(this._makeKey(model));
118
+ req.onsuccess = () => resolve(req.result ? req.result.data : null);
119
+ req.onerror = () => reject(req.error);
120
+ });
121
+ }
122
+
123
+ /**
124
+ * 清除所有缓存
125
+ */
126
+ async clear(): Promise<void> {
127
+ if (!this._db) return;
128
+ return new Promise((resolve, reject) => {
129
+ const tx = this._db!.transaction(this._storeName, 'readwrite');
130
+ const store = tx.objectStore(this._storeName);
131
+ const req = store.clear();
132
+ req.onsuccess = () => resolve();
133
+ req.onerror = () => reject(req.error);
134
+ });
135
+ }
136
+
137
+ /**
138
+ * 列出已缓存模型
139
+ */
140
+ async listCached(): Promise<ModelCacheInfo[]> {
141
+ if (!this._db) return [];
142
+ return new Promise((resolve, reject) => {
143
+ const tx = this._db!.transaction(this._storeName, 'readonly');
144
+ const store = tx.objectStore(this._storeName);
145
+ const req = store.getAll();
146
+ req.onsuccess = () => {
147
+ const results: ModelCacheInfo[] = req.result.map((r: any) => ({
148
+ name: r.name,
149
+ version: r.version,
150
+ cachedAt: r.cachedAt,
151
+ size: r.size,
152
+ }));
153
+ resolve(results);
154
+ };
155
+ req.onerror = () => reject(req.error);
156
+ });
157
+ }
158
+ }
159
+
160
+ /** 全局模型缓存管理器实例 */
161
+ export const modelCache = new ModelCacheManager();
162
+
13
163
  /**
14
164
  * 资源类型枚举
15
165
  */
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @file Scanner Core Class
3
+ * @description Main entry point for the Scanner library
4
+ * @module core/scanner
5
+ */
6
+
7
+ import { ScannerConfig, ScannerModule, ImageSource, Face } from './config';
8
+ import { ScannerError, ErrorCodes } from './errors';
9
+
10
+ /**
11
+ * Scanner main class
12
+ * Provides unified interface for all scanning operations
13
+ */
14
+ export class Scanner {
15
+ /**
16
+ * Internal configuration
17
+ */
18
+ private _config: Required<ScannerConfig>;
19
+ /**
20
+ * Loaded modules
21
+ */
22
+ private _modules: Map<string, ScannerModule>;
23
+ /**
24
+ * Initialization state
25
+ */
26
+ private _initialized: boolean = false;
27
+
28
+ /**
29
+ * Create a new Scanner instance
30
+ * @param config Scanner configuration
31
+ */
32
+ constructor(config: ScannerConfig = {}) {
33
+ this._config = this._normalizeConfig(config);
34
+ this._modules = new Map();
35
+ }
36
+
37
+ /**
38
+ * Check if scanner is initialized
39
+ */
40
+ get initialized(): boolean {
41
+ return this._initialized;
42
+ }
43
+
44
+ /**
45
+ * Normalize configuration with defaults
46
+ * @param config User provided config
47
+ * @returns Normalized config with defaults
48
+ */
49
+ private _normalizeConfig(config: ScannerConfig): Required<ScannerConfig> {
50
+ return {
51
+ debug: config.debug ?? false,
52
+ modules: {
53
+ face: config.modules?.face ?? true,
54
+ faceComparator: config.modules?.faceComparator ?? false,
55
+ faceLiveness: config.modules?.faceLiveness ?? false,
56
+ idCard: config.modules?.idCard ?? false,
57
+ qr: config.modules?.qr ?? false,
58
+ },
59
+ performance: {
60
+ maxCanvasWidth: config.performance?.maxCanvasWidth ?? 1280,
61
+ useWorker: config.performance?.useWorker ?? true,
62
+ lazyLoad: config.performance?.lazyLoad ?? true,
63
+ },
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Initialize the scanner and load enabled modules
69
+ */
70
+ async initialize(): Promise<void> {
71
+ // Immediately load face core module
72
+ const { FaceDetector } = await import('../modules/face/detector');
73
+ const detector = new FaceDetector();
74
+ await detector.initialize();
75
+ this._modules.set('face-detector', detector);
76
+ this._initialized = true;
77
+ }
78
+
79
+ /**
80
+ * Detect faces in an image source
81
+ * @param input Image source (video, canvas, image, etc.)
82
+ * @returns Array of detected faces
83
+ */
84
+ async detectFace(input: ImageSource): Promise<Face[]> {
85
+ const detector = this._modules.get('face-detector');
86
+ if (!detector) {
87
+ throw new ScannerError(
88
+ 'Scanner not initialized. Call initialize() first.',
89
+ ErrorCodes.NOT_INITIALIZED
90
+ );
91
+ }
92
+ return (detector as any).detect(input);
93
+ }
94
+
95
+ /**
96
+ * Destroy the scanner and release all resources
97
+ */
98
+ async destroy(): Promise<void> {
99
+ for (const module of this._modules.values()) {
100
+ await module.destroy();
101
+ }
102
+ this._modules.clear();
103
+ this._initialized = false;
104
+ }
105
+ }
106
+
107
+ // Re-export types
108
+ export { ScannerConfig, ScannerModule, ImageSource, Face } from './config';
109
+ export { ScannerError, ErrorCodes } from './errors';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @file Browser Utilities
3
+ * @description Browser environment utility functions
4
+ * @module core/utils/browser
5
+ */
6
+
7
+ export {};
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @file Canvas 对象池
3
+ * @description 提供 OffscreenCanvas/HTMLCanvasElement 的复用机制,减少内存分配和 GC 压力
4
+ * @module core/utils/canvas-pool
5
+ */
6
+
7
+ /**
8
+ * Canvas 池条目
9
+ */
10
+ export interface PooledCanvas {
11
+ canvas: OffscreenCanvas | HTMLCanvasElement;
12
+ width: number;
13
+ height: number;
14
+ inUse: boolean;
15
+ }
16
+
17
+ interface PoolEntry {
18
+ canvas: OffscreenCanvas | HTMLCanvasElement;
19
+ width: number;
20
+ height: number;
21
+ inUse: boolean;
22
+ lastUsed: number;
23
+ }
24
+
25
+ /**
26
+ * Canvas 对象池
27
+ *
28
+ * 复用 Canvas 元素,避免频繁创建和销毁导致的内存抖动
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const pool = new CanvasPool();
33
+ * const acquired = pool.acquire(640, 480);
34
+ * // 使用 canvas 进行绘制...
35
+ * pool.release(acquired);
36
+ * ```
37
+ */
38
+ export class CanvasPool {
39
+ private _pool: PoolEntry[] = [];
40
+ private _maxSize: number = 4;
41
+ private _useOffscreen: boolean;
42
+
43
+ /**
44
+ * 创建 Canvas 池
45
+ * @param maxSize 最大池大小
46
+ * @param useOffscreen 是否使用 OffscreenCanvas
47
+ */
48
+ constructor(maxSize: number = 4, useOffscreen: boolean = true) {
49
+ this._maxSize = maxSize;
50
+ this._useOffscreen = useOffscreen && typeof OffscreenCanvas !== 'undefined';
51
+ }
52
+
53
+ /**
54
+ * 从池中获取 Canvas
55
+ *
56
+ * 1. 查找尺寸匹配的可用 canvas(允许 10% 误差)
57
+ * 2. 无匹配则创建新的
58
+ * 3. 超出 _maxSize 则回收最旧的 inUse=false 的
59
+ * 4. 返回 PooledCanvas,标记 inUse=true
60
+ *
61
+ * @param width 宽度
62
+ * @param height 高度
63
+ */
64
+ acquire(width: number, height: number): PooledCanvas {
65
+ // 1. 查找尺寸匹配的可用 canvas(允许 10% 误差)
66
+ const matched = this._pool.find(p =>
67
+ !p.inUse &&
68
+ Math.abs(p.width - width) / width <= 0.1 &&
69
+ Math.abs(p.height - height) / height <= 0.1
70
+ );
71
+
72
+ if (matched) {
73
+ matched.inUse = true;
74
+ matched.lastUsed = Date.now();
75
+ return {
76
+ canvas: matched.canvas,
77
+ width: matched.width,
78
+ height: matched.height,
79
+ inUse: true,
80
+ };
81
+ }
82
+
83
+ // 2. 无匹配则创建新的
84
+ let canvas: OffscreenCanvas | HTMLCanvasElement;
85
+ if (this._useOffscreen) {
86
+ canvas = new OffscreenCanvas(width, height);
87
+ } else {
88
+ // 在非 OffscreenCanvas 环境(如 Node.js 测试)回退到 HTMLCanvasElement
89
+ if (typeof document !== 'undefined') {
90
+ canvas = document.createElement('canvas');
91
+ canvas.width = width;
92
+ canvas.height = height;
93
+ } else {
94
+ // 无法创建 canvas
95
+ throw new Error('CanvasPool: 无法创建 canvas (OffscreenCanvas 不可用,且 document 不存在)');
96
+ }
97
+ }
98
+
99
+ const entry: PoolEntry = {
100
+ canvas,
101
+ width,
102
+ height,
103
+ inUse: true,
104
+ lastUsed: Date.now(),
105
+ };
106
+
107
+ // 3. 超出 _maxSize 则回收最旧的 inUse=false 的
108
+ if (this._pool.length >= this._maxSize) {
109
+ const oldestIdx = this._findOldestAvailableIndex();
110
+ if (oldestIdx !== -1) {
111
+ this._pool.splice(oldestIdx, 1);
112
+ }
113
+ }
114
+
115
+ this._pool.push(entry);
116
+ return {
117
+ canvas: entry.canvas,
118
+ width: entry.width,
119
+ height: entry.height,
120
+ inUse: true,
121
+ };
122
+ }
123
+
124
+ /**
125
+ * 归还 Canvas 到池中
126
+ * @param pooled 要归还的 PooledCanvas
127
+ */
128
+ release(pooled: PooledCanvas): void {
129
+ const entry = this._pool.find(p => p.canvas === pooled.canvas);
130
+ if (entry) {
131
+ entry.inUse = false;
132
+ entry.lastUsed = Date.now();
133
+ }
134
+ }
135
+
136
+ /**
137
+ * 清空池
138
+ */
139
+ clear(): void {
140
+ this._pool = [];
141
+ }
142
+
143
+ /**
144
+ * 获取池统计信息
145
+ */
146
+ getStats(): { total: number; inUse: number; available: number } {
147
+ return {
148
+ total: this._pool.length,
149
+ inUse: this._pool.filter(p => p.inUse).length,
150
+ available: this._pool.filter(p => !p.inUse).length,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * 查找最旧的可用条目索引
156
+ */
157
+ private _findOldestAvailableIndex(): number {
158
+ let oldestIdx = -1;
159
+ let oldestTime = Infinity;
160
+ for (let i = 0; i < this._pool.length; i++) {
161
+ if (!this._pool[i].inUse && this._pool[i].lastUsed < oldestTime) {
162
+ oldestTime = this._pool[i].lastUsed;
163
+ oldestIdx = i;
164
+ }
165
+ }
166
+ return oldestIdx;
167
+ }
168
+ }
169
+
170
+ /** 全局共享的 Canvas 池实例 */
171
+ export const globalCanvasPool = new CanvasPool();
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @file Canvas Utilities
3
+ * @description Canvas-related utility functions
4
+ * @module core/utils/canvas
5
+ */
6
+
7
+ export {};