@muhgholy/next-drive 4.7.0 → 4.9.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}&fit={mode}&position={anchor}&format={format}
594
594
  ```
595
595
 
596
596
  ### Parameters
@@ -598,10 +598,46 @@ 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, quality factor, and default fit |
602
+ | `size` | string | Scale factor (xs/sm/md/lg/xl) or standalone dimension preset |
603
+ | `fit` | `cover` / `contain` / `fill` / `inside` / `outside` | How image fits into dimensions |
604
+ | `position` | `center` / `top` / `bottom` / `left` / `right` / `attention` / `entropy` | Crop anchor point (for cover/contain) |
603
605
  | `format` | `jpeg` / `webp` / `avif` / `png` | Output format |
604
606
 
607
+ ### Fit Options
608
+
609
+ | Fit | Behavior | Use Case |
610
+ |-----|----------|----------|
611
+ | `cover` | Crop to fill exact dimensions | Thumbnails, avatars, cards |
612
+ | `contain` | Fit within dimensions (may letterbox) | Logos, icons |
613
+ | `fill` | Stretch to exact dimensions (may distort) | Background fills |
614
+ | `inside` | Fit within, no upscaling *(default)* | Article images |
615
+ | `outside` | Cover minimum dimensions | Backgrounds |
616
+
617
+ ### Position Options (for cover/contain)
618
+
619
+ | Position | Anchor Point |
620
+ |----------|--------------|
621
+ | `center` | Center *(default)* |
622
+ | `top` | Top center |
623
+ | `bottom` | Bottom center |
624
+ | `left` | Left center |
625
+ | `right` | Right center |
626
+ | `attention` | Focus on most "interesting" area (AI-based) |
627
+ | `entropy` | Focus on highest entropy area |
628
+
629
+ ### How Display + Size Work Together
630
+
631
+ When **display** is specified, it defines the aspect ratio, base dimensions, and default fit. The **size** parameter then scales those dimensions:
632
+
633
+ ```
634
+ display=article-image + size=sm → 400×225 (16:9, half size, fit=inside)
635
+ display=thumbnail + size=md → 150×150 (1:1, fit=cover)
636
+ display=avatar + fit=contain → 128×128 (override default cover to contain)
637
+ ```
638
+
639
+ When **no display** is specified, size uses standalone presets (fixed dimensions).
640
+
605
641
  ### Quality Presets
606
642
 
607
643
  | Preset | Base Quality | Use Case |
@@ -613,88 +649,77 @@ Serve optimized images with dynamic compression, resizing, and format conversion
613
649
 
614
650
  > Quality is dynamically adjusted based on file size. Larger files get more aggressive compression.
615
651
 
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 |
652
+ ### Display Presets (Aspect Ratio + Dimensions + Fit)
653
+
654
+ | Display | Aspect Ratio | Base Size | Quality | Default Fit |
655
+ |---------|--------------|-----------|---------|-------------|
656
+ | `article-header` | 16:9 | 1200×675 | 0.9 | inside |
657
+ | `article-image` | 16:9 | 800×450 | 0.85 | inside |
658
+ | `thumbnail` | 1:1 | 150×150 | 0.7 | cover |
659
+ | `avatar` | 1:1 | 128×128 | 0.8 | cover |
660
+ | `logo` | 2:1 | 200×100 | 0.95 | contain |
661
+ | `card` | 4:3 | 400×300 | 0.8 | cover |
662
+ | `gallery` | 1:1 | 600×600 | 0.85 | cover |
663
+ | `og` | ~1.9:1 | 1200×630 | 0.9 | cover |
664
+ | `icon` | 1:1 | 48×48 | 0.75 | cover |
665
+ | `cover` | 16:9 | 1920×1080 | 0.9 | cover |
666
+ | `story` | 9:16 | 1080×1920 | 0.85 | cover |
667
+ | `video` | 16:9 | 1280×720 | 0.85 | cover |
668
+ | `banner` | 3:1 | 1200×400 | 0.9 | cover |
669
+ | `portrait` | 3:4 | 600×800 | 0.85 | inside |
670
+ | `landscape` | 4:3 | 800×600 | 0.85 | inside |
671
+
672
+ ### Size Scale (with Display)
673
+
674
+ When used with a display preset, size scales the dimensions:
675
+
676
+ | Size | Scale | Example with `article-image` (800×450) |
677
+ |------|-------|----------------------------------------|
678
+ | `xs` | 0.25× | 200×113 |
679
+ | `sm` | 0.5× | 400×225 |
680
+ | `md` | 1.0× | 800×450 |
681
+ | `lg` | 1.5× | 1200×675 |
682
+ | `xl` | 2.0× | 1600×900 |
683
+ | `2xl` | 2.5× | 2000×1125 |
684
+
685
+ ### Standalone Size Presets (without Display)
686
+
687
+ When no display is specified, use these fixed dimension presets:
688
+
689
+ | Size | Dimensions | Size | Dimensions |
690
+ |------|------------|------|------------|
691
+ | `xs` | 64×64 | `landscape-sm` | 480×270 |
692
+ | `sm` | 128×128 | `landscape` | 800×450 |
693
+ | `md` | 256×256 | `landscape-lg` | 1280×720 |
694
+ | `lg` | 512×512 | `portrait-sm` | 270×480 |
695
+ | `xl` | 1024×1024 | `portrait` | 450×800 |
696
+ | `icon` | 48×48 | `wide` | 1200×630 |
697
+ | `thumb` | 150×150 | `banner` | 1200×400 |
698
+ | `video` | 1280×720 | `card` | 400×300 |
683
699
 
684
700
  ### Examples
685
701
 
686
702
  ```html
