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.
- package/dist/id-scanner-lib.esm.js +108 -315
- package/dist/id-scanner-lib.esm.js.map +1 -1
- package/dist/id-scanner-lib.js +109 -320
- package/dist/id-scanner-lib.js.map +1 -1
- package/package.json +1 -1
- package/src/compat/index.ts +7 -0
- package/src/compat/v1-adapter.ts +84 -0
- package/src/core/config.ts +113 -267
- package/src/core/errors.ts +68 -117
- package/src/core/resource-manager.ts +150 -0
- package/src/core/scanner.ts +109 -0
- package/src/core/utils/browser.ts +7 -0
- package/src/core/utils/canvas-pool.ts +171 -0
- package/src/core/utils/canvas.ts +7 -0
- package/src/core/utils/image.ts +7 -0
- package/src/core/utils/index.ts +9 -0
- package/src/core/utils/resource-manager.ts +155 -0
- package/src/core/utils/validate.ts +7 -0
- package/src/core/utils/worker.ts +130 -0
- package/src/modules/face/comparator/comparator.ts +45 -0
- package/src/modules/face/comparator/index.ts +1 -0
- package/src/modules/face/detector/detector.ts +83 -0
- package/src/modules/face/detector/index.ts +2 -0
- package/src/modules/face/detector/types.ts +80 -0
- package/src/modules/face/liveness/index.ts +7 -0
- package/src/modules/face/tracker/index.ts +7 -0
- package/src/modules/id-card/anti-fake/index.ts +7 -0
- package/src/modules/id-card/detector/index.ts +7 -0
- package/src/modules/id-card/parser/index.ts +7 -0
- package/src/modules/qr/scanner/index.ts +7 -0
package/src/core/errors.ts
CHANGED
|
@@ -1,174 +1,125 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file
|
|
3
|
-
* @description
|
|
2
|
+
* @file Scanner Errors
|
|
3
|
+
* @description Error classes for Scanner
|
|
4
4
|
* @module core/errors
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* 所有库特定错误的基类
|
|
8
|
+
* Scanner specific error
|
|
10
9
|
*/
|
|
11
|
-
export class
|
|
12
|
-
/**
|
|
10
|
+
export class ScannerError extends Error {
|
|
11
|
+
/**
|
|
12
|
+
* Error code
|
|
13
|
+
*/
|
|
13
14
|
public code: string;
|
|
14
|
-
/**
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Module that threw the error
|
|
17
|
+
*/
|
|
18
|
+
public module?: string;
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
19
|
-
* @param message
|
|
20
|
-
* @param
|
|
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,
|
|
26
|
+
constructor(message: string, code: string, module?: string) {
|
|
23
27
|
super(message);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
this.
|
|
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
|
|
46
|
-
constructor(message
|
|
47
|
-
super(
|
|
48
|
-
this.name = '
|
|
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
|
|
57
|
-
constructor(message
|
|
58
|
-
super(
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
82
|
-
constructor(message
|
|
83
|
-
super(
|
|
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
|
|
93
|
-
constructor(message
|
|
94
|
-
super(
|
|
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
|
|
115
|
-
constructor(message
|
|
116
|
-
super(
|
|
117
|
-
this.name = '
|
|
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
|
|
148
|
-
constructor(
|
|
149
|
-
super(
|
|
150
|
-
this.name = '
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
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,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();
|