Browse Source

feat(asset-server-plugin): Support for specifying format in query param

Closes #482. This allows modern formats to be used even if the source image
files are jpegs etc.
Michael Bromley 3 years ago
parent
commit
5a0cbe61f3

+ 17 - 1
packages/asset-server-plugin/src/common.ts

@@ -1,7 +1,7 @@
 import { REQUEST_CONTEXT_KEY } from '@vendure/core/dist/common/constants';
 import { Request } from 'express';
 
-import { AssetServerOptions } from './types';
+import { AssetServerOptions, ImageTransformFormat } from './types';
 
 export function getAssetUrlPrefixFn(options: AssetServerOptions) {
     const { assetUrlPrefix, route } = options;
@@ -22,3 +22,19 @@ export function getAssetUrlPrefixFn(options: AssetServerOptions) {
     }
     throw new Error(`The assetUrlPrefix option was of an unexpected type: ${JSON.stringify(assetUrlPrefix)}`);
 }
+
+export function getValidFormat(format?: unknown): ImageTransformFormat | undefined {
+    if (typeof format !== 'string') {
+        return undefined;
+    }
+    switch (format) {
+        case 'jpg':
+        case 'jpeg':
+        case 'png':
+        case 'webp':
+        case 'avif':
+            return format;
+        default:
+            return undefined;
+    }
+}

+ 33 - 10
packages/asset-server-plugin/src/plugin.ts

@@ -15,6 +15,7 @@ import { fromBuffer } from 'file-type';
 import fs from 'fs-extra';
 import path from 'path';
 
+import { getValidFormat } from './common';
 import { loggerCtx } from './constants';
 import { defaultAssetStorageStrategyFactory } from './default-asset-storage-strategy-factory';
 import { HashedAssetNamingStrategy } from './hashed-asset-naming-strategy';
@@ -79,6 +80,22 @@ import { AssetServerOptions, ImageTransformPreset } from './types';
  *
  * `http://localhost:3000/assets/landscape.jpg?w=150&h=150&mode=crop&fpx=0.2&fpy=0.7`
  *
+ * ### Format
+ *
+ * Since v1.7.0, the image format can be specified by adding the `format` query parameter:
+ *
+ * `http://localhost:3000/assets/some-asset.jpg?format=webp`
+ *
+ * This means that, no matter the format of your original asset files, you can use more modern formats in your storefront if the browser
+ * supports them. Supported values for `format` are:
+ *
+ * * `jpeg` or `jpg`
+ * * `png`
+ * * `webp`
+ * * `avif`
+ *
+ * The `format` parameter can also be combined with presets (see below).
+ *
  * ### Transform presets
  *
  * Presets can be defined which allow a single preset name to be used instead of specifying the width, height and mode. Presets are
@@ -245,15 +262,19 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
                     const image = await transformImage(file, req.query as any, this.presets || []);
                     try {
                         const imageBuffer = await image.toBuffer();
+                        const cachedFileName = this.getFileNameFromRequest(req);
                         if (!req.query.cache || req.query.cache === 'true') {
-                            const cachedFileName = this.getFileNameFromRequest(req);
                             await AssetServerPlugin.assetStorage.writeFileFromBuffer(
                                 cachedFileName,
                                 imageBuffer,
                             );
                             Logger.debug(`Saved cached asset: ${cachedFileName}`, loggerCtx);
                         }
-                        res.set('Content-Type', `image/${(await image.metadata()).format}`);
+                        let mimeType = this.getMimeType(cachedFileName);
+                        if (!mimeType) {
+                            mimeType = (await fromBuffer(imageBuffer))?.mime || 'image/jpeg';
+                        }
+                        res.set('Content-Type', mimeType);
                         res.setHeader('content-security-policy', `default-src 'self'`);
                         res.send(imageBuffer);
                         return;
@@ -269,22 +290,23 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
     }
 
     private getFileNameFromRequest(req: Request): string {
-        const { w, h, mode, preset, fpx, fpy } = req.query;
+        const { w, h, mode, preset, fpx, fpy, format } = req.query;
         const focalPoint = fpx && fpy ? `_fpx${fpx}_fpy${fpy}` : '';
+        const imageFormat = getValidFormat(format);
         let imageParamHash: string | null = null;
         if (w || h) {
             const width = w || '';
             const height = h || '';
-            imageParamHash = this.md5(`_transform_w${width}_h${height}_m${mode}${focalPoint}`);
+            imageParamHash = this.md5(`_transform_w${width}_h${height}_m${mode}${focalPoint}${imageFormat}`);
         } else if (preset) {
             if (this.presets && !!this.presets.find(p => p.name === preset)) {
-                imageParamHash = this.md5(`_transform_pre_${preset}${focalPoint}`);
+                imageParamHash = this.md5(`_transform_pre_${preset}${focalPoint}${imageFormat}`);
             }
         }
 
         const decodedReqPath = decodeURIComponent(req.path);
         if (imageParamHash) {
-            return path.join(this.cacheDir, this.addSuffix(decodedReqPath, imageParamHash));
+            return path.join(this.cacheDir, this.addSuffix(decodedReqPath, imageParamHash, imageFormat));
         } else {
             return decodedReqPath;
         }
@@ -294,11 +316,12 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
         return createHash('md5').update(input).digest('hex');
     }
 