687
- <!-- Article header with OG dimensions -->
688
- <img src="/api/drive?action=serve&id=123&display=article-header&size=wide&format=webp">
703
+ <!-- Article image, smaller variant (400×225, fit=inside) -->
704
+ <img src="/api/drive?action=serve&id=123&display=article-image&size=sm&format=webp">
705
+
706
+ <!-- Thumbnail with cover fit (crops to fill 150×150 square) -->
707
+ <img src="/api/drive?action=serve&id=123&display=thumbnail&format=webp">
708
+
709
+ <!-- Avatar with top-focused crop (for face photos) -->
710
+ <img src="/api/drive?action=serve&id=123&display=avatar&fit=cover&position=top&format=webp">
711
+
712
+ <!-- Gallery with AI-based attention crop -->
713
+ <img src="/api/drive?action=serve&id=123&display=gallery&fit=cover&position=attention&format=webp">
689
714
 
690
- <!-- Thumbnail with aggressive compression -->
691
- <img src="/api/drive?action=serve&id=123&display=thumbnail&size=thumb&format=webp">
715
+ <!-- Card image, override default cover to contain -->
716
+ <img src="/api/drive?action=serve&id=123&display=card&fit=contain&format=webp">
692
717
 
693
- <!-- Avatar -->
694
- <img src="/api/drive?action=serve&id=123&display=avatar&size=avatar-md&format=webp">
718
+ <!-- Banner with custom position -->
719
+ <img src="/api/drive?action=serve&id=123&display=banner&position=bottom&format=webp">
695
720
 
696
- <!-- Gallery image -->
697
- <img src="/api/drive?action=serve&id=123&display=gallery&size=landscape&format=webp">
721
+ <!-- Standalone size, no display -->
722
+ <img src="/api/drive?action=serve&id=123&size=landscape&fit=cover&format=webp">
698
723
 
699
724
  <!-- Just quality, no resize -->
700
725
  <img src="/api/drive?action=serve&id=123&quality=medium&format=webp">
