@muhgholy/next-drive 4.7.0 → 4.8.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 CHANGED
@@ -590,7 +590,7 @@ Serve optimized images with dynamic compression, resizing, and format conversion
590
590
  ### URL Format
591
591
 
592
592
  ```
593
- /api/drive?action=serve&id={fileId}&quality={preset}&display={context}&size={preset}&format={format}
593
+ /api/drive?action=serve&id={fileId}&quality={preset}&display={context}&size={scale}&format={format}
594
594
  ```
595
595
 
596
596
  ### Parameters
@@ -598,10 +598,22 @@ Serve optimized images with dynamic compression, resizing, and format conversion
598
598
  | Parameter | Type | Description |
599
599
  |-----------|------|-------------|
600
600
  | `quality` | `low` / `medium` / `high` / `1-100` | Compression level |
601
- | `display` | string | Context-based quality adjustment |
602
- | `size` | string | Predefined dimensions preset |
601
+ | `display` | string | Sets aspect ratio, base dimensions, and quality factor |
602
+ | `size` | string | Scale factor (xs/sm/md/lg/xl) or standalone dimension preset |
603
603
  | `format` | `jpeg` / `webp` / `avif` / `png` | Output format |
604
604
 
605
+ ### How Display + Size Work Together
606
+
607
+ When **display** is specified, it defines the aspect ratio and base dimensions. The **size** parameter then scales those dimensions:
608
+
609
+ ```
610
+ display=article-image + size=sm → 400×225 (16:9, half size)
611
+ display=article-image + size=md → 800×450 (16:9, default)
612
+ display=article-image + size=lg → 1200×675 (16:9, 1.5x)
613
+ ```
614
+
615
+ When **no display** is specified, size uses standalone presets (fixed dimensions).
616
+
605
617
  ### Quality Presets
606
618
 
607
619
  | Preset | Base Quality | Use Case |
@@ -613,88 +625,74 @@ Serve optimized images with dynamic compression, resizing, and format conversion
613
625
 
614
626
  > Quality is dynamically adjusted based on file size. Larger files get more aggressive compression.
615
627
 
