vasuzex 2.1.20 → 2.1.21

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.
@@ -37,23 +37,25 @@ export class MediaManager {
37
37
  const cached = await this.getCachedThumbnail(cacheKey);
38
38
 
39
39
  if (cached) {
40
+ // Detect content type from cached buffer
41
+ const contentType = await this.detectContentType(cached);
40
42
  return {
41
43
  buffer: cached,
42
44
  fromCache: true,
43
- contentType: 'image/jpeg',
45
+ contentType,
44
46
  };
45
47
  }
46
48
 
47
49
  // Generate thumbnail
48
- const thumbnail = await this.generateThumbnail(imagePath, width, height);
50
+ const result = await this.generateThumbnail(imagePath, width, height);
49
51
 
50
52
  // Cache it
51
- await this.cacheThumbnail(cacheKey, thumbnail);
53
+ await this.cacheThumbnail(cacheKey, result.buffer);
52
54
 
53
55
  return {
54
- buffer: thumbnail,
56
+ buffer: result.buffer,
55
57
  fromCache: false,
56
- contentType: 'image/jpeg',
58
+ contentType: result.contentType,
57
59
  };
58
60
  }
59
61
 
@@ -78,19 +80,47 @@ export class MediaManager {
78
80
  const storage = this.app.make('storage');
79
81
  const imageBuffer = await storage.get(imagePath);
80
82
 
81
- const thumbnail = await sharp(imageBuffer)
83
+ // Detect image metadata
84
+ const image = sharp(imageBuffer);
85
+ const metadata = await image.metadata();
86
+ const hasAlpha = metadata.hasAlpha;
87
+ const originalFormat = metadata.format;
88
+
89
+ // Build the sharp pipeline
90
+ let pipeline = sharp(imageBuffer)
82
91
  .resize(width, height, {
83
92
  fit: this.config.thumbnails.fit,
84
93
  position: this.config.thumbnails.position,
85
94
  withoutEnlargement: true,
86
- })
87
- .jpeg({
95
+ });
96
+
97
+ // Choose output format based on input format and transparency
98
+ let contentType = 'image/jpeg';
99
+
100
+ if (originalFormat === 'webp') {
101
+ // Preserve WebP format (supports transparency and good compression)
102
+ pipeline = pipeline.webp({
103
+ quality: this.config.thumbnails.quality,
104
+ });
105
+ contentType = 'image/webp';
106
+ } else if (hasAlpha) {
107
+ // For PNG or other formats with transparency, use PNG
108
+ pipeline = pipeline.png({
109
+ quality: this.config.thumbnails.quality,
110
+ compressionLevel: 9,
111
+ });
112
+ contentType = 'image/png';
113
+ } else {
114
+ // For opaque images, use JPEG for best compression
115
+ pipeline = pipeline.jpeg({
88
116
  quality: this.config.thumbnails.quality,
89
117
  progressive: true,
90
- })
91
- .toBuffer();
118
+ });
119
+ }
120
+
121
+ const thumbnail = await pipeline.toBuffer();
92
122
 
93
- return thumbnail;
123
+ return { buffer: thumbnail, contentType };
94
124
  }
95
125
 
96
126
  /**
@@ -139,22 +169,27 @@ export class MediaManager {
139
169
  */