@@ -357,75 +357,77 @@ 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, defaultFit: "inside" },
361
+ "article-image": { ratio: [16, 9], baseWidth: 800, qualityFactor: 0.85, defaultFit: "inside" },
362
+ "thumbnail": { ratio: [1, 1], baseWidth: 150, qualityFactor: 0.7, defaultFit: "cover" },
363
+ "avatar": { ratio: [1, 1], baseWidth: 128, qualityFactor: 0.8, defaultFit: "cover" },
364
+ "logo": { ratio: [2, 1], baseWidth: 200, qualityFactor: 0.95, defaultFit: "contain" },
365
+ "card": { ratio: [4, 3], baseWidth: 400, qualityFactor: 0.8, defaultFit: "cover" },
366
+ "gallery": { ratio: [1, 1], baseWidth: 600, qualityFactor: 0.85, defaultFit: "cover" },
367
+ "og": { ratio: [1200, 630], baseWidth: 1200, qualityFactor: 0.9, defaultFit: "cover" },
368
+ "icon": { ratio: [1, 1], baseWidth: 48, qualityFactor: 0.75, defaultFit: "cover" },
369
+ "cover": { ratio: [16, 9], baseWidth: 1920, qualityFactor: 0.9, defaultFit: "cover" },
370
+ "story": { ratio: [9, 16], baseWidth: 1080, qualityFactor: 0.85, defaultFit: "cover" },
371
+ "video": { ratio: [16, 9], baseWidth: 1280, qualityFactor: 0.85, defaultFit: "cover" },
372
+ "banner": { ratio: [3, 1], baseWidth: 1200, qualityFactor: 0.9, defaultFit: "cover" },
373
+ "portrait": { ratio: [3, 4], baseWidth: 600, qualityFactor: 0.85, defaultFit: "inside" },
374
+ "landscape": { ratio: [4, 3], baseWidth: 800, qualityFactor: 0.85, defaultFit: "inside" }
382
375
  };
