id-scanner-lib 1.3.3 → 1.6.2
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/README.md +324 -410
- package/dist/id-scanner-lib.esm.js +4826 -0
- package/dist/id-scanner-lib.esm.js.map +1 -0
- package/dist/id-scanner-lib.js +4858 -0
- package/dist/id-scanner-lib.js.map +1 -0
- package/dist/types/browser-image-compression.d.ts +19 -0
- package/dist/types/tesseract.d.ts +280 -0
- package/package.json +89 -78
- package/src/core/base-module.ts +78 -0
- package/src/core/camera-manager.ts +813 -0
- package/src/core/config.ts +305 -0
- package/src/core/errors.ts +174 -0
- package/src/core/event-emitter.test.ts +42 -0
- package/src/core/event-emitter.ts +110 -0
- package/src/core/loading-state.test.ts +67 -0
- package/src/core/loading-state.ts +156 -0
- package/src/core/logger.test.ts +49 -0
- package/src/core/logger.ts +549 -0
- package/src/core/module-manager.ts +163 -0
- package/src/core/plugin-manager.ts +429 -0
- package/src/core/resource-manager.ts +762 -0
- package/src/core/result.ts +163 -0
- package/src/core/scanner-factory.ts +236 -0
- package/src/index.ts +117 -939
- package/src/interfaces/external-types.ts +200 -0
- package/src/interfaces/face-detection.ts +309 -0
- package/src/interfaces/scanner-module.ts +384 -0
- package/src/modules/face/face-detector.ts +988 -0
- package/src/modules/face/index.ts +208 -0
- package/src/modules/face/liveness-detector.ts +908 -0
- package/src/modules/face/types.ts +133 -0
- package/src/{id-recognition → modules/id-card}/anti-fake-detector.ts +274 -240
- package/src/modules/id-card/id-card-detector.ts +474 -0
- package/src/modules/id-card/index.ts +425 -0
- package/src/{id-recognition → modules/id-card}/ocr-processor.ts +149 -92
- package/src/modules/id-card/ocr-worker.ts +259 -0
- package/src/modules/id-card/types.ts +178 -0
- package/src/modules/qrcode/index.ts +175 -0
- package/src/modules/qrcode/qr-code-scanner.ts +231 -0
- package/src/modules/qrcode/types.ts +169 -0
- package/src/types/common.test.ts +99 -0
- package/src/types/common.ts +166 -0
- package/src/types/tesseract.d.ts +265 -22
- package/src/utils/camera.test.ts +30 -0
- package/src/utils/camera.ts +4 -1
- package/src/utils/error-handler.test.ts +137 -0
- package/src/utils/error-handler.ts +110 -0
- package/src/utils/image-processing.ts +68 -49
- package/src/utils/index.test.ts +186 -0
- package/src/utils/index.ts +429 -0
- package/src/utils/performance.ts +168 -131
- package/src/utils/resource-manager.ts +65 -146
- package/src/utils/retry.test.ts +142 -0
- package/src/utils/retry.ts +282 -0
- package/src/utils/types.ts +90 -2
- package/src/utils/utils.test.ts +171 -0
- package/src/utils/worker.ts +123 -84
- package/src/version.ts +11 -0
- package/tools/scaffold.js +543 -0
- package/dist/id-scanner-core.esm.js +0 -11349
- package/dist/id-scanner-core.js +0 -11361
- package/dist/id-scanner-core.min.js +0 -1
- package/dist/id-scanner-ocr.esm.js +0 -2319
- package/dist/id-scanner-ocr.js +0 -2328
- package/dist/id-scanner-ocr.min.js +0 -1
- package/dist/id-scanner-qr.esm.js +0 -1296
- package/dist/id-scanner-qr.js +0 -1305
- package/dist/id-scanner-qr.min.js +0 -1
- package/dist/id-scanner.js +0 -4561
- package/dist/id-scanner.min.js +0 -1
- package/src/core.ts +0 -138
- package/src/demo/demo.ts +0 -204
- package/src/id-recognition/data-extractor.ts +0 -262
- package/src/id-recognition/id-detector.ts +0 -510
- package/src/id-recognition/ocr-worker.ts +0 -156
- package/src/index-umd.ts +0 -477
- package/src/ocr-module.ts +0 -187
- package/src/qr-module.ts +0 -179
- package/src/scanner/barcode-scanner.ts +0 -251
- package/src/scanner/qr-scanner.ts +0 -167
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 重试工具测试
|
|
3
|
+
* @description 测试重试功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { withRetry, AsyncCache, Semaphore } from './retry';
|
|
7
|
+
|
|
8
|
+
describe('withRetry', () => {
|
|
9
|
+
it('should retry on failure and succeed', async () => {
|
|
10
|
+
let attempts = 0;
|
|
11
|
+
const fn = async () => {
|
|
12
|
+
attempts++;
|
|
13
|
+
if (attempts < 3) throw new Error('fail');
|
|
14
|
+
return 'success';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const result = await withRetry(fn, {
|
|
18
|
+
maxAttempts: 3,
|
|
19
|
+
initialDelay: 10
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(result).toBe('success');
|
|
23
|
+
expect(attempts).toBe(3);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should fail after max attempts', async () => {
|
|
27
|
+
const fn = async () => {
|
|
28
|
+
throw new Error('always fails');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await expect(
|
|
32
|
+
withRetry(fn, { maxAttempts: 2, initialDelay: 10 })
|
|
33
|
+
).rejects.toThrow('always fails');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should not retry if shouldRetry returns false', async () => {
|
|
37
|
+
let attempts = 0;
|
|
38
|
+
const fn = async () => {
|
|
39
|
+
attempts++;
|
|
40
|
+
throw new Error('test');
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
await expect(
|
|
44
|
+
withRetry(fn, {
|
|
45
|
+
maxAttempts: 3,
|
|
46
|
+
initialDelay: 10,
|
|
47
|
+
shouldRetry: () => false
|
|
48
|
+
})
|
|
49
|
+
).rejects.toThrow();
|
|
50
|
+
|
|
51
|
+
expect(attempts).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('AsyncCache', () => {
|
|
56
|
+
it('should store and retrieve values', async () => {
|
|
57
|
+
const cache = new AsyncCache<string>(1000);
|
|
58
|
+
cache.set('key1', 'value1');
|
|
59
|
+
|
|
60
|
+
expect(cache.get('key1')).toBe('value1');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return undefined for expired cache', async () => {
|
|
64
|
+
const cache = new AsyncCache<string>(50);
|
|
65
|
+
cache.set('key1', 'value1');
|
|
66
|
+
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, 60));
|
|
68
|
+
|
|
69
|
+
expect(cache.get('key1')).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should getOrSet value if not exists', async () => {
|
|
73
|
+
const cache = new AsyncCache<string>(1000);
|
|
74
|
+
|
|
75
|
+
const result = await cache.getOrSet('key1', async () => 'computed');
|
|
76
|
+
|
|
77
|
+
expect(result).toBe('computed');
|
|
78
|
+
expect(cache.get('key1')).toBe('computed');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should not recompute if exists', async () => {
|
|
82
|
+
const cache = new AsyncCache<string>(1000);
|
|
83
|
+
let computeCount = 0;
|
|
84
|
+
|
|
85
|
+
const compute = async () => {
|
|
86
|
+
computeCount++;
|
|
87
|
+
return 'value';
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
await cache.getOrSet('key1', compute);
|
|
91
|
+
await cache.getOrSet('key1', compute);
|
|
92
|
+
|
|
93
|
+
expect(computeCount).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should clear all cache', async () => {
|
|
97
|
+
const cache = new AsyncCache<string>(1000);
|
|
98
|
+
cache.set('key1', 'value1');
|
|
99
|
+
cache.set('key2', 'value2');
|
|
100
|
+
|
|
101
|
+
expect(cache.size).toBe(2);
|
|
102
|
+
|
|
103
|
+
cache.clear();
|
|
104
|
+
expect(cache.size).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('Semaphore', () => {
|
|
109
|
+
it('should acquire and release permits', async () => {
|
|
110
|
+
const sem = new Semaphore(2);
|
|
111
|
+
|
|
112
|
+
expect(sem.availablePermits).toBe(2);
|
|
113
|
+
|
|
114
|
+
await sem.acquire();
|
|
115
|
+
expect(sem.availablePermits).toBe(1);
|
|
116
|
+
|
|
117
|
+
await sem.acquire();
|
|
118
|
+
expect(sem.availablePermits).toBe(0);
|
|
119
|
+
|
|
120
|
+
sem.release();
|
|
121
|
+
expect(sem.availablePermits).toBe(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should tryAcquire returns false when no permits', () => {
|
|
125
|
+
const sem = new Semaphore(1);
|
|
126
|
+
sem.acquire();
|
|
127
|
+
|
|
128
|
+
expect(sem.tryAcquire()).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should queue waiting acquires', async () => {
|
|
132
|
+
const sem = new Semaphore(1);
|
|
133
|
+
|
|
134
|
+
// First acquire
|
|
135
|
+
await sem.acquire();
|
|
136
|
+
expect(sem.availablePermits).toBe(0);
|
|
137
|
+
|
|
138
|
+
// Release
|
|
139
|
+
sem.release();
|
|
140
|
+
expect(sem.availablePermits).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 重试工具
|
|
3
|
+
* @description 提供重试逻辑功能
|
|
4
|
+
* @module utils/retry
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 重试选项
|
|
9
|
+
*/
|
|
10
|
+
export interface RetryOptions {
|
|
11
|
+
/** 最大重试次数 */
|
|
12
|
+
maxAttempts?: number;
|
|
13
|
+
/** 初始等待时间(ms) */
|
|
14
|
+
initialDelay?: number;
|
|
15
|
+
/** 最大等待时间(ms) */
|
|
16
|
+
maxDelay?: number;
|
|
17
|
+
/** 指数退避因子 */
|
|
18
|
+
backoffFactor?: number;
|
|
19
|
+
/** 是否随机抖动 */
|
|
20
|
+
jitter?: boolean;
|
|
21
|
+
/** 重试条件 */
|
|
22
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 默认重试选项
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_OPTIONS: Required<RetryOptions> = {
|
|
29
|
+
maxAttempts: 3,
|
|
30
|
+
initialDelay: 1000,
|
|
31
|
+
maxDelay: 10000,
|
|
32
|
+
backoffFactor: 2,
|
|
33
|
+
jitter: true,
|
|
34
|
+
shouldRetry: () => true
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 带重试的异步函数包装器
|
|
39
|
+
* @param fn 要执行的异步函数
|
|
40
|
+
* @param options 重试选项
|
|
41
|
+
* @returns 函数结果
|
|
42
|
+
*/
|
|
43
|
+
export async function withRetry<T>(
|
|
44
|
+
fn: () => Promise<T>,
|
|
45
|
+
options: RetryOptions = {}
|
|
46
|
+
): Promise<T> {
|
|
47
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
48
|
+
let lastError: unknown;
|
|
49
|
+
let delay = opts.initialDelay;
|
|
50
|
+
|
|
51
|
+
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
return await fn();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
lastError = error;
|
|
56
|
+
|
|
57
|
+
// 检查是否应该重试
|
|
58
|
+
if (!opts.shouldRetry(error)) {
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 如果不是最后一次尝试,等待后重试
|
|
63
|
+
if (attempt < opts.maxAttempts) {
|
|
64
|
+
// 添加随机抖动
|
|
65
|
+
const jitterAmount = opts.jitter ? Math.random() * delay : 0;
|
|
66
|
+
const waitTime = Math.min(delay + jitterAmount, opts.maxDelay);
|
|
67
|
+
|
|
68
|
+
await sleep(waitTime);
|
|
69
|
+
|
|
70
|
+
// 指数退避
|
|
71
|
+
delay = Math.min(delay * opts.backoffFactor, opts.maxDelay);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw lastError;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 睡眠函数
|
|
81
|
+
* @param ms 毫秒数
|
|
82
|
+
*/
|
|
83
|
+
function sleep(ms: number): Promise<void> {
|
|
84
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 创建指数退避重试函数
|
|
89
|
+
* @param options 重试选项
|
|
90
|
+
* @returns 包装后的重试函数
|
|
91
|
+
*/
|
|
92
|
+
export function createRetryable<T extends (...args: any[]) => Promise<any>>(
|
|
93
|
+
fn: T,
|
|
94
|
+
options: RetryOptions = {}
|
|
95
|
+
): T {
|
|
96
|
+
const wrapped = (...args: Parameters<T>): Promise<ReturnType<T>> => {
|
|
97
|
+
return withRetry(() => fn(...args), options);
|
|
98
|
+
};
|
|
99
|
+
Object.defineProperty(wrapped, 'name', { value: fn.name, configurable: true });
|
|
100
|
+
return wrapped as T;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 异步缓存
|
|
105
|
+
* @description 用于缓存异步函数的结果
|
|
106
|
+
*/
|
|
107
|
+
export class AsyncCache<T> {
|
|
108
|
+
private cache: Map<string, { value: T; expiry: number }> = new Map();
|
|
109
|
+
private defaultTTL: number;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param defaultTTL 默认过期时间(毫秒)
|
|
113
|
+
*/
|
|
114
|
+
constructor(defaultTTL: number = 5 * 60 * 1000) {
|
|
115
|
+
this.defaultTTL = defaultTTL;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 获取缓存值
|
|
120
|
+
* @param key 缓存键
|
|
121
|
+
* @returns 缓存值,如果不存在或已过期则返回undefined
|
|
122
|
+
*/
|
|
123
|
+
get(key: string): T | undefined {
|
|
124
|
+
const entry = this.cache.get(key);
|
|
125
|
+
if (!entry) return undefined;
|
|
126
|
+
|
|
127
|
+
if (Date.now() > entry.expiry) {
|
|
128
|
+
this.cache.delete(key);
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return entry.value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 设置缓存值
|
|
137
|
+
* @param key 缓存键
|
|
138
|
+
* @param value 缓存值
|
|
139
|
+
* @param ttl 过期时间(毫秒)
|
|
140
|
+
*/
|
|
141
|
+
set(key: string, value: T, ttl?: number): void {
|
|
142
|
+
this.cache.set(key, {
|
|
143
|
+
value,
|
|
144
|
+
expiry: Date.now() + (ttl || this.defaultTTL)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 删除缓存值
|
|
150
|
+
* @param key 缓存键
|
|
151
|
+
*/
|
|
152
|
+
delete(key: string): void {
|
|
153
|
+
this.cache.delete(key);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 清空缓存
|
|
158
|
+
*/
|
|
159
|
+
clear(): void {
|
|
160
|
+
this.cache.clear();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 获取缓存大小
|
|
165
|
+
*/
|
|
166
|
+
get size(): number {
|
|
167
|
+
// 清理过期项 (避免在迭代中删除)
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
const expiredKeys: string[] = [];
|
|
170
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
171
|
+
if (now > entry.expiry) {
|
|
172
|
+
expiredKeys.push(key);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
expiredKeys.forEach(key => this.cache.delete(key));
|
|
176
|
+
return this.cache.size;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 异步获取或设置缓存
|
|
181
|
+
* @param key 缓存键
|
|
182
|
+
* @param fn 获取值的函数
|
|
183
|
+
* @param ttl 过期时间
|
|
184
|
+
* @returns 缓存值
|
|
185
|
+
*/
|
|
186
|
+
async getOrSet(
|
|
187
|
+
key: string,
|
|
188
|
+
fn: () => Promise<T>,
|
|
189
|
+
ttl?: number
|
|
190
|
+
): Promise<T> {
|
|
191
|
+
const cached = this.get(key);
|
|
192
|
+
if (cached !== undefined) {
|
|
193
|
+
return cached;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const value = await fn();
|
|
197
|
+
this.set(key, value, ttl);
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 简单信号量
|
|
204
|
+
* @description 用于控制并发数量
|
|
205
|
+
*/
|
|
206
|
+
export class Semaphore {
|
|
207
|
+
private permits: number;
|
|
208
|
+
private queue: Array<() => void> = [];
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param permits 最大许可数
|
|
212
|
+
*/
|
|
213
|
+
constructor(permits: number) {
|
|
214
|
+
this.permits = permits;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 获取许可
|
|
219
|
+
* @returns Promise
|
|
220
|
+
*/
|
|
221
|
+
async acquire(): Promise<void> {
|
|
222
|
+
if (this.permits > 0) {
|
|
223
|
+
this.permits--;
|
|
224
|
+
return Promise.resolve();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return new Promise<void>(resolve => {
|
|
228
|
+
this.queue.push(resolve);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 释放许可
|
|
234
|
+
*/
|
|
235
|
+
release(): void {
|
|
236
|
+
this.permits++;
|
|
237
|
+
const next = this.queue.shift();
|
|
238
|
+
if (next) {
|
|
239
|
+
this.permits--;
|
|
240
|
+
next();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 尝试获取许可
|
|
246
|
+
* @returns 是否获取成功
|
|
247
|
+
*/
|
|
248
|
+
tryAcquire(): boolean {
|
|
249
|
+
if (this.permits > 0) {
|
|
250
|
+
this.permits--;
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 获取当前可用许可数
|
|
258
|
+
*/
|
|
259
|
+
get availablePermits(): number {
|
|
260
|
+
return this.permits;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 带信号量的异步函数包装器
|
|
266
|
+
* @param fn 异步函数
|
|
267
|
+
* @param semaphore 信号量
|
|
268
|
+
* @returns 包装后的函数
|
|
269
|
+
*/
|
|
270
|
+
export function withSemaphore<T extends (...args: any[]) => Promise<any>>(
|
|
271
|
+
fn: T,
|
|
272
|
+
semaphore: Semaphore
|
|
273
|
+
): T {
|
|
274
|
+
return (async (...args: Parameters<T>): Promise<ReturnType<T>> => {
|
|
275
|
+
await semaphore.acquire();
|
|
276
|
+
try {
|
|
277
|
+
return await fn(...args);
|
|
278
|
+
} finally {
|
|
279
|
+
semaphore.release();
|
|
280
|
+
}
|
|
281
|
+
}) as T;
|
|
282
|
+
}
|
package/src/utils/types.ts
CHANGED
|
@@ -70,12 +70,100 @@ export interface DetectionResult {
|
|
|
70
70
|
* ```
|
|
71
71
|
*/
|
|
72
72
|
export interface IDCardInfo {
|
|
73
|
+
/** 姓名 */
|
|
73
74
|
name?: string;
|
|
75
|
+
/** 性别 */
|
|
74
76
|
gender?: string;
|
|
75
|
-
|
|
77
|
+
/** 民族 */
|
|
78
|
+
ethnicity?: string;
|
|
79
|
+
/** 出生日期 */
|
|
76
80
|
birthDate?: string;
|
|
81
|
+
/** 地址 */
|
|
77
82
|
address?: string;
|
|
83
|
+
/** 身份证号码 */
|
|
78
84
|
idNumber?: string;
|
|
79
|
-
|
|
85
|
+
/** 签发机关 */
|
|
86
|
+
issueAuthority?: string;
|
|
87
|
+
/** 有效期起始日期 */
|
|
88
|
+
validFrom?: string;
|
|
89
|
+
/** 有效期截止日期 */
|
|
90
|
+
validTo?: string;
|
|
91
|
+
/** 有效期限(完整文本) */
|
|
80
92
|
validPeriod?: string;
|
|
93
|
+
/** 照片区域 */
|
|
94
|
+
photoRegion?: {
|
|
95
|
+
x: number;
|
|
96
|
+
y: number;
|
|
97
|
+
width: number;
|
|
98
|
+
height: number;
|
|
99
|
+
};
|
|
100
|
+
/** 类型 */
|
|
101
|
+
type?: string;
|
|
102
|
+
/** 置信度 */
|
|
103
|
+
confidence?: number;
|
|
104
|
+
/** 其他属性 */
|
|
105
|
+
[key: string]: any;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 点坐标
|
|
110
|
+
*/
|
|
111
|
+
export interface Point {
|
|
112
|
+
x: number;
|
|
113
|
+
y: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 矩形区域
|
|
118
|
+
*/
|
|
119
|
+
export interface Rect {
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
width: number;
|
|
123
|
+
height: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 可释放资源接口
|
|
128
|
+
*/
|
|
129
|
+
export interface Disposable {
|
|
130
|
+
/** 释放资源 */
|
|
131
|
+
dispose(): Promise<void>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 图像处理选项
|
|
136
|
+
*/
|
|
137
|
+
export interface ImageProcessingOptions {
|
|
138
|
+
/** 亮度调整 (-100 到 100) */
|
|
139
|
+
brightness?: number;
|
|
140
|
+
/** 对比度调整 (-100 到 100) */
|
|
141
|
+
contrast?: number;
|
|
142
|
+
/** 饱和度调整 (-100 到 100) */
|
|
143
|
+
saturation?: number;
|
|
144
|
+
/** 锐化强度 (0 到 10) */
|
|
145
|
+
sharpen?: number | boolean;
|
|
146
|
+
/** 高斯模糊半径 (0 到 10) */
|
|
147
|
+
blur?: number;
|
|
148
|
+
/** 是否应用灰度转换 */
|
|
149
|
+
grayscale?: boolean;
|
|
150
|
+
/** 是否应用二值化 */
|
|
151
|
+
binarize?: boolean;
|
|
152
|
+
/** 二值化阈值 (0 到 255) */
|
|
153
|
+
threshold?: number;
|
|
154
|
+
/** 是否应用边缘检测 */
|
|
155
|
+
edgeDetection?: boolean;
|
|
156
|
+
/** 是否应用降噪 */
|
|
157
|
+
denoise?: boolean;
|
|
158
|
+
/** 是否应用直方图均衡化 */
|
|
159
|
+
histogramEqualization?: boolean;
|
|
160
|
+
/** 是否应用透视校正 */
|
|
161
|
+
perspectiveCorrection?: boolean;
|
|
162
|
+
/** 透视校正点 */
|
|
163
|
+
perspectivePoints?: {
|
|
164
|
+
topLeft: Point;
|
|
165
|
+
topRight: Point;
|
|
166
|
+
bottomRight: Point;
|
|
167
|
+
bottomLeft: Point;
|
|
168
|
+
};
|
|
81
169
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 工具函数测试
|
|
3
|
+
* @description 测试通用工具函数
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
delay,
|
|
8
|
+
throttle,
|
|
9
|
+
debounce,
|
|
10
|
+
formatBytes,
|
|
11
|
+
generateUUID,
|
|
12
|
+
chunk,
|
|
13
|
+
safeParseJSON,
|
|
14
|
+
clamp,
|
|
15
|
+
isValidUrl,
|
|
16
|
+
} from '../utils';
|
|
17
|
+
|
|
18
|
+
describe('Utils', () => {
|
|
19
|
+
describe('delay', () => {
|
|
20
|
+
it('should delay for specified milliseconds', async () => {
|
|
21
|
+
const start = Date.now();
|
|
22
|
+
await delay(100);
|
|
23
|
+
const elapsed = Date.now() - start;
|
|
24
|
+
expect(elapsed).toBeGreaterThanOrEqual(90);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('throttle', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.useFakeTimers();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
jest.useRealTimers();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should throttle function calls', () => {
|
|
38
|
+
const fn = jest.fn();
|
|
39
|
+
const throttled = throttle(fn, 100);
|
|
40
|
+
|
|
41
|
+
throttled();
|
|
42
|
+
throttled();
|
|
43
|
+
throttled();
|
|
44
|
+
|
|
45
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should allow calls after throttle period', () => {
|
|
49
|
+
const fn = jest.fn();
|
|
50
|
+
const throttled = throttle(fn, 100);
|
|
51
|
+
|
|
52
|
+
throttled();
|
|
53
|
+
jest.advanceTimersByTime(100);
|
|
54
|
+
throttled();
|
|
55
|
+
|
|
56
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('debounce', () => {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
jest.useFakeTimers();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
jest.useRealTimers();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should debounce function calls', () => {
|
|
70
|
+
const fn = jest.fn();
|
|
71
|
+
const debounced = debounce(fn, 100);
|
|
72
|
+
|
|
73
|
+
debounced();
|
|
74
|
+
debounced();
|
|
75
|
+
debounced();
|
|
76
|
+
|
|
77
|
+
jest.advanceTimersByTime(100);
|
|
78
|
+
|
|
79
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should execute immediately when immediate is true', () => {
|
|
83
|
+
const fn = jest.fn();
|
|
84
|
+
const debounced = debounce(fn, 100, true);
|
|
85
|
+
|
|
86
|
+
debounced();
|
|
87
|
+
debounced();
|
|
88
|
+
debounced();
|
|
89
|
+
|
|
90
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('formatBytes', () => {
|
|
95
|
+
it('should format bytes correctly', () => {
|
|
96
|
+
expect(formatBytes(0)).toBe('0 Bytes');
|
|
97
|
+
expect(formatBytes(1024)).toBe('1 KB');
|
|
98
|
+
expect(formatBytes(1024 * 1024)).toBe('1 MB');
|
|
99
|
+
expect(formatBytes(1024 * 1024 * 1024)).toBe('1 GB');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should respect decimal places', () => {
|
|
103
|
+
expect(formatBytes(1536, 2)).toBe('1.5 KB');
|
|
104
|
+
expect(formatBytes(1536, 0)).toBe('2 KB');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('generateUUID', () => {
|
|
109
|
+
it('should generate valid UUID format', () => {
|
|
110
|
+
const uuid = generateUUID();
|
|
111
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
112
|
+
expect(uuid).toMatch(uuidRegex);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should generate unique UUIDs', () => {
|
|
116
|
+
const uuids = new Set(Array.from({ length: 100 }, () => generateUUID()));
|
|
117
|
+
expect(uuids.size).toBe(100);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('chunk', () => {
|
|
122
|
+
it('should split array into chunks', () => {
|
|
123
|
+
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
124
|
+
const chunks = chunk(arr, 3);
|
|
125
|
+
expect(chunks).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle last chunk smaller than chunk size', () => {
|
|
129
|
+
const arr = [1, 2, 3, 4, 5];
|
|
130
|
+
const chunks = chunk(arr, 2);
|
|
131
|
+
expect(chunks).toEqual([[1, 2], [3, 4], [5]]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should throw error for invalid chunk size', () => {
|
|
135
|
+
expect(() => chunk([1, 2, 3], 0)).toThrow('Chunk size must be greater than 0');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('safeParseJSON', () => {
|
|
140
|
+
it('should parse valid JSON', () => {
|
|
141
|
+
const result = safeParseJSON('{"a": 1}', null);
|
|
142
|
+
expect(result).toEqual({ a: 1 });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should return fallback for invalid JSON', () => {
|
|
146
|
+
const result = safeParseJSON('invalid', { fallback: true });
|
|
147
|
+
expect(result).toEqual({ fallback: true });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('clamp', () => {
|
|
152
|
+
it('should clamp value within range', () => {
|
|
153
|
+
expect(clamp(5, 0, 10)).toBe(5);
|
|
154
|
+
expect(clamp(-5, 0, 10)).toBe(0);
|
|
155
|
+
expect(clamp(15, 0, 10)).toBe(10);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('isValidUrl', () => {
|
|
160
|
+
it('should validate correct URLs', () => {
|
|
161
|
+
expect(isValidUrl('https://example.com')).toBe(true);
|
|
162
|
+
expect(isValidUrl('http://localhost:3000')).toBe(true);
|
|
163
|
+
expect(isValidUrl('ftp://files.example.com')).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should reject invalid URLs', () => {
|
|
167
|
+
expect(isValidUrl('not-a-url')).toBe(false);
|
|
168
|
+
expect(isValidUrl('')).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|