@pz4l/tinyimg-core 0.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/README.md +688 -0
- package/README.zh-CN.md +688 -0
- package/dist/index.d.mts +516 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +851 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
2
|
+
|
|
3
|
+
# tinyimg-core
|
|
4
|
+
|
|
5
|
+
Core library for TinyPNG image compression with intelligent caching and multi-API key management.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Multi-API key management with smart rotation strategies
|
|
10
|
+
- MD5-based permanent caching system with two-level hierarchy
|
|
11
|
+
- Concurrent compression with rate limiting
|
|
12
|
+
- Cache statistics for monitoring and CLI display
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @pz4l/tinyimg-core
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Cache System
|
|
21
|
+
|
|
22
|
+
### Overview
|
|
23
|
+
|
|
24
|
+
TinyImg uses an MD5-based permanent cache for compressed images with two cache levels:
|
|
25
|
+
|
|
26
|
+
1. **Project Cache** (priority): `node_modules/.tinyimg_cache/` - Relative to project root
|
|
27
|
+
2. **Global Cache** (fallback): `~/.tinyimg/cache/` - Shared across projects
|
|
28
|
+
|
|
29
|
+
The cache system provides:
|
|
30
|
+
|
|
31
|
+
- Automatic cache hit detection using MD5 content hashing
|
|
32
|
+
- Atomic writes for concurrent safety
|
|
33
|
+
- Graceful corruption handling (silent re-compression)
|
|
34
|
+
- Statistics reporting for CLI display
|
|
35
|
+
|
|
36
|
+
### API Reference
|
|
37
|
+
|
|
38
|
+
#### calculateMD5
|
|
39
|
+
|
|
40
|
+
Calculates MD5 hash of a file's content. Used as cache key for storing/retrieving compressed images.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { calculateMD5 } from '@pz4l/tinyimg-core'
|
|
44
|
+
|
|
45
|
+
const hash = await calculateMD5('/path/to/image.png')
|
|
46
|
+
console.log(hash) // 'a1b2c3d4e5f6...'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Parameters:**
|
|
50
|
+
|
|
51
|
+
- `imagePath: string` - Absolute path to the image file
|
|
52
|
+
|
|
53
|
+
**Returns:** `Promise<string>` - 32-character hexadecimal MD5 hash
|
|
54
|
+
|
|
55
|
+
**Note:** Same content produces same hash regardless of filename or location.
|
|
56
|
+
|
|
57
|
+
#### getProjectCachePath
|
|
58
|
+
|
|
59
|
+
Returns the path to the project-level cache directory.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { getProjectCachePath } from '@pz4l/tinyimg-core'
|
|
63
|
+
|
|
64
|
+
const cachePath = getProjectCachePath('/Users/test/project')
|
|
65
|
+
// Returns: '/Users/test/project/node_modules/.tinyimg_cache'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Parameters:**
|
|
69
|
+
|
|
70
|
+
- `projectRoot: string` - Absolute path to the project root directory
|
|
71
|
+
|
|
72
|
+
**Returns:** `string` - Path to project cache directory
|
|
73
|
+
|
|
74
|
+
#### getGlobalCachePath
|
|
75
|
+
|
|
76
|
+
Returns the path to the global cache directory (shared across projects).
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { getGlobalCachePath } from '@pz4l/tinyimg-core'
|
|
80
|
+
|
|
81
|
+
const cachePath = getGlobalCachePath()
|
|
82
|
+
// Returns: '/Users/username/.tinyimg/cache'
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Returns:** `string` - Path to global cache directory
|
|
86
|
+
|
|
87
|
+
#### readCache
|
|
88
|
+
|
|
89
|
+
Reads cached compressed image from cache directories in priority order.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { readCache } from '@pz4l/tinyimg-core'
|
|
93
|
+
|
|
94
|
+
const cached = await readCache('image.png', [
|
|
95
|
+
getProjectCachePath('/project'),
|
|
96
|
+
getGlobalCachePath()
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
if (cached) {
|
|
100
|
+
console.log('Cache hit!')
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.log('Cache miss')
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Parameters:**
|
|
108
|
+
|
|
109
|
+
- `imagePath: string` - Absolute path to the source image
|
|
110
|
+
- `cacheDirs: string[]` - Array of cache directories in priority order
|
|
111
|
+
|
|
112
|
+
**Returns:** `Promise<Buffer | null>` - Cached compressed data or null if miss
|
|
113
|
+
|
|
114
|
+
**Behavior:**
|
|
115
|
+
|
|
116
|
+
- Iterates through cache directories in order
|
|
117
|
+
- Returns first successful read
|
|
118
|
+
- Returns null if all caches miss or corrupted
|
|
119
|
+
- Silent failure - no errors thrown for missing/corrupted cache
|
|
120
|
+
|
|
121
|
+
#### writeCache
|
|
122
|
+
|
|
123
|
+
Writes compressed image data to cache using atomic write pattern.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { writeCache } from '@pz4l/tinyimg-core'
|
|
127
|
+
|
|
128
|
+
const compressed = await compressImage(image)
|
|
129
|
+
await writeCache('image.png', compressed, getProjectCachePath('/project'))
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Parameters:**
|
|
133
|
+
|
|
134
|
+
- `imagePath: string` - Absolute path to the source image
|
|
135
|
+
- `data: Buffer` - Compressed image data to cache
|
|
136
|
+
- `cacheDir: string` - Cache directory to write to
|
|
137
|
+
|
|
138
|
+
**Behavior:**
|
|
139
|
+
|
|
140
|
+
- Uses atomic write (temp file + rename) for concurrent safety
|
|
141
|
+
- Auto-creates cache directory if needed
|
|
142
|
+
- Safe for concurrent writes from multiple processes
|
|
143
|
+
|
|
144
|
+
#### CacheStorage Class
|
|
145
|
+
|
|
146
|
+
Object-oriented interface for cache operations.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { CacheStorage } from '@pz4l/tinyimg-core'
|
|
150
|
+
|
|
151
|
+
const storage = new CacheStorage('/path/to/cache')
|
|
152
|
+
|
|
153
|
+
// Get cache file path for an image
|
|
154
|
+
const cachePath = await storage.getCachePath('/path/to/image.png')
|
|
155
|
+
|
|
156
|
+
// Read from cache
|
|
157
|
+
const data = await storage.read('/path/to/image.png')
|
|
158
|
+
|
|
159
|
+
// Write to cache
|
|
160
|
+
await storage.write('/path/to/image.png', compressedData)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Constructor:**
|
|
164
|
+
|
|
165
|
+
- `cacheDir: string` - Cache directory path
|
|
166
|
+
|
|
167
|
+
**Methods:**
|
|
168
|
+
|
|
169
|
+
- `async getCachePath(imagePath: string): Promise<string>` - Get cache file path
|
|
170
|
+
- `async read(imagePath: string): Promise<Buffer | null>` - Read cached data
|
|
171
|
+
- `async write(imagePath: string, data: Buffer): Promise<void>` - Write data to cache
|
|
172
|
+
|
|
173
|
+
#### getCacheStats
|
|
174
|
+
|
|
175
|
+
Returns cache statistics (file count and total size) for a directory.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { getCacheStats } from '@pz4l/tinyimg-core'
|
|
179
|
+
|
|
180
|
+
const stats = await getCacheStats('/path/to/cache')
|
|
181
|
+
console.log(`Files: ${stats.count}, Size: ${formatBytes(stats.size)}`)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Parameters:**
|
|
185
|
+
|
|
186
|
+
- `cacheDir: string` - Cache directory path
|
|
187
|
+
|
|
188
|
+
**Returns:** `Promise<CacheStats>` - Object with `count` and `size` (in bytes)
|
|
189
|
+
|
|
190
|
+
**Behavior:**
|
|
191
|
+
|
|
192
|
+
- Returns `{ count: 0, size: 0 }` for non-existent directories
|
|
193
|
+
- Handles errors gracefully (no exceptions thrown)
|
|
194
|
+
|
|
195
|
+
#### getAllCacheStats
|
|
196
|
+
|
|
197
|
+
Returns statistics for both project and global cache.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { getAllCacheStats } from '@pz4l/tinyimg-core'
|
|
201
|
+
|
|
202
|
+
// Get both project and global stats
|
|
203
|
+
const stats = await getAllCacheStats('/project/path')
|
|
204
|
+
console.log(`Project: ${stats.project?.count} files`)
|
|
205
|
+
console.log(`Global: ${stats.global.count} files`)
|
|
206
|
+
|
|
207
|
+
// Get only global stats
|
|
208
|
+
const globalOnly = await getAllCacheStats()
|
|
209
|
+
console.log(`Global: ${globalOnly.global.count} files`)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Parameters:**
|
|
213
|
+
|
|
214
|
+
- `projectRoot?: string` - Optional project root directory
|
|
215
|
+
|
|
216
|
+
**Returns:** `Promise<{ project: CacheStats | null, global: CacheStats }>` - Statistics object
|
|
217
|
+
|
|
218
|
+
**Behavior:**
|
|
219
|
+
|
|
220
|
+
- Project stats is `null` if no `projectRoot` provided
|
|
221
|
+
- Global stats always returned
|
|
222
|
+
|
|
223
|
+
#### formatBytes
|
|
224
|
+
|
|
225
|
+
Converts bytes to human-readable format for CLI display.
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { formatBytes } from '@pz4l/tinyimg-core'
|
|
229
|
+
|
|
230
|
+
formatBytes(0) // "0 B"
|
|
231
|
+
formatBytes(512) // "512 B"
|
|
232
|
+
formatBytes(1024) // "1.00 KB"
|
|
233
|
+
formatBytes(1536) // "1.50 KB"
|
|
234
|
+
formatBytes(1048576) // "1.00 MB"
|
|
235
|
+
formatBytes(1073741824) // "1.00 GB"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Parameters:**
|
|
239
|
+
|
|
240
|
+
- `bytes: number` - Number of bytes
|
|
241
|
+
|
|
242
|
+
**Returns:** `string` - Formatted string (e.g., "1.23 MB", "456 KB")
|
|
243
|
+
|
|
244
|
+
**Units:** B, KB, MB, GB (using 1024 as threshold)
|
|
245
|
+
|
|
246
|
+
### Usage Example
|
|
247
|
+
|
|
248
|
+
Complete example showing cache read/write pattern:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import {
|
|
252
|
+
formatBytes,
|
|
253
|
+
getAllCacheStats,
|
|
254
|
+
getGlobalCachePath,
|
|
255
|
+
getProjectCachePath,
|
|
256
|
+
readCache,
|
|
257
|
+
writeCache
|
|
258
|
+
} from 'tinyimg-core'
|
|
259
|
+
|
|
260
|
+
async function compressWithCache(imagePath: string, projectRoot: string) {
|
|
261
|
+
// Try to read from cache (project first, then global)
|
|
262
|
+
const cached = await readCache(imagePath, [
|
|
263
|
+
getProjectCachePath(projectRoot),
|
|
264
|
+
getGlobalCachePath()
|
|
265
|
+
])
|
|
266
|
+
|
|
267
|
+
if (cached) {
|
|
268
|
+
console.log('Cache hit! Using compressed image.')
|
|
269
|
+
return cached
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Cache miss - compress image
|
|
273
|
+
console.log('Cache miss. Compressing image...')
|
|
274
|
+
const compressed = await compressImage(imagePath)
|
|
275
|
+
|
|
276
|
+
// Write to project cache
|
|
277
|
+
await writeCache(imagePath, compressed, getProjectCachePath(projectRoot))
|
|
278
|
+
|
|
279
|
+
return compressed
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function showCacheStats(projectRoot: string) {
|
|
283
|
+
const stats = await getAllCacheStats(projectRoot)
|
|
284
|
+
|
|
285
|
+
console.log('Cache Statistics:')
|
|
286
|
+
console.log(`Project: ${stats.project?.count || 0} files, ${formatBytes(stats.project?.size || 0)}`)
|
|
287
|
+
console.log(`Global: ${stats.global.count} files, ${formatBytes(stats.global.size)}`)
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Cache Behavior
|
|
292
|
+
|
|
293
|
+
**Cache Key:**
|
|
294
|
+
|
|
295
|
+
- MD5 hash of original image content
|
|
296
|
+
- Same content = same hash, regardless of filename/location
|
|
297
|
+
|
|
298
|
+
**Cache File:**
|
|
299
|
+
|
|
300
|
+
- Filename: MD5 hash (no extension)
|
|
301
|
+
- Content: Compressed image data (Buffer)
|
|
302
|
+
|
|
303
|
+
**Cache Policy:**
|
|
304
|
+
|
|
305
|
+
- No TTL - cache is permanent until manually cleaned
|
|
306
|
+
- Corrupted cache files handled gracefully (silent re-compression)
|
|
307
|
+
- Atomic writes prevent concurrent write corruption
|
|
308
|
+
|
|
309
|
+
**Cache Priority:**
|
|
310
|
+
|
|
311
|
+
1. Project cache checked first (fastest, project-specific)
|
|
312
|
+
2. Global cache checked second (shared, fallback)
|
|
313
|
+
|
|
314
|
+
**Storage Locations:**
|
|
315
|
+
|
|
316
|
+
- Project: `<projectRoot>/node_modules/.tinyimg_cache/`
|
|
317
|
+
- Global: `~/.tinyimg/cache/`
|
|
318
|
+
|
|
319
|
+
## API Key Management
|
|
320
|
+
|
|
321
|
+
Coming soon in Phase 2 documentation.
|
|
322
|
+
|
|
323
|
+
## Compression API
|
|
324
|
+
|
|
325
|
+
### Overview
|
|
326
|
+
|
|
327
|
+
The compression API provides programmatic access to TinyPNG image compression with intelligent caching, multi-key management, and fallback strategies.
|
|
328
|
+
|
|
329
|
+
### compressImage
|
|
330
|
+
|
|
331
|
+
Compresses a single image with cache integration and automatic fallback.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { compressImage } from '@pz4l/tinyimg-core'
|
|
335
|
+
|
|
336
|
+
const imageBuffer = Buffer.from(/* image data */)
|
|
337
|
+
const compressed = await compressImage(imageBuffer, {
|
|
338
|
+
mode: 'auto', // 'auto' | 'api' | 'web'
|
|
339
|
+
cache: true, // Enable caching (default: true)
|
|
340
|
+
maxRetries: 8, // Max retry attempts (default: 8)
|
|
341
|
+
})
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Signature:**
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
async function compressImage(
|
|
348
|
+
buffer: Buffer,
|
|
349
|
+
options?: CompressServiceOptions
|
|
350
|
+
): Promise<Buffer>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Parameters:**
|
|
354
|
+
|
|
355
|
+
- `buffer: Buffer` - Original image data as Node.js Buffer
|
|
356
|
+
- `options?: CompressServiceOptions` - Compression options (see below)
|
|
357
|
+
|
|
358
|
+
**Returns:** `Promise<Buffer>` - Compressed image data
|
|
359
|
+
|
|
360
|
+
**Behavior:**
|
|
361
|
+
|
|
362
|
+
- Checks cache first (project cache, then global cache)
|
|
363
|
+
- Compresses using API keys with automatic rotation
|
|
364
|
+
- Falls back to web compressor if all keys exhausted
|
|
365
|
+
- Writes result to project cache for future use
|
|
366
|
+
- Gracefully handles cache errors (continues with compression)
|
|
367
|
+
|
|
368
|
+
### compressImages
|
|
369
|
+
|
|
370
|
+
Compresses multiple images with concurrency control.
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
import { compressImages } from '@pz4l/tinyimg-core'
|
|
374
|
+
|
|
375
|
+
const images = [buffer1, buffer2, buffer3]
|
|
376
|
+
const compressed = await compressImages(images, {
|
|
377
|
+
concurrency: 8, // Max parallel compressions (default: 8)
|
|
378
|
+
mode: 'auto',
|
|
379
|
+
cache: true,
|
|
380
|
+
})
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Signature:**
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
async function compressImages(
|
|
387
|
+
buffers: Buffer[],
|
|
388
|
+
options?: CompressServiceOptions
|
|
389
|
+
): Promise<Buffer[]>
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Parameters:**
|
|
393
|
+
|
|
394
|
+
- `buffers: Buffer[]` - Array of image buffers to compress
|
|
395
|
+
- `options?: CompressServiceOptions` - Compression options
|
|
396
|
+
|
|
397
|
+
**Returns:** `Promise<Buffer[]>` - Array of compressed image buffers (same order as input)
|
|
398
|
+
|
|
399
|
+
**Behavior:**
|
|
400
|
+
|
|
401
|
+
- Processes images with configurable concurrency limit
|
|
402
|
+
- Each image goes through the same pipeline as `compressImage`
|
|
403
|
+
- Maintains order of results matching input order
|
|
404
|
+
- Failed compressions will throw (use try/catch for individual handling)
|
|
405
|
+
|
|
406
|
+
### KeyPool
|
|
407
|
+
|
|
408
|
+
Manages multiple API keys with automatic rotation and quota tracking.
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { KeyPool } from '@pz4l/tinyimg-core'
|
|
412
|
+
|
|
413
|
+
// Create pool with random strategy (default)
|
|
414
|
+
const pool = new KeyPool('random')
|
|
415
|
+
|
|
416
|
+
// Create pool with round-robin strategy
|
|
417
|
+
const pool = new KeyPool('round-robin')
|
|
418
|
+
|
|
419
|
+
// Create pool with priority strategy
|
|
420
|
+
const pool = new KeyPool('priority')
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**Constructor:**
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
new KeyPool(strategy?: KeyStrategy)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Parameters:**
|
|
430
|
+
|
|
431
|
+
- `strategy: KeyStrategy` - Key selection strategy: `'random'` | `'round-robin'` | `'priority'`
|
|
432
|
+
- `random` (default): Randomly select available keys
|
|
433
|
+
- `round-robin`: Cycle through keys in order
|
|
434
|
+
- `priority`: Prefer API keys, fallback to web compressor
|
|
435
|
+
|
|
436
|
+
**Methods:**
|
|
437
|
+
|
|
438
|
+
- `async selectKey(): Promise<string>` - Select and return an available API key
|
|
439
|
+
- `decrementQuota(): void` - Mark current key's quota as used
|
|
440
|
+
- `getCurrentKey(): string | null` - Get the currently selected key
|
|
441
|
+
|
|
442
|
+
**Throws:**
|
|
443
|
+
|
|
444
|
+
- `NoValidKeysError` - When no API keys are configured
|
|
445
|
+
- `AllKeysExhaustedError` - When all keys have exhausted their quota
|
|
446
|
+
|
|
447
|
+
### Type Definitions
|
|
448
|
+
|
|
449
|
+
#### CompressServiceOptions
|
|
450
|
+
|
|
451
|
+
Options for compression operations.
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
interface CompressServiceOptions {
|
|
455
|
+
/** Compression mode (default: 'auto') */
|
|
456
|
+
mode?: 'auto' | 'api' | 'web'
|
|
457
|
+
|
|
458
|
+
/** Enable cache (default: true) */
|
|
459
|
+
cache?: boolean
|
|
460
|
+
|
|
461
|
+
/** Use project cache only, ignore global cache (default: false) */
|
|
462
|
+
projectCacheOnly?: boolean
|
|
463
|
+
|
|
464
|
+
/** Concurrency limit for batch operations (default: 8) */
|
|
465
|
+
concurrency?: number
|
|
466
|
+
|
|
467
|
+
/** Maximum retry attempts (default: 8) */
|
|
468
|
+
maxRetries?: number
|
|
469
|
+
|
|
470
|
+
/** Custom KeyPool instance for advanced usage */
|
|
471
|
+
keyPool?: KeyPool
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
#### CompressOptions
|
|
476
|
+
|
|
477
|
+
Base compression options (used internally).
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
interface CompressOptions {
|
|
481
|
+
/** Compression mode (default: 'auto') */
|
|
482
|
+
mode?: 'auto' | 'api' | 'web'
|
|
483
|
+
|
|
484
|
+
/** Custom compressor array for fallback chain */
|
|
485
|
+
compressors?: ICompressor[]
|
|
486
|
+
|
|
487
|
+
/** Maximum retry attempts (default: 3) */
|
|
488
|
+
maxRetries?: number
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
#### CompressionMode
|
|
493
|
+
|
|
494
|
+
Type for compression mode selection.
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
type CompressionMode = 'auto' | 'api' | 'web'
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
#### KeyStrategy
|
|
501
|
+
|
|
502
|
+
Type for key pool strategy selection.
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
type KeyStrategy = 'random' | 'round-robin' | 'priority'
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Error Types
|
|
509
|
+
|
|
510
|
+
#### AllKeysExhaustedError
|
|
511
|
+
|
|
512
|
+
Thrown when all API keys have exhausted their quota.
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import { AllKeysExhaustedError } from '@pz4l/tinyimg-core'
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
await compressImage(buffer)
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
if (error instanceof AllKeysExhaustedError) {
|
|
522
|
+
console.log('All API keys exhausted, falling back to web compressor')
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
#### NoValidKeysError
|
|
528
|
+
|
|
529
|
+
Thrown when no API keys are configured.
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
import { NoValidKeysError } from '@pz4l/tinyimg-core'
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
const pool = new KeyPool('random')
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
if (error instanceof NoValidKeysError) {
|
|
539
|
+
console.log('Please configure API keys via TINYPNG_KEYS env var')
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
#### AllCompressionFailedError
|
|
545
|
+
|
|
546
|
+
Thrown when all compression methods (API and web) have failed.
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
import { AllCompressionFailedError } from '@pz4l/tinyimg-core'
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
await compressImage(buffer)
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
if (error instanceof AllCompressionFailedError) {
|
|
556
|
+
console.log('Compression failed - image may be corrupted or unsupported')
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Complete Usage Example
|
|
562
|
+
|
|
563
|
+
Full workflow example showing compression with caching and error handling:
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
567
|
+
import {
|
|
568
|
+
AllCompressionFailedError,
|
|
569
|
+
AllKeysExhaustedError,
|
|
570
|
+
compressImage,
|
|
571
|
+
compressImages,
|
|
572
|
+
formatBytes,
|
|
573
|
+
getAllCacheStats,
|
|
574
|
+
getGlobalCachePath,
|
|
575
|
+
getProjectCachePath,
|
|
576
|
+
KeyPool,
|
|
577
|
+
NoValidKeysError,
|
|
578
|
+
} from 'tinyimg-core'
|
|
579
|
+
|
|
580
|
+
// Single image compression
|
|
581
|
+
async function compressSingleImage(inputPath: string, outputPath: string) {
|
|
582
|
+
try {
|
|
583
|
+
const imageBuffer = await readFile(inputPath)
|
|
584
|
+
|
|
585
|
+
const compressed = await compressImage(imageBuffer, {
|
|
586
|
+
mode: 'auto', // Try API first, fallback to web
|
|
587
|
+
cache: true, // Enable caching
|
|
588
|
+
maxRetries: 8, // Retry on transient failures
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
await writeFile(outputPath, compressed)
|
|
592
|
+
|
|
593
|
+
const savings = ((1 - compressed.length / imageBuffer.length) * 100).toFixed(1)
|
|
594
|
+
console.log(`Compressed: ${savings}% reduction`)
|
|
595
|
+
|
|
596
|
+
return compressed
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
if (error instanceof AllCompressionFailedError) {
|
|
600
|
+
console.error('Compression failed: All methods exhausted')
|
|
601
|
+
}
|
|
602
|
+
else if (error instanceof NoValidKeysError) {
|
|
603
|
+
console.error('No API keys configured. Set TINYPNG_KEYS env var.')
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
console.error('Unexpected error:', error)
|
|
607
|
+
}
|
|
608
|
+
throw error
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Batch compression with concurrency
|
|
613
|
+
async function compressBatch(inputPaths: string[], outputDir: string) {
|
|
614
|
+
const images = await Promise.all(
|
|
615
|
+
inputPaths.map(path => readFile(path))
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
const compressed = await compressImages(images, {
|
|
619
|
+
concurrency: 8, // Process 8 images in parallel
|
|
620
|
+
mode: 'auto',
|
|
621
|
+
cache: true,
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
// Save results
|
|
625
|
+
await Promise.all(
|
|
626
|
+
compressed.map((data, i) =>
|
|
627
|
+
writeFile(`${outputDir}/compressed-${i}.png`, data)
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
// Calculate total savings
|
|
632
|
+
const originalSize = images.reduce((sum, buf) => sum + buf.length, 0)
|
|
633
|
+
const compressedSize = compressed.reduce((sum, buf) => sum + buf.length, 0)
|
|
634
|
+
const savings = ((1 - compressedSize / originalSize) * 100).toFixed(1)
|
|
635
|
+
|
|
636
|
+
console.log(`Batch complete: ${savings}% total reduction`)
|
|
637
|
+
return compressed
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Show cache statistics
|
|
641
|
+
async function showStats(projectRoot: string) {
|
|
642
|
+
const stats = await getAllCacheStats(projectRoot)
|
|
643
|
+
|
|
644
|
+
console.log('Cache Statistics:')
|
|
645
|
+
console.log(` Project: ${stats.project?.count || 0} files (${formatBytes(stats.project?.size || 0)})`)
|
|
646
|
+
console.log(` Global: ${stats.global.count} files (${formatBytes(stats.global.size)})`)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Manual KeyPool usage (advanced)
|
|
650
|
+
async function manualKeyManagement() {
|
|
651
|
+
try {
|
|
652
|
+
const pool = new KeyPool('round-robin')
|
|
653
|
+
|
|
654
|
+
// Get a key for manual API calls
|
|
655
|
+
const key = await pool.selectKey()
|
|
656
|
+
console.log(`Using key: ${key.substring(0, 4)}****${key.slice(-4)}`)
|
|
657
|
+
|
|
658
|
+
// Mark quota as used after compression
|
|
659
|
+
pool.decrementQuota()
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
if (error instanceof AllKeysExhaustedError) {
|
|
663
|
+
console.log('All keys exhausted - using web fallback')
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Run examples
|
|
669
|
+
async function main() {
|
|
670
|
+
await compressSingleImage('input.png', 'output.png')
|
|
671
|
+
await showStats(process.cwd())
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
main().catch(console.error)
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
## Error Handling
|
|
678
|
+
|
|
679
|
+
The cache system is designed to fail gracefully:
|
|
680
|
+
|
|
681
|
+
- Missing cache directory → Returns null (cache miss)
|
|
682
|
+
- Corrupted cache file → Returns null (triggers re-compression)
|
|
683
|
+
- Concurrent writes → Atomic write pattern prevents corruption
|
|
684
|
+
- Permission errors → Silent failure (logs warning)
|
|
685
|
+
|
|
686
|
+
## License
|
|
687
|
+
|
|
688
|
+
MIT
|