383
- var SIZE_PRESETS = {
384
- // Square sizes
376
+ var VALID_FIT_OPTIONS = ["cover", "contain", "fill", "inside", "outside"];
377
+ var VALID_POSITION_OPTIONS = [
378
+ "center",
379
+ "top",
380
+ "right top",
381
+ "right",
382
+ "right bottom",
383
+ "bottom",
384
+ "left bottom",
385
+ "left",
386
+ "left top",
387
+ "attention",
388
+ "entropy"
389
+ ];
390
+ var SIZE_SCALES = {
391
+ "xs": 0.25,
392
+ "sm": 0.5,
393
+ "md": 1,
394
+ "lg": 1.5,
395
+ "xl": 2,
396
+ "2xl": 2.5
397
+ };
398
+ var STANDALONE_SIZES = {
385
399
  "xs": { width: 64, height: 64 },
386
400
  "sm": { width: 128, height: 128 },
387
401
  "md": { width: 256, height: 256 },
388
402
  "lg": { width: 512, height: 512 },
389
403
  "xl": { width: 1024, height: 1024 },
390
404
  "2xl": { width: 1600, height: 1600 },
391
- // Named squares
392
405
  "icon": { width: 48, height: 48 },
393
406
  "thumb": { width: 150, height: 150 },
394
407
  "square": { width: 600, height: 600 },
395
408
  "avatar-sm": { width: 64, height: 64 },
396
409
  "avatar-md": { width: 128, height: 128 },
397
410
  "avatar-lg": { width: 256, height: 256 },
398
- // Landscape (16:9)
399
411
  "landscape-sm": { width: 480, height: 270 },
400
412
  "landscape": { width: 800, height: 450 },
401
413
  "landscape-lg": { width: 1280, height: 720 },
402
414
  "landscape-xl": { width: 1920, height: 1080 },
403
- // Portrait (9:16)
404
415
  "portrait-sm": { width: 270, height: 480 },
405
416
  "portrait": { width: 450, height: 800 },
406
417
  "portrait-lg": { width: 720, height: 1280 },
407
- // Wide/Banner (OG, social)
408
418
  "wide": { width: 1200, height: 630 },
409
- // Open Graph standard
410
419
  "banner": { width: 1200, height: 400 },
411
- // Banner/header
412
420
  "banner-sm": { width: 800, height: 200 },
413
- // Classic photo ratios
414
421
  "photo-4x3": { width: 800, height: 600 },
415
- // 4:3
416
422
  "photo-3x2": { width: 900, height: 600 },
417
- // 3:2
418
- // Story/vertical (9:16)
419
423
  "story": { width: 1080, height: 1920 },
420
- // Video thumbnails
421
424
  "video": { width: 1280, height: 720 },
422
425
  "video-sm": { width: 640, height: 360 },
423
- // Card sizes
424
426
  "card-sm": { width: 300, height: 200 },
425
427
  "card": { width: 400, height: 300 },
426
428
  "card-lg": { width: 600, height: 400 }
427
429
  };
428
- var getImageSettings = (fileSizeInBytes, qualityPreset, display, size) => {
430
+ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size, fit, position) => {
429
431
  let baseQuality = 80;
430
432
  if (qualityPreset === "low") baseQuality = 30;
431
433
  else if (qualityPreset === "medium") baseQuality = 50;
@@ -434,8 +436,28 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size) => {
434
436
  const n = parseInt(qualityPreset, 10);
435
437
  if (!isNaN(n)) baseQuality = Math.min(100, Math.max(1, n));
436
438
  }
437
- const displayFactor = display && DISPLAY_PRESETS[display] ? DISPLAY_PRESETS[display] : 1;
438
- baseQuality = Math.round(baseQuality * displayFactor);
439
+ let width;
440
+ let height;
441
+ let qualityFactor = 1;
442
+ let defaultFit = "inside";
443
+ const displayPreset = display ? DISPLAY_PRESETS[display] : void 0;
444
+ if (displayPreset) {
445
+ qualityFactor = displayPreset.qualityFactor;
446
+ defaultFit = displayPreset.defaultFit;
447
+ const [ratioW, ratioH] = displayPreset.ratio;
448
+ const scale = size && SIZE_SCALES[size] ? SIZE_SCALES[size] : 1;
449
+ width = Math.round(displayPreset.baseWidth * scale);
450
+ height = Math.round(width * ratioH / ratioW);
451
+ } else if (size) {
452
+ const standalone = STANDALONE_SIZES[size];
453
+ if (standalone) {
454
+ width = standalone.width;
455
+ height = standalone.height;
456
+ }
457
+ }
458
+ const resolvedFit = fit && VALID_FIT_OPTIONS.includes(fit) ? fit : defaultFit;
459
+ const resolvedPosition = position && VALID_POSITION_OPTIONS.includes(position) ? position : void 0;
460
+ baseQuality = Math.round(baseQuality * qualityFactor);
439
461
  let quality = baseQuality;
440
462
  let effort = 4;
441
463
  let pngCompression = 6;
@@ -463,12 +485,12 @@ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size) => {
463
485
  pngCompression = 7;
464
486
  }
465
487
  }
466
- const dimensions = size && SIZE_PRESETS[size] ? SIZE_PRESETS[size] : void 0;
467
488
  return {
468
489
  quality: Math.max(1, Math.min(100, quality)),
469
490
  effort,
470
491
  pngCompression,
471
- ...dimensions && { width: dimensions.width, height: dimensions.height }
492
+ ...width && height && { width, height, fit: resolvedFit },
493
+ ...resolvedPosition && { position: resolvedPosition }
472
494
  };
473
495
  };