616
- ### Display Presets (Quality Context)
617
-
618
- | Display | Quality Factor | Use Case |
619
- |---------|----------------|----------|
620
- | `article-header` | 0.9 | Hero/banner images |
621
- | `article-image` | 0.85 | In-content images |
622
- | `thumbnail` | 0.7 | Small previews |
623
- | `avatar` | 0.8 | Profile pictures |
624
- | `logo` | 0.95 | Branding/logos |
625
- | `card` | 0.8 | Card components |
626
- | `gallery` | 0.85 | Gallery/grid |
627
- | `og` | 0.9 | Open Graph/social |
628
- | `icon` | 0.75 | Small icons |
629
- | `cover` | 0.9 | Full-width covers |
630
- | `story` | 0.85 | Story/vertical |
631
-
632
- ### Size Presets (Dimensions)
633
-
634
- **Square Sizes:**
635
- | Size | Dimensions |
636
- |------|------------|
637
- | `xs` | 64×64 |
638
- | `sm` | 128×128 |
639
- | `md` | 256×256 |
640
- | `lg` | 512×512 |
641
- | `xl` | 1024×1024 |
642
- | `2xl` | 1600×1600 |
643
- | `icon` | 48×48 |
644
- | `thumb` | 150×150 |
645
- | `square` | 600×600 |
646
- | `avatar-sm` | 64×64 |
647
- | `avatar-md` | 128×128 |
648
- | `avatar-lg` | 256×256 |
649
-
650
- **Landscape (16:9):**
651
- | Size | Dimensions |
652
- |------|------------|
653
- | `landscape-sm` | 480×270 |
654
- | `landscape` | 800×450 |
655
- | `landscape-lg` | 1280×720 |
656
- | `landscape-xl` | 1920×1080 |
657
-
658
- **Portrait (9:16):**
659
- | Size | Dimensions |
660
- |------|------------|
661
- | `portrait-sm` | 270×480 |
662
- | `portrait` | 450×800 |
663
- | `portrait-lg` | 720×1280 |
664
-
665
- **Wide/Banner:**
666
- | Size | Dimensions | Ratio |
667
- |------|------------|-------|
668
- | `wide` | 1200×630 | OG standard |
669
- | `banner` | 1200×400 | 3:1 |
670
- | `banner-sm` | 800×200 | 4:1 |
671
-
672
- **Other:**
673
- | Size | Dimensions | Ratio |
674
- |------|------------|-------|
675
- | `photo-4x3` | 800×600 | 4:3 |
676
- | `photo-3x2` | 900×600 | 3:2 |
677
- | `story` | 1080×1920 | 9:16 |
678
- | `video` | 1280×720 | 16:9 |
679
- | `video-sm` | 640×360 | 16:9 |
680
- | `card-sm` | 300×200 | 3:2 |
681
- | `card` | 400×300 | 4:3 |
682
- | `card-lg` | 600×400 | 3:2 |
628
+ ### Display Presets (Aspect Ratio + Dimensions)
629
+
630
+ | Display | Aspect Ratio | Base Size | Quality Factor |
631
+ |---------|--------------|-----------|----------------|
632
+ | `article-header` | 16:9 | 1200×675 | 0.9 |
633
+ | `article-image` | 16:9 | 800×450 | 0.85 |
634
+ | `thumbnail` | 1:1 | 150×150 | 0.7 |
635
+ | `avatar` | 1:1 | 128×128 | 0.8 |
636
+ | `logo` | 2:1 | 200×100 | 0.95 |
637
+ | `card` | 4:3 | 400×300 | 0.8 |
638
+ | `gallery` | 1:1 | 600×600 | 0.85 |
639
+ | `og` | ~1.9:1 | 1200×630 | 0.9 |
640
+ | `icon` | 1:1 | 48×48 | 0.75 |
641
+ | `cover` | 16:9 | 1920×1080 | 0.9 |
642
+ | `story` | 9:16 | 1080×1920 | 0.85 |
643
+ | `video` | 16:9 | 1280×720 | 0.85 |
644
+ | `banner` | 3:1 | 1200×400 | 0.9 |
645
+ | `portrait` | 3:4 | 600×800 | 0.85 |
646
+ | `landscape` | 4:3 | 800×600 | 0.85 |
647
+
648
+ ### Size Scale (with Display)
649
+
650
+ When used with a display preset, size scales the dimensions:
651
+
652
+ | Size | Scale | Example with `article-image` (800×450) |
653
+ |------|-------|----------------------------------------|
654
+ | `xs` | 0.25× | 200×113 |
655
+ | `sm` | 0.5× | 400×225 |
656
+ | `md` | 1.0× | 800×450 |
657
+ | `lg` | 1.5× | 1200×675 |
658
+ | `xl` | 2.0× | 1600×900 |
659
+ | `2xl` | 2.5× | 2000×1125 |
660
+
661
+ ### Standalone Size Presets (without Display)
662
+
663
+ When no display is specified, use these fixed dimension presets:
664
+
665
+ | Size | Dimensions | Size | Dimensions |
666
+ |------|------------|------|------------|
667
+ | `xs` | 64×64 | `landscape-sm` | 480×270 |
668
+ | `sm` | 128×128 | `landscape` | 800×450 |
669
+ | `md` | 256×256 | `landscape-lg` | 1280×720 |
670
+ | `lg` | 512×512 | `portrait-sm` | 270×480 |
671
+ | `xl` | 1024×1024 | `portrait` | 450×800 |
672
+ | `icon` | 48×48 | `wide` | 1200×630 |
673
+ | `thumb` | 150×150 | `banner` | 1200×400 |
674
+ | `video` | 1280×720 | `card` | 400×300 |
683
675
 
684
676
  ### Examples
685
677
 