140
170
  async getCachedThumbnail(cacheKey) {
141
171
  try {
142
- const cachePath = join(this.cacheDir, `${cacheKey}.jpg`);
143
-
144
- if (!existsSync(cachePath)) {
145
- return null;
146
- }
172
+ // Try all supported image extensions
173
+ for (const ext of ['.webp', '.png', '.jpg']) {
174
+ const cachePath = join(this.cacheDir, `${cacheKey}${ext}`);
175
+
176
+ if (!existsSync(cachePath)) {
177
+ continue;
178
+ }
147
179
 
148
- // Check if cache expired
149
- const stats = await stat(cachePath);
150
- const age = Date.now() - stats.mtimeMs;
151
-
152
- if (age > this.cacheTTL) {
153
- await unlink(cachePath);
154
- return null;
155
- }
180
+ // Check if cache expired
181
+ const stats = await stat(cachePath);
182
+ const age = Date.now() - stats.mtimeMs;
183
+
184
+ if (age > this.cacheTTL) {
185
+ await unlink(cachePath);
186
+ continue;
187
+ }
156
188
 
157
- return await readFile(cachePath);
189
+ return await readFile(cachePath);
190
+ }
191
+
192
+ return null;
158
193
  } catch (error) {
159
194
  return null;
160
195
  }
@@ -165,7 +200,15 @@ export class MediaManager {
165
200
  */
166
201
  async cacheThumbnail(cacheKey, buffer) {
167
202
  try {
168
- const cachePath = join(this.cacheDir, `${cacheKey}.jpg`);
203
+ // Detect format from buffer to use correct extension
204
+ const metadata = await sharp(buffer).metadata();
205
+ const formatMap = {
206
+ webp: '.webp',
207
+ png: '.png',
208
+ jpeg: '.jpg',
209
+ };
210
+ const ext = formatMap[metadata.format] || '.jpg';
211
+ const cachePath = join(this.cacheDir, `${cacheKey}${ext}`);
169
212
  await mkdir(dirname(cachePath), { recursive: true });
170
213
  await writeFile(cachePath, buffer);
171
214
  } catch (error) {
@@ -180,12 +223,12 @@ export class MediaManager {
180
223
  async getCacheStats() {
181
224
  try {
182
225
  const files = await readdir(this.cacheDir);
183
- const jpgFiles = files.filter(f => f.endsWith('.jpg'));
226
+ const imageFiles = files.filter(f => f.endsWith('.jpg') || f.endsWith('.png') || f.endsWith('.webp'));
184
227
 
185
228
  let totalSize = 0;
186
229
  let expiredCount = 0;
187
230
 
188
- for (const file of jpgFiles) {
231
+ for (const file of imageFiles) {
189
232
  const filePath = join(this.cacheDir, file);
190
233
  const stats = await stat(filePath);
191
234
  totalSize += stats.size;
@@ -197,7 +240,7 @@ export class MediaManager {
197
240
  }
198
241
 
199
242
  return {
200
- total: jpgFiles.length,
243
+ total: imageFiles.length,
201
244
  size: totalSize,
202
245
  sizeFormatted: this.formatBytes(totalSize),
203
246
  expired: expiredCount,
@@ -223,11 +266,11 @@ export class MediaManager {
223
266
  async clearExpiredCache() {
224
267
  try {
225
268
  const files = await readdir(this.cacheDir);
226
- const jpgFiles = files.filter(f => f.endsWith('.jpg'));
269
+ const imageFiles = files.filter(f => f.endsWith('.jpg') || f.endsWith('.png') || f.endsWith('.webp'));
227
270
 
228
271
  let cleared = 0;
229
272
 
230
- for (const file of jpgFiles) {
273
+ for (const file of imageFiles) {
231
274
  const filePath = join(this.cacheDir, file);
232
275
  const stats = await stat(filePath);
233
276
  const age = Date.now() - stats.mtimeMs;
@@ -251,14 +294,14 @@ export class MediaManager {
251
294
  async clearAllCache() {
252
295
  try {
253
296
  const files = await readdir(this.cacheDir);
254
- const jpgFiles = files.filter(f => f.endsWith('.jpg'));
297
+ const imageFiles = files.filter(f => f.endsWith('.jpg') || f.endsWith('.png') || f.endsWith('.webp'));
255
298
 
256
- for (const file of jpgFiles) {
299
+ for (const file of imageFiles) {
257
300
  const filePath = join(this.cacheDir, file);
258
301
  await unlink(filePath);
259
302
  }
260
303
 
261
- return jpgFiles.length;
304
+ return imageFiles.length;
262
305
  } catch (error) {
263
306
  console.error('Failed to clear cache:', error.message);
264
307
  return 0;
@@ -293,6 +336,25 @@ export class MediaManager {
293
336
  return types[ext] || 'application/octet-stream';
294
337
  }
295
338
 
339
+ /**
340
+ * Detect content type from buffer
341
+ */
342
+ async detectContentType(buffer) {
343
+ try {
344
+ const metadata = await sharp(buffer).metadata();
345
+ const formatMap = {
346
+ jpeg: 'image/jpeg',
347
+ png: 'image/png',
348
+ gif: 'image/gif',
349
+ webp: 'image/webp',
350
+ svg: 'image/svg+xml',
351
+ };
352
+ return formatMap[metadata.format] || 'image/jpeg';
353
+ } catch (error) {
354
+ return 'image/jpeg';
355
+ }
356
+ }
357
+
296
358
  /**
297
359
  * Format bytes to human readable
298
360
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vasuzex",
3
- "version": "2.1.20",
3
+ "version": "2.1.21",
4
4
  "description": "Laravel-inspired framework for Node.js monorepos - V2 with optimized dependencies",
5
5
  "type": "module",
6
6
  "main": "./framework/index.js",