474
496
  var objectIdSchema = z.string().refine((val) => isValidObjectId(val), {
@@ -1547,9 +1569,9 @@ var driveUpload = async (source, key, options) => {
1547
1569
  }
1548
1570
  }
1549
1571
  let resolvedParentId = null;
1550
- if (options.folder?.path) {
1572
+ if (options.folder && "path" in options.folder) {
1551
1573
  resolvedParentId = await resolveFolderByPath(options.folder.path, key, accountId);
1552
- } else if (options.folder?.id && options.folder.id !== "root") {
1574
+ } else if (options.folder && "id" in options.folder && options.folder.id !== "root") {
1553
1575
  resolvedParentId = options.folder.id;
1554
1576
  } else if (options.parentId && options.parentId !== "root") {
1555
1577
  resolvedParentId = options.parentId;
@@ -1709,23 +1731,30 @@ var driveAPIHandler = async (req, res) => {
1709
1731
  const quality = req.query.quality;
1710
1732
  const display = req.query.display;
1711
1733
  const sizePreset = req.query.size;
1734
+ const fit = req.query.fit;
1735
+ const position = req.query.position;
1712
1736
  const isImage = mime.startsWith("image/");
1713
- const shouldTransform = isImage && (format || quality || display || sizePreset);
1737
+ const shouldTransform = isImage && (format || quality || display || sizePreset || fit);
1714
1738
  res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
1715
1739
  if (config.cors?.enabled) {
1716
1740
  res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
1717
1741
  }
1718
1742
  if (shouldTransform) {
1719
1743
  try {
1720
- const settings = getImageSettings(fileSize, quality, display, sizePreset);
1744
+ const settings = getImageSettings(fileSize, quality, display, sizePreset, fit, position);
1721
1745
  let targetFormat = format || mime.split("/")[1];
1722
1746
  if (targetFormat === "jpg") targetFormat = "jpeg";
1747
+ if (!["jpeg", "png", "webp", "avif"].includes(targetFormat)) {
1748
+ targetFormat = format || "webp";
1749
+ }
1723
1750
  const cacheDir = path.join(config.storage.path, "file", drive._id.toString(), "cache");
1724
1751
  const cacheKey = [
1725
1752
  "opt",
1726
1753
  `q${settings.quality}`,
1727
1754
  `e${settings.effort}`,
1728
1755
  settings.width ? `${settings.width}x${settings.height}` : "orig",
1756
+ settings.fit || "none",
1757
+ settings.position || "c",
1729
1758
  targetFormat
1730
1759
  ].join("_");
1731
1760
  const cachePath = path.join(cacheDir, `${cacheKey}.bin`);
@@ -1745,7 +1774,8 @@ var driveAPIHandler = async (req, res) => {
1745
1774
  let pipeline = sharp();
1746
1775
  if (settings.width && settings.height) {
1747
1776
  pipeline = pipeline.resize(settings.width, settings.height, {
1748
- fit: "inside",
1777
+ fit: settings.fit || "inside",
1778
+ position: settings.position || "center",
1749
1779
  withoutEnlargement: true
1750
1780
  });
1751
1781
  }
@@ -1756,13 +1786,17 @@ var driveAPIHandler = async (req, res) => {
1756
1786
  pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
1757
1787
  res.setHeader("Content-Type", "image/png");
1758
1788
  } else if (targetFormat === "webp") {
1759
- pipeline = pipeline.webp({ quality: settings.quality, effort: settings.effort });
1789
+ const webpEffort = Math.min(settings.effort, 6);
1790
+ pipeline = pipeline.webp({ quality: settings.quality, effort: webpEffort });
1760
1791
  res.setHeader("Content-Type", "image/webp");
1761
1792
  } else if (targetFormat === "avif") {
1762
1793
  pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
1763
1794
  res.setHeader("Content-Type", "image/avif");
1764
1795
  }
1765
1796
  res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1797
+ pipeline.on("error", (err) => {
1798
+ console.error("[next-drive] Pipeline error:", err);
1799
+ });
1766
1800
  stream.pipe(pipeline);
1767
1801
  pipeline.clone().toFile(cachePath).catch((e) => console.error("[next-drive] Cache write failed:", e));
1768
1802
  pipeline.clone().pipe(res);
@@ -2306,5 +2340,5 @@ var driveAPIHandler = async (req, res) => {
2306
2340
  };
2307
2341
 
2308
2342
  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
2343
+ //# sourceMappingURL=chunk-C5CORNPP.js.map
2344
+ //# sourceMappingURL=chunk-C5CORNPP.js.map