686
678
  ```html
687
- <!-- Article header with OG dimensions -->
688
- <img src="/api/drive?action=serve&id=123&display=article-header&size=wide&format=webp">
679
+ <!-- Article image, smaller variant (400×225) -->
680
+ <img src="/api/drive?action=serve&id=123&display=article-image&size=sm&format=webp">
681
+
682
+ <!-- Article image, default size (800×450) -->
683
+ <img src="/api/drive?action=serve&id=123&display=article-image&format=webp">
684
+
685
+ <!-- Article image, larger variant (1200×675) -->
686
+ <img src="/api/drive?action=serve&id=123&display=article-image&size=lg&format=webp">
689
687
 
690
- <!-- Thumbnail with aggressive compression -->
691
- <img src="/api/drive?action=serve&id=123&display=thumbnail&size=thumb&format=webp">
688
+ <!-- Thumbnail (150×150 square) -->
689
+ <img src="/api/drive?action=serve&id=123&display=thumbnail&format=webp">
692
690
 
693
- <!-- Avatar -->
694
- <img src="/api/drive?action=serve&id=123&display=avatar&size=avatar-md&format=webp">
691
+ <!-- Avatar, smaller (64×64) -->
692
+ <img src="/api/drive?action=serve&id=123&display=avatar&size=sm&format=webp">
695
693
 
696
- <!-- Gallery image -->
697
- <img src="/api/drive?action=serve&id=123&display=gallery&size=landscape&format=webp">
694
+ <!-- Standalone size, no display -->
695
+ <img src="/api/drive?action=serve&id=123&size=landscape&format=webp">
698
696
 
699
697
  <!-- Just quality, no resize -->
700
698
  <img src="/api/drive?action=serve&id=123&quality=medium&format=webp">
@@ -357,70 +357,58 @@ var extractImageMetadata = async (filePath) => {
357
357
  }
358
358
  };
359
359
  var DISPLAY_PRESETS = {
360
- "article-header": 0.9,
361
- // Hero/banner images - high quality
362
- "article-image": 0.85,
363
- // In-content images
364
- "thumbnail": 0.7,
365
- // Small previews - lower quality ok
366
- "avatar": 0.8,
367
- // Profile pictures
368
- "logo": 0.95,
369
- // Branding - needs clarity
370
- "card": 0.8,
371
- // Card components
372
- "gallery": 0.85,
373
- // Gallery/grid images
374
- "og": 0.9,
375
- // Open Graph/social sharing
376
- "icon": 0.75,
377
- // Small icons
378
- "cover": 0.9,
379
- // Full-width covers
380
- "story": 0.85
381
- // Story/vertical format
360
+ "article-header": { ratio: [16, 9], baseWidth: 1200, qualityFactor: 0.9 },
361
+ "article-image": { ratio: [16, 9], baseWidth: 800, qualityFactor: 0.85 },
362
+ "thumbnail": { ratio: [1, 1], baseWidth: 150, qualityFactor: 0.7 },
363
+ "avatar": { ratio: [1, 1], baseWidth: 128, qualityFactor: 0.8 },
364
+ "logo": { ratio: [2, 1], baseWidth: 200, qualityFactor: 0.95 },
365
+ "card": { ratio: [4, 3], baseWidth: 400, qualityFactor: 0.8 },
366
+ "gallery": { ratio: [1, 1], baseWidth: 600, qualityFactor: 0.85 },
367
+ "og": { ratio: [1200, 630], baseWidth: 1200, qualityFactor: 0.9 },
368
+ "icon": { ratio: [1, 1], baseWidth: 48, qualityFactor: 0.75 },
369
+ "cover": { ratio: [16, 9], baseWidth: 1920, qualityFactor: 0.9 },
370
+ "story": { ratio: [9, 16], baseWidth: 1080, qualityFactor: 0.85 },
371
+ "video": { ratio: [16, 9], baseWidth: 1280, qualityFactor: 0.85 },
372
+ "banner": { ratio: [3, 1], baseWidth: 1200, qualityFactor: 0.9 },
373
+ "portrait": { ratio: [3, 4], baseWidth: 600, qualityFactor: 0.85 },
374
+ "landscape": { ratio: [4, 3], baseWidth: 800, qualityFactor: 0.85 }
382
375
  };
383
- var SIZE_PRESETS = {
384
- // Square sizes
376
+ var SIZE_SCALES = {
377
+ "xs": 0.25,
378
+ "sm": 0.5,
379
+ "md": 1,
380
+ "lg": 1.5,
381
+ "xl": 2,
382
+ "2xl": 2.5
383
+ };
384
+ var STANDALONE_SIZES = {
385
385
  "xs": { width: 64, height: 64 },
386
386
  "sm": { width: 128, height: 128 },
387
387
  "md": { width: 256, height: 256 },
388
388
  "lg": { width: 512, height: 512 },
389
389
  "xl": { width: 1024, height: 1024 },
390
390
  "2xl": { width: 1600, height: 1600 },
391
- // Named squares
392
391
  "icon": { width: 48, height: 48 },
393
392
  "thumb": { width: 150, height: 150 },
394
393
  "square": { width: 600, height: 600 },
395
394
  "avatar-sm": { width: 64, height: 64 },
396
395
  "avatar-md": { width: 128, height: 128 },
397
396
  "avatar-lg": { width: 256, height: 256 },
398
- // Landscape (16:9)
399
397
  "landscape-sm": { width: 480, height: 270 },
400
398
  "landscape": { width: 800, height: 450 },
401
399
  "landscape-lg": { width: 1280, height: 720 },
402
400
  "landscape-xl": { width: 1920, height: 1080 },
403
- // Portrait (9:16)
404
401
  "portrait-sm": { width: 270, height: 480 },
405
402
  "portrait": { width: 450, height: 800 },
406
403
  "portrait-lg": { width: 720, height: 1280 },
407
- // Wide/Banner (OG, social)
408
404
  "wide": { width: 1200, height: 630 },
409
- // Open Graph standard
410
405
  "banner": { width: 1200, height: 400 },
411
- // Banner/header
412
406
  "banner-sm": { width: 800, height: 200 },
413
- // Classic photo ratios
414
407
  "photo-4x3": { width: 800, height: 600 },
415
- // 4:3
416
408
  "photo-3x2": { width: 900, height: 600 },
417
- // 3:2
418
- // Story/vertical (9:16)
419
409
  "story": { width: 1080, height: 1920 },
420
- // Video thumbnails
421
410
  "video": { width: 1280, height: 720 },
422
411
  "video-sm": { width: 640, height: 360 },
423
- // Card sizes
424
412
  "card-sm": { width: 300, height: 200 },
425
413
  "card": { width: 400, height: 300 },
426
414
  "card-lg": { width: 600, height: 400 }
@@ -434,8 +422,24 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size) => {
434
422
  const n = parseInt(qualityPreset, 10);
435
423
  if (!isNaN(n)) baseQuality = Math.min(100, Math.max(1, n));
436
424
  }
437
- const displayFactor = display && DISPLAY_PRESETS[display] ? DISPLAY_PRESETS[display] : 1;
438
- baseQuality = Math.round(baseQuality * displayFactor);
425
+ let width;
426
+ let height;
427
+ let qualityFactor = 1;
428
+ const displayPreset = display ? DISPLAY_PRESETS[display] : void 0;
429
+ if (displayPreset) {
430
+ qualityFactor = displayPreset.qualityFactor;
431
+ const [ratioW, ratioH] = displayPreset.ratio;
432
+ const scale = size && SIZE_SCALES[size] ? SIZE_SCALES[size] : 1;
433
+ width = Math.round(displayPreset.baseWidth * scale);
434
+ height = Math.round(width * ratioH / ratioW);
435
+ } else if (size) {
436
+ const standalone = STANDALONE_SIZES[size];
437
+ if (standalone) {
438
+ width = standalone.width;
439
+ height = standalone.height;
440
+ }
441
+ }
442
+ baseQuality = Math.round(baseQuality * qualityFactor);
439
443
  let quality = baseQuality;
440
444
  let effort = 4;
441
445
  let pngCompression = 6;
@@ -463,12 +467,11 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size) => {
463
467
  pngCompression = 7;
464
468
  }
465
469
  }
466
- const dimensions = size && SIZE_PRESETS[size] ? SIZE_PRESETS[size] : void 0;
467
470
  return {
468
471
  quality: Math.max(1, Math.min(100, quality)),
469
472
  effort,
470
473
  pngCompression,
471
- ...dimensions && { width: dimensions.width, height: dimensions.height }
474
+ ...width && height && { width, height }
472
475
  };
473
476
  };
474
477
  var objectIdSchema = z.string().refine((val) => isValidObjectId(val), {
@@ -1547,9 +1550,9 @@ var driveUpload = async (source, key, options) => {
1547
1550
  }
1548
1551
  }
1549
1552
  let resolvedParentId = null;
1550
- if (options.folder?.path) {
1553
+ if (options.folder && "path" in options.folder) {
1551
1554
  resolvedParentId = await resolveFolderByPath(options.folder.path, key, accountId);
1552
- } else if (options.folder?.id && options.folder.id !== "root") {
1555
+ } else if (options.folder && "id" in options.folder && options.folder.id !== "root") {
1553
1556
  resolvedParentId = options.folder.id;
1554
1557
  } else if (options.parentId && options.parentId !== "root") {
1555
1558
  resolvedParentId = options.parentId;
@@ -1720,6 +1723,9 @@ var driveAPIHandler = async (req, res) => {
1720
1723
  const settings = getImageSettings(fileSize, quality, display, sizePreset);
1721
1724
  let targetFormat = format || mime.split("/")[1];
1722
1725
  if (targetFormat === "jpg") targetFormat = "jpeg";
1726
+ if (!["jpeg", "png", "webp", "avif"].includes(targetFormat)) {
1727
+ targetFormat = format || "webp";
1728
+ }
1723
1729
  const cacheDir = path.join(config.storage.path, "file", drive._id.toString(), "cache");
1724
1730
  const cacheKey = [
1725
1731
  "opt",
@@ -1756,13 +1762,17 @@ var driveAPIHandler = async (req, res) => {
1756
1762
  pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
1757
1763
  res.setHeader("Content-Type", "image/png");
1758
1764
  } else if (targetFormat === "webp") {
1759
- pipeline = pipeline.webp({ quality: settings.quality, effort: settings.effort });
1765
+ const webpEffort = Math.min(settings.effort, 6);
1766
+ pipeline = pipeline.webp({ quality: settings.quality, effort: webpEffort });
1760
1767
  res.setHeader("Content-Type", "image/webp");
1761
1768
  } else if (targetFormat === "avif") {
1762
1769
  pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
1763
1770
  res.setHeader("Content-Type", "image/avif");
1764
1771
  }
1765
1772
  res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1773
+ pipeline.on("error", (err) => {
1774
+ console.error("[next-drive] Pipeline error:", err);
1775
+ });
1766
1776
  stream.pipe(pipeline);
1767
1777
  pipeline.clone().toFile(cachePath).catch((e) => console.error("[next-drive] Cache write failed:", e));
1768
1778
  pipeline.clone().pipe(res);
@@ -2306,5 +2316,5 @@ var driveAPIHandler = async (req, res) => {
2306
2316
  };
2307
2317
 
2308
2318
  export { driveAPIHandler, driveConfiguration, driveDelete, driveFilePath, driveFileSchemaZod, driveGetUrl, driveInfo, driveList, driveReadFile, driveUpload, getDriveConfig, getDriveInformation };
2309
- //# sourceMappingURL=chunk-OWKTTRQC.js.map
2310
- //# sourceMappingURL=chunk-OWKTTRQC.js.map
2319
+ //# sourceMappingURL=chunk-AYHO6FSR.js.map
2320
+ //# sourceMappingURL=chunk-AYHO6FSR.js.map