Skip to content

Commit

Permalink
feat(asset-server-plugin): Add q query param for dynamic quality
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Mar 12, 2024
1 parent 0dc01bc commit b96289b
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 17 deletions.
45 changes: 35 additions & 10 deletions packages/asset-server-plugin/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from '@vendure/core';
import { createHash } from 'crypto';
import express, { NextFunction, Request, Response } from 'express';
import { fileTypeFromBuffer } from 'file-type';
import fs from 'fs-extra';
import path from 'path';

Expand All @@ -23,6 +22,11 @@ import { SharpAssetPreviewStrategy } from './sharp-asset-preview-strategy';
import { transformImage } from './transform-image';
import { AssetServerOptions, ImageTransformPreset } from './types';

async function getFileType(buffer: Buffer) {
const { fileTypeFromBuffer } = await import('file-type');
return fileTypeFromBuffer(buffer);
}

/**
* @description
* The `AssetServerPlugin` serves assets (images and other files) from the local file system, and can also be configured to use
Expand Down Expand Up @@ -96,6 +100,16 @@ import { AssetServerOptions, ImageTransformPreset } from './types';
*
* The `format` parameter can also be combined with presets (see below).
*
* ### Quality
*
* Since v2.2.0, the image quality can be specified by adding the `q` query parameter:
*
* `http://localhost:3000/assets/some-asset.jpg?q=75`
*
* This applies to the `jpg`, `webp` and `avif` formats. The default quality value for `jpg` and `webp` is 80, and for `avif` is 50.
*
* The `q` 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
Expand Down Expand Up @@ -244,7 +258,7 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
const file = await AssetServerPlugin.assetStorage.readFileToBuffer(key);
let mimeType = this.getMimeType(key);
if (!mimeType) {
mimeType = (await fileTypeFromBuffer(file))?.mime || 'application/octet-stream';
mimeType = (await getFileType(file))?.mime || 'application/octet-stream';
}
res.contentType(mimeType);
res.setHeader('content-security-policy', "default-src 'self'");
Expand Down Expand Up @@ -289,7 +303,7 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
}
let mimeType = this.getMimeType(cachedFileName);
if (!mimeType) {
mimeType = (await fileTypeFromBuffer(imageBuffer))?.mime || 'image/jpeg';
mimeType = (await getFileType(imageBuffer))?.mime || 'image/jpeg';
}
res.set('Content-Type', mimeType);
res.setHeader('content-security-policy', "default-src 'self'");
Expand All @@ -307,26 +321,37 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
}

private getFileNameFromRequest(req: Request): string {
const { w, h, mode, preset, fpx, fpy, format } = req.query;
const { w, h, mode, preset, fpx, fpy, format, q } = req.query;
/* eslint-disable @typescript-eslint/restrict-template-expressions */
const focalPoint = fpx && fpy ? `_fpx${fpx}_fpy${fpy}` : '';
const quality = q ? `_q${q}` : '';
const imageFormat = getValidFormat(format);
let imageParamHash: string | null = null;
let imageParamsString = '';
if (w || h) {
const width = w || '';
const height = h || '';
imageParamHash = this.md5(`_transform_w${width}_h${height}_m${mode}${focalPoint}${imageFormat}`);
imageParamsString = `_transform_w${width}_h${height}_m${mode}`;
} else if (preset) {
if (this.presets && !!this.presets.find(p => p.name === preset)) {
imageParamHash = this.md5(`_transform_pre_${preset}${focalPoint}${imageFormat}`);
imageParamsString = `_transform_pre_${preset}`;
}
} else if (imageFormat) {
imageParamHash = this.md5(`_transform_${imageFormat}`);
}

if (focalPoint) {
imageParamsString += focalPoint;
}
if (imageFormat) {
imageParamsString += imageFormat;
}
if (quality) {
imageParamsString += quality;
}

/* eslint-enable @typescript-eslint/restrict-template-expressions */

const decodedReqPath = decodeURIComponent(req.path);
if (imageParamHash) {
if (imageParamsString !== '') {
const imageParamHash = this.md5(imageParamsString);
return path.join(this.cacheDir, this.addSuffix(decodedReqPath, imageParamHash, imageFormat));
} else {
return decodedReqPath;
Expand Down
39 changes: 32 additions & 7 deletions packages/asset-server-plugin/src/transform-image.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import sharp, { Region, ResizeOptions } from 'sharp';
import { Logger } from '@vendure/core';
import sharp, { FormatEnum, Region, ResizeOptions } from 'sharp';

import { getValidFormat } from './common';
import { loggerCtx } from './constants';
import { ImageTransformFormat, ImageTransformPreset } from './types';

export type Dimensions = { w: number; h: number };
Expand All @@ -16,6 +18,8 @@ export async function transformImage(
): Promise<sharp.Sharp> {
let targetWidth = Math.round(+queryParams.w) || undefined;
let targetHeight = Math.round(+queryParams.h) || undefined;
const quality =
queryParams.q != null ? Math.round(Math.max(Math.min(+queryParams.q, 100), 1)) : undefined;
let mode = queryParams.mode || 'crop';
const fpx = +queryParams.fpx || undefined;
const fpy = +queryParams.fpy || undefined;
Expand All @@ -36,7 +40,11 @@ export async function transformImage(
}

const image = sharp(originalImage);
applyFormat(image, imageFormat);
try {
await applyFormat(image, imageFormat, quality);
} catch (e: any) {
Logger.error(e.message, loggerCtx, e.stack);
}
if (fpx && fpy && targetWidth && targetHeight && mode === 'crop') {
const metadata = await image.metadata();
if (metadata.width && metadata.height) {
Expand All @@ -54,22 +62,39 @@ export async function transformImage(
return image.resize(targetWidth, targetHeight, options);
}

function applyFormat(image: sharp.Sharp, format: ImageTransformFormat | undefined) {
async function applyFormat(
image: sharp.Sharp,
format: ImageTransformFormat | undefined,
quality: number | undefined,
) {
switch (format) {
case 'jpg':
case 'jpeg':
return image.jpeg();
return image.jpeg({ quality });
case 'png':
return image.png();
case 'webp':
return image.webp();
return image.webp({ quality });
case 'avif':
return image.avif();
default:
return image.avif({ quality });
default: {
if (quality) {
// If a quality has been specified but no format, we need to determine the format from the image
// and apply the quality to that format.
const metadata = await image.metadata();
if (isImageTransformFormat(metadata.format)) {
return applyFormat(image, metadata.format, quality);
}
}
return image;
}
}
}

function isImageTransformFormat(input: keyof FormatEnum | undefined): input is ImageTransformFormat {
return !!input && ['jpg', 'jpeg', 'webp', 'avif'].includes(input);
}

/**
* 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
Expand Down

0 comments on commit b96289b

Please sign in to comment.