-    private addSuffix(fileName: string, suffix: string): string {
-        const ext = path.extname(fileName);
-        const baseName = path.basename(fileName, ext);
+    private addSuffix(fileName: string, suffix: string, ext?: string): string {
+        const originalExt = path.extname(fileName);
+        const effectiveExt = ext ? `.${ext}` : originalExt;
+        const baseName = path.basename(fileName, originalExt);
         const dirName = path.dirname(fileName);
-        return path.join(dirName, `${baseName}${suffix}${ext}`);
+        return path.join(dirName, `${baseName}${suffix}${effectiveExt}`);
     }
 
     /**

+ 20 - 1
packages/asset-server-plugin/src/transform-image.ts

@@ -1,6 +1,7 @@
 import sharp, { Region, ResizeOptions } from 'sharp';
 
-import { ImageTransformPreset } from './types';
+import { getValidFormat } from './common';
+import { ImageTransformFormat, ImageTransformPreset } from './types';
 
 export type Dimensions = { w: number; h: number };
 export type Point = { x: number; y: number };
@@ -18,6 +19,7 @@ export async function transformImage(
     let mode = queryParams.mode || 'crop';
     const fpx = +queryParams.fpx || undefined;
     const fpy = +queryParams.fpy || undefined;
+    const imageFormat = getValidFormat(queryParams.format);
     if (queryParams.preset) {
         const matchingPreset = presets.find(p => p.name === queryParams.preset);
         if (matchingPreset) {
@@ -34,6 +36,7 @@ export async function transformImage(
     }
 
     const image = sharp(originalImage);
+    applyFormat(image, imageFormat);
     if (fpx && fpy && targetWidth && targetHeight && mode === 'crop') {
         const metadata = await image.metadata();
         if (metadata.width && metadata.height) {
@@ -51,6 +54,22 @@ export async function transformImage(
     return image.resize(targetWidth, targetHeight, options);
 }
 
+function applyFormat(image: sharp.Sharp, format: ImageTransformFormat | undefined) {
+    switch (format) {
+        case 'jpg':
+        case 'jpeg':
+            return image.jpeg();
+        case 'png':
+            return image.png();
+        case 'webp':
+            return image.webp();
+        case 'avif':
+            return image.avif();
+        default:
+            return image;
+    }
+}
+
 /**
  * Resize an image but keep it centered on the focal point.
  * Based on the method outlined in https://github.com/lovell/sharp/issues/1198#issuecomment-384591756

+ 2 - 1
packages/asset-server-plugin/src/types.ts

@@ -5,6 +5,8 @@ import {
     RequestContext,
 } from '@vendure/core';
 
+export type ImageTransformFormat = 'jpg' | 'jpeg' | 'png' | 'webp' | 'avif';
+
 /**
  * @description
  * Specifies the way in which an asset preview image will be resized to fit in the
@@ -16,7 +18,6 @@ import {
  *
  * @docsCategory AssetServerPlugin
  */
-
 export type ImageTransformMode = 'crop' | 'resize';
 
 /**