@websolutespa/bom-mixer-image 3.0.1 → 4.0.0-next.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @websolutespa/bom-mixer-image
2
2
 
3
+ ## 4.0.0-next.1
4
+
5
+ ### Patch Changes
6
+
7
+ - AVIF is served to browsers that accept it (`IMAGE_AVIF=false` disables it); animated GIFs stay WebP. AVIF files are cached next to the WebP ones.
8
+ - Responses add `s-maxage` and `stale-while-revalidate` to `Cache-Control` and `Vary: Accept`.
9
+ - Sources carrying `v`, `version` or `updatedAt` are served `immutable` for one year.
10
+ - Updated dependencies
11
+ - @websolutespa/bom-core@4.0.0-next.1
12
+
13
+ ## 4.0.0-next.0
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies
18
+ - @websolutespa/bom-core@4.0.0-next.0
19
+
3
20
  ## 3.0.1
4
21
 
5
22
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ type IImageOptions = IImageResize & {
12
12
  search: string;
13
13
  url: string;
14
14
  };
15
+ type IImageFormat = 'avif' | 'webp';
15
16
  type IImageHandlerNextContext = {
16
17
  params: Promise<{
17
18
  slugs?: string[];
@@ -29,6 +30,16 @@ declare function imageGetHandler({
29
30
  sizes,
30
31
  quality
31
32
  }: IImageHandlerOptions): (request: NextRequest, context: IImageHandlerNextContext) => Promise<Response>;
33
+ /**
34
+ * Picks AVIF when the browser accepts it, WebP otherwise. Animated GIFs stay
35
+ * WebP: sharp does not encode animated AVIF.
36
+ */
37
+ declare function getImageFormat(accept: string | null | undefined, src: string): IImageFormat;
38
+ /**
39
+ * Cache headers for a processed image. Responses vary by `Accept` because the
40
+ * format is negotiated; versioned sources are immutable.
41
+ */
42
+ declare function getImageCacheHeaders(options: Pick<IImageOptions, 'url' | 'search'>, format: IImageFormat): Record<string, string>;
32
43
  //#endregion
33
- export { IImageHandlerOptions, IImageOptions, imageGetHandler, imageHandler };
44
+ export { IImageHandlerOptions, IImageOptions, getImageCacheHeaders, getImageFormat, imageGetHandler, imageHandler };
34
45
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/image/image.handler.ts"],"sourcesContent":[],"mappings":";;;;KAsBY,oBAAA;;EAAA,KAAA,EAAA,MAAA,EAAA;EAMA,OAAA,EAAA,MAAA;AAIV,CAAA;AAMc,KAVJ,aAAA,GAAgB,YAUA,GAAA;EAAA,MAAA,EAAA,MAAA;EAAA,MAC1B,EAAA,MAAA;EAAY,GACZ,EAAA,MAAA;CAAU;KANP,wBAAA,GAQF;EAAoB,MAAA,EAPb,OAOa,CAAA;IAsFP,KAAA,CAAA,EAAA,MAAe,EAAA;EAAA,CAAA,CAAA,GAAA;IAC7B,KAAA,CAAA,EAAA,MAAA,EAAA;EAAY,CAAA;CACF;AAET,iBA9Fa,YAAA,CA8Fb;EAAA,OAAA;EAAA,KAAA;EAAA;AAAA,CAAA,EA1FA,oBA0FA,CAAA,EAAA,GAAA;AACsB,iBALT,eAAA,CAKS;EAAA,OAAA;EAAA,KAAA;EAAA;AAAA,CAAA,EADtB,oBACsB,CAAA,EAAA,CAAA,OAAA,EAAA,WAAA,EAAA,OAAA,EAAsB,wBAAtB,EAAA,GAAiD,OAAjD,CAAyD,QAAzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/image/image.handler.ts"],"sourcesContent":[],"mappings":";;;;KA2BY,oBAAA;;EAAA,KAAA,EAAA,MAAA,EAAA;EAMA,OAAA,EAAA,MAAA;AAIV,CAAA;AAIG,KARO,aAAA,GAAgB,YAQC,GACnB;EAGM,MAAA,EAAA,MAAA;EAAY,MAAA,EAAA,MAAA;EAAA,GAC1B,EAAA,MAAA;CAAY;KAPT,YAAA,GASH,MAAA,GAAA,MAAA;KAPG,wBAAA,GAQF;EAAoB,MAAA,EAPb,OAOa,CAAA;IAwEP,KAAA,CAAA,EAAA,MAAe,EAAA;EAAA,CAAA,CAAA,GAAA;IAC7B,KAAA,CAAA,EAAA,MAAA,EAAA;EAAY,CAAA;CACF;AAET,iBAhFa,YAAA,CAgFb;EAAA,OAAA;EAAA,KAAA;EAAA;AAAA,CAAA,EA5EA,oBA4EA,CAAA,EAAA,GAAA;AACsB,iBALT,eAAA,CAKS;EAAA,OAAA;EAAA,KAAA;EAAA;AAAA,CAAA,EADtB,oBACsB,CAAA,EAAA,CAAA,OAAA,EAAA,WAAA,EAAA,OAAA,EAAsB,wBAAtB,EAAA,GAAiD,OAAjD,CAAyD,QAAzD,CAAA;;;;AAAwD;AAwDjE,iBAAA,cAAA,CAAgE,MAAY,EAAA,MAAA,GAAA,IAAA,GAAA,SAAA,EAAA,GAAA,EAAA,MAAA,CAAA,EAAZ,YAAY;AAmB5F;;;;AAA6F,iBAA7E,oBAAA,CAA6E,OAAA,EAA/C,IAA+C,CAA1C,aAA0C,EAAA,KAAA,GAAA,QAAA,CAAA,EAAA,MAAA,EAAA,YAAA,CAAA,EAAe,MAAf,CAAA,MAAA,EAAA,MAAA,CAAA"}
package/dist/index.js CHANGED
@@ -31,6 +31,9 @@ function errorHandler(error, response) {
31
31
  //#region src/image/image.handler.ts
32
32
  const IMAGE_STRATEGY = process.env.IMAGE_STRATEGY ? process.env.IMAGE_STRATEGY : ImageStrategy.Mixer;
33
33
  const IMAGE_MAX_AGE = 604800;
34
+ const IMAGE_IMMUTABLE_MAX_AGE = 31536e3;
35
+ const IMAGE_STALE_WHILE_REVALIDATE = 86400;
36
+ const IMAGE_AVIF = process.env.IMAGE_AVIF !== "false";
34
37
  const IMAGE_FOCAL_X = 50;
35
38
  const IMAGE_FOCAL_Y = 50;
36
39
  const IMAGE_QUALITY = 90;
@@ -51,12 +54,11 @@ function imageHandler({ domains = [], sizes = [], quality = IMAGE_QUALITY }) {
51
54
  } catch (error) {
52
55
  return response.status(400).json({ errors: [{ message: error }] });
53
56
  }
54
- const cachedName = urlToCachedName(imageOptions);
57
+ const format = getImageFormat(request.headers.accept, imageOptions.url);
58
+ const cachedName = urlToCachedName(imageOptions, format);
55
59
  if (fs.existsSync(cachedName)) {
56
60
  const cachedImage = await getCachedImage(cachedName);
57
- response.setHeader("Content-Type", "image/webp");
58
- response.setHeader("Expires", new Date(Date.now() + IMAGE_MAX_AGE * 1e3).toUTCString());
59
- response.setHeader("Cache-Control", `public, max-age=${IMAGE_MAX_AGE}`);
61
+ setImageHeaders(response, imageOptions, format);
60
62
  return response.send(cachedImage);
61
63
  }
62
64
  if (isInvalidReferer(request)) return response.status(401).send("you are not authorized to perform this action");
@@ -65,12 +67,10 @@ function imageHandler({ domains = [], sizes = [], quality = IMAGE_QUALITY }) {
65
67
  if (originalImageBuffer) {
66
68
  const originalImage = sharp(originalImageBuffer, { animated: isGIF(imageOptions.url) });
67
69
  const resizedImage = await resizeImage(originalImage, imageOptions);
68
- const compressedImage = resizedImage.webp({ quality: imageOptions.q });
70
+ const compressedImage = encodeImage(resizedImage, format, imageOptions.q);
69
71
  compressedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("imageHandler.sharp.error", cachedName, error));
70
72
  const data = await compressedImage.toBuffer();
71
- response.setHeader("Content-Type", "image/webp");
72
- response.setHeader("Expires", new Date(Date.now() + IMAGE_MAX_AGE * 1e3).toUTCString());
73
- response.setHeader("Cache-Control", `public, max-age=${IMAGE_MAX_AGE}`);
73
+ setImageHeaders(response, imageOptions, format);
74
74
  return response.end(data);
75
75
  }
76
76
  } catch (error) {
@@ -96,10 +96,11 @@ function imageGetHandler({ domains = [], sizes = [], quality = IMAGE_QUALITY })
96
96
  } catch (error) {
97
97
  return getJsonErrorResponse(400, error);
98
98
  }
99
- const cachedName = urlToCachedName(imageOptions);
99
+ const format = getImageFormat(request.headers.get("accept"), imageOptions.url);
100
+ const cachedName = urlToCachedName(imageOptions, format);
100
101
  if (fs.existsSync(cachedName)) {
101
102
  const cachedImage = await getCachedImage(cachedName);
102
- return getImageResponse(cachedImage);
103
+ return getImageResponse(cachedImage, imageOptions, format);
103
104
  }
104
105
  if (isInvalidRefererValue(request.headers.get("referer"))) return new Response("you are not authorized to perform this action", { status: 401 });
105
106
  if (imageOptions.url.indexOf("http") === -1 && isVercel()) return NextResponse.redirect(new URL(imageOptions.url, request.url), 307);
@@ -107,10 +108,10 @@ function imageGetHandler({ domains = [], sizes = [], quality = IMAGE_QUALITY })
107
108
  if (originalImageBuffer) {
108
109
  const originalImage = sharp(originalImageBuffer, { animated: isGIF(imageOptions.url) });
109
110
  const resizedImage = await resizeImage(originalImage, imageOptions);
110
- const compressedImage = resizedImage.webp({ quality: imageOptions.q });
111
+ const compressedImage = encodeImage(resizedImage, format, imageOptions.q);
111
112
  compressedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("imageHandler.sharp.error", cachedName, error));
112
113
  const data = await compressedImage.toBuffer();
113
- return getImageResponse(data);
114
+ return getImageResponse(data, imageOptions, format);
114
115
  }
115
116
  return getJsonErrorResponse(404, "image not found");
116
117
  } catch (error) {
@@ -122,6 +123,43 @@ function imageGetHandler({ domains = [], sizes = [], quality = IMAGE_QUALITY })
122
123
  function isGIF(src) {
123
124
  return src.indexOf(".gif") !== -1;
124
125
  }
126
+ /**
127
+
128
+ * Picks AVIF when the browser accepts it, WebP otherwise. Animated GIFs stay
129
+
130
+ * WebP: sharp does not encode animated AVIF.
131
+
132
+ */
133
+ function getImageFormat(accept, src) {
134
+ if (IMAGE_AVIF && !isGIF(src) && accept && accept.includes("image/avif")) return "avif";
135
+ return "webp";
136
+ }
137
+ function encodeImage(image, format, quality) {
138
+ return format === "avif" ? image.avif({ quality }) : image.webp({ quality });
139
+ }
140
+ function isVersionedSource(options) {
141
+ return /[?&](v|version|updatedAt)=/.test(options.search || "") || /[?&](v|version|updatedAt)=/.test(options.url || "");
142
+ }
143
+ /**
144
+
145
+ * Cache headers for a processed image. Responses vary by `Accept` because the
146
+
147
+ * format is negotiated; versioned sources are immutable.
148
+
149
+ */
150
+ function getImageCacheHeaders(options, format) {
151
+ const maxAge = isVersionedSource(options) ? IMAGE_IMMUTABLE_MAX_AGE : IMAGE_MAX_AGE;
152
+ const cacheControl = isVersionedSource(options) ? `public, max-age=${maxAge}, s-maxage=${maxAge}, immutable` : `public, max-age=${maxAge}, s-maxage=${maxAge}, stale-while-revalidate=${IMAGE_STALE_WHILE_REVALIDATE}`;
153
+ return {
154
+ "Cache-Control": cacheControl,
155
+ "Content-Type": `image/${format}`,
156
+ Expires: new Date(Date.now() + maxAge * 1e3).toUTCString(),
157
+ Vary: "Accept"
158
+ };
159
+ }
160
+ function setImageHeaders(response, options, format) {
161
+ Object.entries(getImageCacheHeaders(options, format)).forEach(([key, value]) => response.setHeader(key, value));
162
+ }
125
163
  function isInvalidReferer(request) {
126
164
  const { headers } = request;
127
165
  const referer = headers.referer;
@@ -276,12 +314,8 @@ function getOriginalImage(url, domains) {
276
314
  }
277
315
  });
278
316
  }
279
- function getImageResponse(data) {
280
- return new Response(data, { headers: {
281
- "Cache-Control": `public, max-age=${IMAGE_MAX_AGE}`,
282
- "Content-Type": "image/webp",
283
- Expires: new Date(Date.now() + IMAGE_MAX_AGE * 1e3).toUTCString()
284
- } });
317
+ function getImageResponse(data, options, format) {
318
+ return new Response(data, { headers: getImageCacheHeaders(options, format) });
285
319
  }
286
320
  function getJsonErrorResponse(status, message) {
287
321
  return Response.json({ errors: [{ message }] }, { status });
@@ -305,12 +339,13 @@ function urlToFileName(url) {
305
339
  function isVercel() {
306
340
  return process && process.env && String(process.env.VERCEL) === "1";
307
341
  }
308
- function urlToCachedName(options) {
342
+ function urlToCachedName(options, format) {
309
343
  const fileName = urlToFileName(options.url);
310
344
  const parsedName = path.parse(fileName);
311
345
  const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
312
346
  if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir);
313
- const cachedName = options.h ? path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${parsedName.ext}`) : path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);
347
+ const formatSuffix = format === "webp" ? "" : `_f_${format}`;
348
+ const cachedName = options.h ? path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${formatSuffix}${parsedName.ext}`) : path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${formatSuffix}${parsedName.ext}`);
314
349
  return cachedName;
315
350
  }
316
351
  function isInvalidDomain(src, domains) {
@@ -320,5 +355,5 @@ function isInvalidDomain(src, domains) {
320
355
  }
321
356
 
322
357
  //#endregion
323
- export { imageGetHandler, imageHandler };
358
+ export { getImageCacheHeaders, getImageFormat, imageGetHandler, imageHandler };
324
359
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["handler: IApiHandler","request: NextApiRequest","response: NextApiResponse","method: ICrudMethod","error: any","IMAGE_STRATEGY: ImageStrategy","request: NextApiRequest","response: NextApiResponse","imageOptions: IImageOptions","error: any","request: NextRequest","context: IImageHandlerNextContext","src: string","referer?: string | null","options: IImageHandlerOptions","query: Partial<Record<'origin' | 'search' | 'w' | 'h' | 'x' | 'y' | 'q', string | string[] | undefined>> & {\r\n slugs?: string[];\r\n }","query: Partial<Record<'origin' | 'search' | 't', string | string[] | undefined>> & {\r\n slugs?: string[];\r\n }","originalImage: sharp.Sharp","url: string","domains: string[]","data: Buffer<ArrayBufferLike>","status: number","message: unknown","pathname: string","path","options: IImageOptions"],"sources":["../src/api/api.handler.ts","../src/image/image.handler.ts"],"sourcesContent":["import { NextApiRequest, NextApiResponse } from 'next';\r\n\r\nexport type ICrudMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | string;\r\n\r\nexport type IHandler = (request: NextApiRequest, response: NextApiResponse) => Promise<any>;\r\n\r\nexport type IApiHandler = Record<ICrudMethod, IHandler>;\r\n\r\nexport function apiHandler(handler: IApiHandler): any {\r\n return async (request: NextApiRequest, response: NextApiResponse): Promise<any> => {\r\n const method: ICrudMethod = (request.method || 'GET').toLowerCase() as ICrudMethod;\r\n // check handler supports HTTP method\r\n if (!handler[method]) {\r\n return response.status(405).end(`Method ${method} Not Allowed`);\r\n }\r\n try {\r\n // global middleware\r\n // await jwtMiddleware(request, response);\r\n // route handler\r\n await handler[method](request, response);\r\n } catch (error) {\r\n // console.log('error', error);\r\n // global error handler\r\n return errorHandler(error, response);\r\n }\r\n };\r\n}\r\n\r\nexport function errorHandler(error: any, response: NextApiResponse): Promise<any> | void {\r\n if (typeof (error) === 'string') {\r\n // custom application error\r\n const is404 = error.toLowerCase().endsWith('not found');\r\n const statusCode = is404 ? 404 : 400;\r\n return response.status(statusCode).json({ message: error });\r\n }\r\n if (error.name === 'UnauthorizedError') {\r\n // jwt authentication error\r\n return response.status(401).json({ message: 'Invalid Token' });\r\n }\r\n // default to 500 server error\r\n console.error(error);\r\n return response.status(500).json({ message: error.message });\r\n}\r\n","// Importing the 'fs' module to work with the file system\r\nimport * as fs from 'fs';\r\n// Importing the 'NextApiRequest' and 'NextApiResponse' from 'next' for handling API requests and responses\r\nimport { NextApiRequest, NextApiResponse } from 'next';\r\nimport { NextRequest, NextResponse } from 'next/server';\r\n// Importing the 'path' module to work with file and directory paths\r\nimport * as path from 'path';\r\n// Importing 'sharp' for image processing\r\nimport sharp from 'sharp';\r\n// Importing 'apiHandler' from '../api/api.handler'\r\nimport { decryptImageResize, IImageResize, ImageStrategy } from '@websolutespa/bom-core';\r\nimport { apiHandler } from '../api/api.handler';\r\n\r\nconst IMAGE_STRATEGY: ImageStrategy = process.env.IMAGE_STRATEGY ?\r\n process.env.IMAGE_STRATEGY as ImageStrategy :\r\n ImageStrategy.Mixer;\r\n\r\nconst IMAGE_MAX_AGE = 604800;\r\nconst IMAGE_FOCAL_X = 50;\r\nconst IMAGE_FOCAL_Y = 50;\r\nconst IMAGE_QUALITY = 90;\r\n\r\nexport type IImageHandlerOptions = {\r\n domains: string[];\r\n sizes: number[];\r\n quality: number;\r\n};\r\n\r\nexport type IImageOptions = IImageResize & {\r\n origin: string;\r\n search: string;\r\n url: string;\r\n};\r\n\r\ntype IImageHandlerNextContext = {\r\n params: Promise<{ slugs?: string[] }> | { slugs?: string[] };\r\n};\r\n\r\nexport function imageHandler({\r\n domains = [],\r\n sizes = [],\r\n quality = IMAGE_QUALITY,\r\n}: IImageHandlerOptions) {\r\n // Returning the 'apiHandler' function with a configuration object\r\n return apiHandler({\r\n // Defining the 'get' method for handling GET requests\r\n get: async (request: NextApiRequest, response: NextApiResponse) => {\r\n try {\r\n let imageOptions: IImageOptions;\r\n try {\r\n imageOptions = (IMAGE_STRATEGY === ImageStrategy.Token ?\r\n getEncryptedImageOptions(request, { domains, sizes, quality }) :\r\n getImageOptions(request, { domains, sizes, quality }));\r\n } catch (error) {\r\n return response.status(400).json({\r\n errors: [{\r\n message: error,\r\n }],\r\n });\r\n }\r\n // Generating a cached name for the image\r\n const cachedName = urlToCachedName(imageOptions);\r\n // Checking if the cached image exists\r\n if (fs.existsSync(cachedName)) {\r\n // console.log('imageHandler.fromCache', cachedName);\r\n // Reading the cached image\r\n const cachedImage = await getCachedImage(cachedName);\r\n // Setting the 'Content-Type' header to 'image/webp'\r\n response.setHeader('Content-Type', 'image/webp');\r\n // Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)\r\n response.setHeader('Expires', new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString());\r\n // Setting the 'Cache-Control' header to 'public' and setting 'max-age' to the maximum age of the image\r\n response.setHeader('Cache-Control', `public, max-age=${IMAGE_MAX_AGE}`);\r\n // Ending the response and sending the cached image\r\n return response.send(cachedImage);\r\n }\r\n // Check referrer\r\n if (isInvalidReferer(request)) {\r\n return response.status(401).send('you are not authorized to perform this action');\r\n }\r\n // Temporarily fix Vercel local image issue\r\n if (imageOptions.url.indexOf('http') === -1 && isVercel()) {\r\n response.redirect(307, imageOptions.url);\r\n }\r\n // Reading the original image\r\n const originalImageBuffer = await getOriginalImage(imageOptions.url, domains);\r\n // Checking if the original image exists\r\n if (originalImageBuffer) {\r\n // Original image\r\n const originalImage = sharp(originalImageBuffer, {\r\n animated: isGIF(imageOptions.url),\r\n });\r\n // Resizing the original image and converting it to WebP format\r\n const resizedImage = await resizeImage(originalImage, imageOptions);\r\n // Converting resized image to WebP format\r\n const compressedImage = resizedImage\r\n .webp({\r\n quality: imageOptions.q,\r\n // nearLossless: true,\r\n });\r\n // Saving the resized image to the cache\r\n compressedImage.toFile(cachedName)\r\n .then(() => console.log('imageHandler.sharp.cached', cachedName))\r\n .catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));\r\n // Converting the resized image to a buffer\r\n const data = await compressedImage.toBuffer();\r\n // Setting the 'Content-Type' header to 'image/webp'\r\n response.setHeader('Content-Type', 'image/webp');\r\n // Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)\r\n response.setHeader('Expires', new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString());\r\n // Setting the 'Cache-Control' header to 'public' and setting 'max-age' to the maximum age of the image\r\n response.setHeader('Cache-Control', `public, max-age=${IMAGE_MAX_AGE}`);\r\n // Sending data in the response\r\n return response.end(data);\r\n }\r\n } catch (error: any) {\r\n // Logging an error message and throwing the error\r\n console.error('imageHandler.error', error);\r\n return response.status(error.status || 500).json({\r\n errors: [{\r\n message: error.message || error.name || error.statusText || 'An error occurred',\r\n }],\r\n });\r\n }\r\n },\r\n });\r\n}\r\n\r\nexport function imageGetHandler({\r\n domains = [],\r\n sizes = [],\r\n quality = IMAGE_QUALITY,\r\n}: IImageHandlerOptions) {\r\n return async (request: NextRequest, context: IImageHandlerNextContext): Promise<Response> => {\r\n try {\r\n let imageOptions: IImageOptions;\r\n try {\r\n imageOptions = (IMAGE_STRATEGY === ImageStrategy.Token ?\r\n await getEncryptedImageOptionsNext(request, context, { domains, sizes, quality }) :\r\n await getImageOptionsNext(request, context, { domains, sizes, quality }));\r\n } catch (error) {\r\n return getJsonErrorResponse(400, error);\r\n }\r\n const cachedName = urlToCachedName(imageOptions);\r\n if (fs.existsSync(cachedName)) {\r\n const cachedImage = await getCachedImage(cachedName);\r\n return getImageResponse(cachedImage);\r\n }\r\n if (isInvalidRefererValue(request.headers.get('referer'))) {\r\n return new Response('you are not authorized to perform this action', {\r\n status: 401,\r\n });\r\n }\r\n if (imageOptions.url.indexOf('http') === -1 && isVercel()) {\r\n return NextResponse.redirect(new URL(imageOptions.url, request.url), 307);\r\n }\r\n const originalImageBuffer = await getOriginalImage(imageOptions.url, domains);\r\n if (originalImageBuffer) {\r\n const originalImage = sharp(originalImageBuffer, {\r\n animated: isGIF(imageOptions.url),\r\n });\r\n const resizedImage = await resizeImage(originalImage, imageOptions);\r\n const compressedImage = resizedImage\r\n .webp({\r\n quality: imageOptions.q,\r\n });\r\n compressedImage.toFile(cachedName)\r\n .then(() => console.log('imageHandler.sharp.cached', cachedName))\r\n .catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));\r\n const data = await compressedImage.toBuffer();\r\n return getImageResponse(data);\r\n }\r\n return getJsonErrorResponse(404, 'image not found');\r\n } catch (error: any) {\r\n console.error('imageHandler.error', error);\r\n return getJsonErrorResponse(\r\n error.status || 500,\r\n error.message || error.name || error.statusText || 'An error occurred'\r\n );\r\n }\r\n };\r\n}\r\n\r\nfunction isGIF(src: string): boolean {\r\n return src.indexOf('.gif') !== -1;\r\n}\r\n\r\nfunction isInvalidReferer(request: NextApiRequest): boolean {\r\n const { headers } = request;\r\n const referer = headers.referer;\r\n return isInvalidRefererValue(referer);\r\n}\r\n\r\nfunction isInvalidRefererValue(referer?: string | null): boolean {\r\n const publicUrl = process.env.NEXT_PUBLIC_URL || '';\r\n // console.log('referef', referer, 'publicUrl', publicUrl, 'IMAGE_STRATEGY', IMAGE_STRATEGY, ImageStrategy.Token);\r\n if (\r\n IMAGE_STRATEGY !== ImageStrategy.Token &&\r\n referer &&\r\n publicUrl !== 'https://' &&\r\n referer.indexOf(publicUrl) !== 0\r\n ) {\r\n return true;\r\n }\r\n return false;\r\n}\r\n\r\nasync function getImageOptionsNext(\r\n request: NextRequest,\r\n context: IImageHandlerNextContext,\r\n options: IImageHandlerOptions\r\n): Promise<IImageOptions> {\r\n const params = await context.params;\r\n const query = Object.fromEntries(request.nextUrl.searchParams.entries());\r\n return getImageOptionsFromQuery({\r\n ...query,\r\n slugs: params.slugs,\r\n }, options);\r\n}\r\n\r\nfunction getImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {\r\n return getImageOptionsFromQuery(request.query, options);\r\n}\r\n\r\nfunction getImageOptionsFromQuery(\r\n query: Partial<Record<'origin' | 'search' | 'w' | 'h' | 'x' | 'y' | 'q', string | string[] | undefined>> & {\r\n slugs?: string[];\r\n },\r\n options: IImageHandlerOptions\r\n): IImageOptions {\r\n // Destructuring 'slugs', 'origin', 'search', 'w', 'h', 'x', 'y', 'q' from the request query\r\n const { slugs, origin = '', search = '', w, h, x, y, q } = query;\r\n // Checking slugs\r\n if (!Array.isArray(slugs) || slugs.length === 0) {\r\n // console.error('imageHandler', 'missing slugs parameter');\r\n throw 'missing slugs parameter';\r\n }\r\n // Parsing 'w' to an integer and assigning it to 'width'\r\n const width = typeof w === 'string' ? parseInt(w) : undefined;\r\n // Checking if 'width' is not defined\r\n if (!width) {\r\n // Logging an error message and returning a 400 status code with a message\r\n // console.error('imageHandler', 'missing w parameter');\r\n throw 'missing w parameter';\r\n }\r\n // if (width > 2048) {\r\n if (!options.sizes.includes(width)) {\r\n throw 'invalid w parameter';\r\n }\r\n // Parsing 'h' to an integer and assigning it to 'height'\r\n const height = typeof h === 'string' ? parseInt(h) : undefined;\r\n // Parsing 'x' to an integer and assigning it to 'focalX'\r\n const focalX = typeof x === 'string' ? parseInt(x) : IMAGE_FOCAL_X;\r\n // Parsing 'y' to an integer and assigning it to 'focalY'\r\n const focalY = typeof y === 'string' ? parseInt(y) : IMAGE_FOCAL_Y;\r\n // Parsing 'q' to an integer, ensuring it's between 30 and 100, and assigning it to 'imageQuality'\r\n const imageQuality = Math.max(30, Math.min(100, (typeof q === 'string' ? parseInt(q) : options.quality)));\r\n // if (width > 2048) {\r\n if (imageQuality !== options.quality) {\r\n throw 'invalid q parameter';\r\n }\r\n // Generating url\r\n const url = origin + '/' + slugs.join('/') + search;\r\n //\r\n return height ? {\r\n w: width,\r\n h: height,\r\n x: focalX,\r\n y: focalY,\r\n q: imageQuality,\r\n src: url,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n } : {\r\n w: width,\r\n q: imageQuality,\r\n src: url,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n };\r\n}\r\n\r\nasync function getEncryptedImageOptionsNext(\r\n request: NextRequest,\r\n context: IImageHandlerNextContext,\r\n options: IImageHandlerOptions\r\n): Promise<IImageOptions> {\r\n const params = await context.params;\r\n const query = Object.fromEntries(request.nextUrl.searchParams.entries());\r\n return getEncryptedImageOptionsFromQuery({\r\n ...query,\r\n slugs: params.slugs,\r\n }, options);\r\n}\r\n\r\nfunction getEncryptedImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {\r\n return getEncryptedImageOptionsFromQuery(request.query, options);\r\n}\r\n\r\nfunction getEncryptedImageOptionsFromQuery(\r\n query: Partial<Record<'origin' | 'search' | 't', string | string[] | undefined>> & {\r\n slugs?: string[];\r\n },\r\n options: IImageHandlerOptions\r\n): IImageOptions {\r\n // Destructuring 'slugs', 'origin', 'search', 't' from the request query\r\n const { slugs, origin = '', search = '', t } = query;\r\n // Checking slugs\r\n if (!Array.isArray(slugs) || slugs.length === 0) {\r\n // console.error('imageHandler', 'missing slugs parameter');\r\n throw 'missing slugs parameter';\r\n }\r\n if (typeof t !== 'string') {\r\n throw 'missing t parameter';\r\n }\r\n const imageResize = decryptImageResize(t);\r\n /*\r\n // Checking if 'w' is not defined\r\n if (!imageResize.w) {\r\n throw 'missing w parameter';\r\n }\r\n // if (width > 2048) {\r\n if (!options.sizes.includes(imageResize.w)) {\r\n throw 'invalid w parameter';\r\n }\r\n // Ensuring 'q' is between 30 and 100, and assigning it to 'imageQuality'\r\n const imageQuality = Math.max(30, Math.min(100, imageResize.q || options.quality));\r\n if (imageQuality !== options.quality) {\r\n throw 'invalid q parameter';\r\n }\r\n */\r\n imageResize.src = decodeURIComponent(imageResize.src);\r\n // Generating url\r\n const url = decodeURIComponent(origin + '/' + slugs.join('/') + search);\r\n //\r\n if (imageResize.src !== url) {\r\n throw `invalid src parameter ${imageResize.src} ${url}`;\r\n }\r\n //\r\n return {\r\n ...imageResize,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n };\r\n}\r\n\r\nasync function resizeImage(originalImage: sharp.Sharp, imageOptions: IImageOptions): Promise<sharp.Sharp> {\r\n const { w: resizeWidth, h: resizeHeight, x: focalX = IMAGE_FOCAL_X, y: focalY = IMAGE_FOCAL_Y } = imageOptions;\r\n const defaultResizeOptions = () => {\r\n return originalImage.resize({\r\n width: resizeWidth,\r\n height: resizeHeight ? resizeHeight : undefined,\r\n fit: sharp.fit.cover,\r\n position: sharp.strategy.attention,\r\n withoutEnlargement: true,\r\n });\r\n };\r\n if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {\r\n const { width: originalWidth, height: originalHeight } = await originalImage.metadata();\r\n if (originalWidth && originalHeight) {\r\n const resizeAspectRatio = resizeWidth / resizeHeight;\r\n const originalAspectRatio = originalWidth / originalHeight;\r\n const prioritizeHeight = resizeAspectRatio < originalAspectRatio;\r\n const imageToResize = originalImage.clone();\r\n // Scale the image up or down to fit the resize dimensions\r\n const scaledImage = imageToResize.resize(prioritizeHeight ? {\r\n height: resizeHeight,\r\n fit: sharp.fit.cover,\r\n } : {\r\n width: resizeWidth,\r\n fit: sharp.fit.cover,\r\n });\r\n const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true });\r\n // extractWidth\r\n const extractWidth = resizeWidth ?? scaledImageInfo.width;\r\n const maxOffsetX = scaledImageInfo.width - extractWidth;\r\n const leftFocalEdge = Math.round(\r\n scaledImageInfo.width * (focalX / 100) - extractWidth / 2\r\n );\r\n // extractX\r\n const extractX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX);\r\n // extractHeight\r\n const extractHeight = resizeHeight ?? scaledImageInfo.height;\r\n const maxOffsetY = scaledImageInfo.height - extractHeight;\r\n const topFocalEdge = Math.round(\r\n scaledImageInfo.height * (focalY / 100) - extractHeight / 2\r\n );\r\n // extractY\r\n const extractY = Math.min(Math.max(0, topFocalEdge), maxOffsetY);\r\n // Extract the focal area from the scaled image\r\n return scaledImage.extract({\r\n left: extractX,\r\n top: extractY,\r\n width: extractWidth,\r\n height: extractHeight,\r\n });\r\n } else {\r\n return defaultResizeOptions();\r\n }\r\n } else {\r\n return defaultResizeOptions();\r\n }\r\n}\r\n\r\n// Defining the 'getOriginalImage' function\r\nfunction getOriginalImage(url: string, domains: string[]): Promise<Buffer> {\r\n // Returning a new promise\r\n return new Promise((resolve, reject) => {\r\n // Checking if 'url' starts with 'http'\r\n if (url.indexOf('http') === 0) {\r\n if (isInvalidDomain(url, domains)) {\r\n return reject({ status: 401, message: 'invalid origin' });\r\n }\r\n // Fetching the image from the URL\r\n fetch(url, {\r\n method: 'GET',\r\n mode: 'cors',\r\n headers: {\r\n 'Access-Control-Allow-Origin': '*',\r\n },\r\n })\r\n .then(response => {\r\n if (!response.ok) {\r\n return reject({ status: response.status, message: response.statusText });\r\n }\r\n response.arrayBuffer().then(buffer => {\r\n return resolve(Buffer.from(buffer));\r\n }).catch(error => {\r\n return reject(error);\r\n });\r\n })\r\n .catch(error => {\r\n // Rejecting the promise with the error\r\n return reject(error);\r\n });\r\n } else {\r\n // Reading the image from the file system\r\n const filePath = path.join(process.cwd(), 'public', url);\r\n // console.log('imageHandler.getOriginalImage.filePath', filePath);\r\n fs.readFile(filePath, (error, data) => {\r\n if (error) {\r\n return reject(error);\r\n }\r\n // Resolving the promise with the image data\r\n return resolve(data);\r\n });\r\n }\r\n });\r\n}\r\n\r\nfunction getImageResponse(data: Buffer<ArrayBufferLike>): Response {\r\n return new Response(data as BodyInit, {\r\n headers: {\r\n 'Cache-Control': `public, max-age=${IMAGE_MAX_AGE}`,\r\n 'Content-Type': 'image/webp',\r\n Expires: new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString(),\r\n },\r\n });\r\n}\r\n\r\nfunction getJsonErrorResponse(status: number, message: unknown): Response {\r\n return Response.json({\r\n errors: [{\r\n message,\r\n }],\r\n }, {\r\n status,\r\n });\r\n}\r\n\r\n// Defining the 'getCachedImage' function\r\nfunction getCachedImage(pathname: string): Promise<Buffer> {\r\n // Returning a new promise\r\n return new Promise((resolve, reject) => {\r\n // Reading the image from the file system\r\n fs.readFile(pathname, (error, data) => {\r\n if (error) {\r\n // Rejecting the promise with the error\r\n return reject(error);\r\n }\r\n // Resolving the promise with the image data\r\n return resolve(data);\r\n });\r\n });\r\n}\r\n\r\n// Defining the 'urlToFileName' function\r\nfunction urlToFileName(url: string): string {\r\n // Replacing useless characters with UNDERSCORE\r\n const segments = url.split('?');\r\n const path = segments[0]!;\r\n const query = segments[1];\r\n let uniqueName = path.replace('://', '_').replace(/\\.|\\/|\\?|=/g, '_');\r\n // Replacing last UNDERSCORE with a DOT\r\n uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf('_'))\r\n + (query ? '_' + query.replace(/\\.|\\/|\\?|=/g, '_') : '')\r\n + '.' + uniqueName.substring(uniqueName.lastIndexOf('_') + 1, uniqueName.length);\r\n // Returning the unique name\r\n return uniqueName;\r\n}\r\n\r\nfunction isVercel(): boolean {\r\n return process && process.env && String(process.env.VERCEL) === '1';\r\n}\r\n\r\n// Defining the 'urlToCachedName' function\r\nfunction urlToCachedName(options: IImageOptions): string {\r\n // Converting the URL to a file name\r\n const fileName = urlToFileName(options.url);\r\n // Parsing the file name\r\n const parsedName = path.parse(fileName);\r\n // Defining the cache directory\r\n const cacheDir = isVercel() ?\r\n path.join('/tmp', '.cache') :\r\n path.join(process.cwd(), '.cache');\r\n // Checking if the cache directory exists\r\n if (!fs.existsSync(cacheDir)) {\r\n // Creating the cache directory\r\n fs.mkdirSync(cacheDir);\r\n }\r\n // Generating the cached name\r\n const cachedName = options.h ?\r\n path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${parsedName.ext}`) :\r\n path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);\r\n // Returning the cached name\r\n return cachedName;\r\n}\r\n\r\nfunction isInvalidDomain(src: string, domains: string[]): boolean {\r\n if (IMAGE_STRATEGY === ImageStrategy.Token) {\r\n return false;\r\n }\r\n const url = new URL(src);\r\n return !domains.includes(url.hostname);\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["handler: IApiHandler","request: NextApiRequest","response: NextApiResponse","method: ICrudMethod","error: any","IMAGE_STRATEGY: ImageStrategy","request: NextApiRequest","response: NextApiResponse","imageOptions: IImageOptions","error: any","request: NextRequest","context: IImageHandlerNextContext","src: string","accept: string | null | undefined","image: sharp.Sharp","format: IImageFormat","quality?: number","options: Pick<IImageOptions, 'url' | 'search'>","options: IImageOptions","referer?: string | null","options: IImageHandlerOptions","query: Partial<Record<'origin' | 'search' | 'w' | 'h' | 'x' | 'y' | 'q', string | string[] | undefined>> & {\r\n slugs?: string[];\r\n }","query: Partial<Record<'origin' | 'search' | 't', string | string[] | undefined>> & {\r\n slugs?: string[];\r\n }","originalImage: sharp.Sharp","url: string","domains: string[]","data: Buffer<ArrayBufferLike>","status: number","message: unknown","pathname: string","path"],"sources":["../src/api/api.handler.ts","../src/image/image.handler.ts"],"sourcesContent":["import { NextApiRequest, NextApiResponse } from 'next';\r\n\r\nexport type ICrudMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | string;\r\n\r\nexport type IHandler = (request: NextApiRequest, response: NextApiResponse) => Promise<any>;\r\n\r\nexport type IApiHandler = Record<ICrudMethod, IHandler>;\r\n\r\nexport function apiHandler(handler: IApiHandler): any {\r\n return async (request: NextApiRequest, response: NextApiResponse): Promise<any> => {\r\n const method: ICrudMethod = (request.method || 'GET').toLowerCase() as ICrudMethod;\r\n // check handler supports HTTP method\r\n if (!handler[method]) {\r\n return response.status(405).end(`Method ${method} Not Allowed`);\r\n }\r\n try {\r\n // global middleware\r\n // await jwtMiddleware(request, response);\r\n // route handler\r\n await handler[method](request, response);\r\n } catch (error) {\r\n // console.log('error', error);\r\n // global error handler\r\n return errorHandler(error, response);\r\n }\r\n };\r\n}\r\n\r\nexport function errorHandler(error: any, response: NextApiResponse): Promise<any> | void {\r\n if (typeof (error) === 'string') {\r\n // custom application error\r\n const is404 = error.toLowerCase().endsWith('not found');\r\n const statusCode = is404 ? 404 : 400;\r\n return response.status(statusCode).json({ message: error });\r\n }\r\n if (error.name === 'UnauthorizedError') {\r\n // jwt authentication error\r\n return response.status(401).json({ message: 'Invalid Token' });\r\n }\r\n // default to 500 server error\r\n console.error(error);\r\n return response.status(500).json({ message: error.message });\r\n}\r\n","// Importing the 'fs' module to work with the file system\r\nimport * as fs from 'fs';\r\n// Importing the 'NextApiRequest' and 'NextApiResponse' from 'next' for handling API requests and responses\r\nimport { NextApiRequest, NextApiResponse } from 'next';\r\nimport { NextRequest, NextResponse } from 'next/server';\r\n// Importing the 'path' module to work with file and directory paths\r\nimport * as path from 'path';\r\n// Importing 'sharp' for image processing\r\nimport sharp from 'sharp';\r\n// Importing 'apiHandler' from '../api/api.handler'\r\nimport { decryptImageResize, IImageResize, ImageStrategy } from '@websolutespa/bom-core';\r\nimport { apiHandler } from '../api/api.handler';\r\n\r\nconst IMAGE_STRATEGY: ImageStrategy = process.env.IMAGE_STRATEGY ?\r\n process.env.IMAGE_STRATEGY as ImageStrategy :\r\n ImageStrategy.Mixer;\r\n\r\nconst IMAGE_MAX_AGE = 604800;\r\n// Versioned sources (`?v=`, `?version=`, `?updatedAt=`) never change at the same URL.\r\nconst IMAGE_IMMUTABLE_MAX_AGE = 31536000;\r\nconst IMAGE_STALE_WHILE_REVALIDATE = 86400;\r\n// AVIF is served to browsers that accept it unless IMAGE_AVIF=false.\r\nconst IMAGE_AVIF = process.env.IMAGE_AVIF !== 'false';\r\nconst IMAGE_FOCAL_X = 50;\r\nconst IMAGE_FOCAL_Y = 50;\r\nconst IMAGE_QUALITY = 90;\r\n\r\nexport type IImageHandlerOptions = {\r\n domains: string[];\r\n sizes: number[];\r\n quality: number;\r\n};\r\n\r\nexport type IImageOptions = IImageResize & {\r\n origin: string;\r\n search: string;\r\n url: string;\r\n};\r\n\r\ntype IImageFormat = 'avif' | 'webp';\r\n\r\ntype IImageHandlerNextContext = {\r\n params: Promise<{ slugs?: string[] }> | { slugs?: string[] };\r\n};\r\n\r\nexport function imageHandler({\r\n domains = [],\r\n sizes = [],\r\n quality = IMAGE_QUALITY,\r\n}: IImageHandlerOptions) {\r\n // Returning the 'apiHandler' function with a configuration object\r\n return apiHandler({\r\n // Defining the 'get' method for handling GET requests\r\n get: async (request: NextApiRequest, response: NextApiResponse) => {\r\n try {\r\n let imageOptions: IImageOptions;\r\n try {\r\n imageOptions = (IMAGE_STRATEGY === ImageStrategy.Token ?\r\n getEncryptedImageOptions(request, { domains, sizes, quality }) :\r\n getImageOptions(request, { domains, sizes, quality }));\r\n } catch (error) {\r\n return response.status(400).json({\r\n errors: [{\r\n message: error,\r\n }],\r\n });\r\n }\r\n const format = getImageFormat(request.headers.accept, imageOptions.url);\r\n // Generating a cached name for the image\r\n const cachedName = urlToCachedName(imageOptions, format);\r\n // Checking if the cached image exists\r\n if (fs.existsSync(cachedName)) {\r\n // Reading the cached image\r\n const cachedImage = await getCachedImage(cachedName);\r\n setImageHeaders(response, imageOptions, format);\r\n // Ending the response and sending the cached image\r\n return response.send(cachedImage);\r\n }\r\n // Check referrer\r\n if (isInvalidReferer(request)) {\r\n return response.status(401).send('you are not authorized to perform this action');\r\n }\r\n // Temporarily fix Vercel local image issue\r\n if (imageOptions.url.indexOf('http') === -1 && isVercel()) {\r\n response.redirect(307, imageOptions.url);\r\n }\r\n // Reading the original image\r\n const originalImageBuffer = await getOriginalImage(imageOptions.url, domains);\r\n // Checking if the original image exists\r\n if (originalImageBuffer) {\r\n // Original image\r\n const originalImage = sharp(originalImageBuffer, {\r\n animated: isGIF(imageOptions.url),\r\n });\r\n // Resizing the original image\r\n const resizedImage = await resizeImage(originalImage, imageOptions);\r\n // Converting the resized image to the negotiated format\r\n const compressedImage = encodeImage(resizedImage, format, imageOptions.q);\r\n // Saving the resized image to the cache\r\n compressedImage.toFile(cachedName)\r\n .then(() => console.log('imageHandler.sharp.cached', cachedName))\r\n .catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));\r\n // Converting the resized image to a buffer\r\n const data = await compressedImage.toBuffer();\r\n setImageHeaders(response, imageOptions, format);\r\n // Sending data in the response\r\n return response.end(data);\r\n }\r\n } catch (error: any) {\r\n // Logging an error message and throwing the error\r\n console.error('imageHandler.error', error);\r\n return response.status(error.status || 500).json({\r\n errors: [{\r\n message: error.message || error.name || error.statusText || 'An error occurred',\r\n }],\r\n });\r\n }\r\n },\r\n });\r\n}\r\n\r\nexport function imageGetHandler({\r\n domains = [],\r\n sizes = [],\r\n quality = IMAGE_QUALITY,\r\n}: IImageHandlerOptions) {\r\n return async (request: NextRequest, context: IImageHandlerNextContext): Promise<Response> => {\r\n try {\r\n let imageOptions: IImageOptions;\r\n try {\r\n imageOptions = (IMAGE_STRATEGY === ImageStrategy.Token ?\r\n await getEncryptedImageOptionsNext(request, context, { domains, sizes, quality }) :\r\n await getImageOptionsNext(request, context, { domains, sizes, quality }));\r\n } catch (error) {\r\n return getJsonErrorResponse(400, error);\r\n }\r\n const format = getImageFormat(request.headers.get('accept'), imageOptions.url);\r\n const cachedName = urlToCachedName(imageOptions, format);\r\n if (fs.existsSync(cachedName)) {\r\n const cachedImage = await getCachedImage(cachedName);\r\n return getImageResponse(cachedImage, imageOptions, format);\r\n }\r\n if (isInvalidRefererValue(request.headers.get('referer'))) {\r\n return new Response('you are not authorized to perform this action', {\r\n status: 401,\r\n });\r\n }\r\n if (imageOptions.url.indexOf('http') === -1 && isVercel()) {\r\n return NextResponse.redirect(new URL(imageOptions.url, request.url), 307);\r\n }\r\n const originalImageBuffer = await getOriginalImage(imageOptions.url, domains);\r\n if (originalImageBuffer) {\r\n const originalImage = sharp(originalImageBuffer, {\r\n animated: isGIF(imageOptions.url),\r\n });\r\n const resizedImage = await resizeImage(originalImage, imageOptions);\r\n const compressedImage = encodeImage(resizedImage, format, imageOptions.q);\r\n compressedImage.toFile(cachedName)\r\n .then(() => console.log('imageHandler.sharp.cached', cachedName))\r\n .catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));\r\n const data = await compressedImage.toBuffer();\r\n return getImageResponse(data, imageOptions, format);\r\n }\r\n return getJsonErrorResponse(404, 'image not found');\r\n } catch (error: any) {\r\n console.error('imageHandler.error', error);\r\n return getJsonErrorResponse(\r\n error.status || 500,\r\n error.message || error.name || error.statusText || 'An error occurred'\r\n );\r\n }\r\n };\r\n}\r\n\r\nfunction isGIF(src: string): boolean {\r\n return src.indexOf('.gif') !== -1;\r\n}\r\n\r\n/**\r\n * Picks AVIF when the browser accepts it, WebP otherwise. Animated GIFs stay\r\n * WebP: sharp does not encode animated AVIF.\r\n */\r\nexport function getImageFormat(accept: string | null | undefined, src: string): IImageFormat {\r\n if (IMAGE_AVIF && !isGIF(src) && accept && accept.includes('image/avif')) {\r\n return 'avif';\r\n }\r\n return 'webp';\r\n}\r\n\r\nfunction encodeImage(image: sharp.Sharp, format: IImageFormat, quality?: number): sharp.Sharp {\r\n return format === 'avif' ? image.avif({ quality }) : image.webp({ quality });\r\n}\r\n\r\nfunction isVersionedSource(options: Pick<IImageOptions, 'url' | 'search'>): boolean {\r\n return /[?&](v|version|updatedAt)=/.test(options.search || '') || /[?&](v|version|updatedAt)=/.test(options.url || '');\r\n}\r\n\r\n/**\r\n * Cache headers for a processed image. Responses vary by `Accept` because the\r\n * format is negotiated; versioned sources are immutable.\r\n */\r\nexport function getImageCacheHeaders(options: Pick<IImageOptions, 'url' | 'search'>, format: IImageFormat): Record<string, string> {\r\n const maxAge = isVersionedSource(options) ? IMAGE_IMMUTABLE_MAX_AGE : IMAGE_MAX_AGE;\r\n const cacheControl = isVersionedSource(options) ?\r\n `public, max-age=${maxAge}, s-maxage=${maxAge}, immutable` :\r\n `public, max-age=${maxAge}, s-maxage=${maxAge}, stale-while-revalidate=${IMAGE_STALE_WHILE_REVALIDATE}`;\r\n return {\r\n 'Cache-Control': cacheControl,\r\n 'Content-Type': `image/${format}`,\r\n Expires: new Date(Date.now() + maxAge * 1000).toUTCString(),\r\n Vary: 'Accept',\r\n };\r\n}\r\n\r\nfunction setImageHeaders(response: NextApiResponse, options: IImageOptions, format: IImageFormat) {\r\n Object.entries(getImageCacheHeaders(options, format)).forEach(([key, value]) => response.setHeader(key, value));\r\n}\r\n\r\nfunction isInvalidReferer(request: NextApiRequest): boolean {\r\n const { headers } = request;\r\n const referer = headers.referer;\r\n return isInvalidRefererValue(referer);\r\n}\r\n\r\nfunction isInvalidRefererValue(referer?: string | null): boolean {\r\n const publicUrl = process.env.NEXT_PUBLIC_URL || '';\r\n // console.log('referef', referer, 'publicUrl', publicUrl, 'IMAGE_STRATEGY', IMAGE_STRATEGY, ImageStrategy.Token);\r\n if (\r\n IMAGE_STRATEGY !== ImageStrategy.Token &&\r\n referer &&\r\n publicUrl !== 'https://' &&\r\n referer.indexOf(publicUrl) !== 0\r\n ) {\r\n return true;\r\n }\r\n return false;\r\n}\r\n\r\nasync function getImageOptionsNext(\r\n request: NextRequest,\r\n context: IImageHandlerNextContext,\r\n options: IImageHandlerOptions\r\n): Promise<IImageOptions> {\r\n const params = await context.params;\r\n const query = Object.fromEntries(request.nextUrl.searchParams.entries());\r\n return getImageOptionsFromQuery({\r\n ...query,\r\n slugs: params.slugs,\r\n }, options);\r\n}\r\n\r\nfunction getImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {\r\n return getImageOptionsFromQuery(request.query, options);\r\n}\r\n\r\nfunction getImageOptionsFromQuery(\r\n query: Partial<Record<'origin' | 'search' | 'w' | 'h' | 'x' | 'y' | 'q', string | string[] | undefined>> & {\r\n slugs?: string[];\r\n },\r\n options: IImageHandlerOptions\r\n): IImageOptions {\r\n // Destructuring 'slugs', 'origin', 'search', 'w', 'h', 'x', 'y', 'q' from the request query\r\n const { slugs, origin = '', search = '', w, h, x, y, q } = query;\r\n // Checking slugs\r\n if (!Array.isArray(slugs) || slugs.length === 0) {\r\n // console.error('imageHandler', 'missing slugs parameter');\r\n throw 'missing slugs parameter';\r\n }\r\n // Parsing 'w' to an integer and assigning it to 'width'\r\n const width = typeof w === 'string' ? parseInt(w) : undefined;\r\n // Checking if 'width' is not defined\r\n if (!width) {\r\n // Logging an error message and returning a 400 status code with a message\r\n // console.error('imageHandler', 'missing w parameter');\r\n throw 'missing w parameter';\r\n }\r\n // if (width > 2048) {\r\n if (!options.sizes.includes(width)) {\r\n throw 'invalid w parameter';\r\n }\r\n // Parsing 'h' to an integer and assigning it to 'height'\r\n const height = typeof h === 'string' ? parseInt(h) : undefined;\r\n // Parsing 'x' to an integer and assigning it to 'focalX'\r\n const focalX = typeof x === 'string' ? parseInt(x) : IMAGE_FOCAL_X;\r\n // Parsing 'y' to an integer and assigning it to 'focalY'\r\n const focalY = typeof y === 'string' ? parseInt(y) : IMAGE_FOCAL_Y;\r\n // Parsing 'q' to an integer, ensuring it's between 30 and 100, and assigning it to 'imageQuality'\r\n const imageQuality = Math.max(30, Math.min(100, (typeof q === 'string' ? parseInt(q) : options.quality)));\r\n // if (width > 2048) {\r\n if (imageQuality !== options.quality) {\r\n throw 'invalid q parameter';\r\n }\r\n // Generating url\r\n const url = origin + '/' + slugs.join('/') + search;\r\n //\r\n return height ? {\r\n w: width,\r\n h: height,\r\n x: focalX,\r\n y: focalY,\r\n q: imageQuality,\r\n src: url,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n } : {\r\n w: width,\r\n q: imageQuality,\r\n src: url,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n };\r\n}\r\n\r\nasync function getEncryptedImageOptionsNext(\r\n request: NextRequest,\r\n context: IImageHandlerNextContext,\r\n options: IImageHandlerOptions\r\n): Promise<IImageOptions> {\r\n const params = await context.params;\r\n const query = Object.fromEntries(request.nextUrl.searchParams.entries());\r\n return getEncryptedImageOptionsFromQuery({\r\n ...query,\r\n slugs: params.slugs,\r\n }, options);\r\n}\r\n\r\nfunction getEncryptedImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {\r\n return getEncryptedImageOptionsFromQuery(request.query, options);\r\n}\r\n\r\nfunction getEncryptedImageOptionsFromQuery(\r\n query: Partial<Record<'origin' | 'search' | 't', string | string[] | undefined>> & {\r\n slugs?: string[];\r\n },\r\n options: IImageHandlerOptions\r\n): IImageOptions {\r\n // Destructuring 'slugs', 'origin', 'search', 't' from the request query\r\n const { slugs, origin = '', search = '', t } = query;\r\n // Checking slugs\r\n if (!Array.isArray(slugs) || slugs.length === 0) {\r\n // console.error('imageHandler', 'missing slugs parameter');\r\n throw 'missing slugs parameter';\r\n }\r\n if (typeof t !== 'string') {\r\n throw 'missing t parameter';\r\n }\r\n const imageResize = decryptImageResize(t);\r\n /*\r\n // Checking if 'w' is not defined\r\n if (!imageResize.w) {\r\n throw 'missing w parameter';\r\n }\r\n // if (width > 2048) {\r\n if (!options.sizes.includes(imageResize.w)) {\r\n throw 'invalid w parameter';\r\n }\r\n // Ensuring 'q' is between 30 and 100, and assigning it to 'imageQuality'\r\n const imageQuality = Math.max(30, Math.min(100, imageResize.q || options.quality));\r\n if (imageQuality !== options.quality) {\r\n throw 'invalid q parameter';\r\n }\r\n */\r\n imageResize.src = decodeURIComponent(imageResize.src);\r\n // Generating url\r\n const url = decodeURIComponent(origin + '/' + slugs.join('/') + search);\r\n //\r\n if (imageResize.src !== url) {\r\n throw `invalid src parameter ${imageResize.src} ${url}`;\r\n }\r\n //\r\n return {\r\n ...imageResize,\r\n url: url,\r\n origin: String(origin),\r\n search: String(search),\r\n };\r\n}\r\n\r\nasync function resizeImage(originalImage: sharp.Sharp, imageOptions: IImageOptions): Promise<sharp.Sharp> {\r\n const { w: resizeWidth, h: resizeHeight, x: focalX = IMAGE_FOCAL_X, y: focalY = IMAGE_FOCAL_Y } = imageOptions;\r\n const defaultResizeOptions = () => {\r\n return originalImage.resize({\r\n width: resizeWidth,\r\n height: resizeHeight ? resizeHeight : undefined,\r\n fit: sharp.fit.cover,\r\n position: sharp.strategy.attention,\r\n withoutEnlargement: true,\r\n });\r\n };\r\n if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {\r\n const { width: originalWidth, height: originalHeight } = await originalImage.metadata();\r\n if (originalWidth && originalHeight) {\r\n const resizeAspectRatio = resizeWidth / resizeHeight;\r\n const originalAspectRatio = originalWidth / originalHeight;\r\n const prioritizeHeight = resizeAspectRatio < originalAspectRatio;\r\n const imageToResize = originalImage.clone();\r\n // Scale the image up or down to fit the resize dimensions\r\n const scaledImage = imageToResize.resize(prioritizeHeight ? {\r\n height: resizeHeight,\r\n fit: sharp.fit.cover,\r\n } : {\r\n width: resizeWidth,\r\n fit: sharp.fit.cover,\r\n });\r\n const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true });\r\n // extractWidth\r\n const extractWidth = resizeWidth ?? scaledImageInfo.width;\r\n const maxOffsetX = scaledImageInfo.width - extractWidth;\r\n const leftFocalEdge = Math.round(\r\n scaledImageInfo.width * (focalX / 100) - extractWidth / 2\r\n );\r\n // extractX\r\n const extractX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX);\r\n // extractHeight\r\n const extractHeight = resizeHeight ?? scaledImageInfo.height;\r\n const maxOffsetY = scaledImageInfo.height - extractHeight;\r\n const topFocalEdge = Math.round(\r\n scaledImageInfo.height * (focalY / 100) - extractHeight / 2\r\n );\r\n // extractY\r\n const extractY = Math.min(Math.max(0, topFocalEdge), maxOffsetY);\r\n // Extract the focal area from the scaled image\r\n return scaledImage.extract({\r\n left: extractX,\r\n top: extractY,\r\n width: extractWidth,\r\n height: extractHeight,\r\n });\r\n } else {\r\n return defaultResizeOptions();\r\n }\r\n } else {\r\n return defaultResizeOptions();\r\n }\r\n}\r\n\r\n// Defining the 'getOriginalImage' function\r\nfunction getOriginalImage(url: string, domains: string[]): Promise<Buffer> {\r\n // Returning a new promise\r\n return new Promise((resolve, reject) => {\r\n // Checking if 'url' starts with 'http'\r\n if (url.indexOf('http') === 0) {\r\n if (isInvalidDomain(url, domains)) {\r\n return reject({ status: 401, message: 'invalid origin' });\r\n }\r\n // Fetching the image from the URL\r\n fetch(url, {\r\n method: 'GET',\r\n mode: 'cors',\r\n headers: {\r\n 'Access-Control-Allow-Origin': '*',\r\n },\r\n })\r\n .then(response => {\r\n if (!response.ok) {\r\n return reject({ status: response.status, message: response.statusText });\r\n }\r\n response.arrayBuffer().then(buffer => {\r\n return resolve(Buffer.from(buffer));\r\n }).catch(error => {\r\n return reject(error);\r\n });\r\n })\r\n .catch(error => {\r\n // Rejecting the promise with the error\r\n return reject(error);\r\n });\r\n } else {\r\n // Reading the image from the file system\r\n const filePath = path.join(process.cwd(), 'public', url);\r\n // console.log('imageHandler.getOriginalImage.filePath', filePath);\r\n fs.readFile(filePath, (error, data) => {\r\n if (error) {\r\n return reject(error);\r\n }\r\n // Resolving the promise with the image data\r\n return resolve(data);\r\n });\r\n }\r\n });\r\n}\r\n\r\nfunction getImageResponse(data: Buffer<ArrayBufferLike>, options: IImageOptions, format: IImageFormat): Response {\r\n return new Response(data as BodyInit, {\r\n headers: getImageCacheHeaders(options, format),\r\n });\r\n}\r\n\r\nfunction getJsonErrorResponse(status: number, message: unknown): Response {\r\n return Response.json({\r\n errors: [{\r\n message,\r\n }],\r\n }, {\r\n status,\r\n });\r\n}\r\n\r\n// Defining the 'getCachedImage' function\r\nfunction getCachedImage(pathname: string): Promise<Buffer> {\r\n // Returning a new promise\r\n return new Promise((resolve, reject) => {\r\n // Reading the image from the file system\r\n fs.readFile(pathname, (error, data) => {\r\n if (error) {\r\n // Rejecting the promise with the error\r\n return reject(error);\r\n }\r\n // Resolving the promise with the image data\r\n return resolve(data);\r\n });\r\n });\r\n}\r\n\r\n// Defining the 'urlToFileName' function\r\nfunction urlToFileName(url: string): string {\r\n // Replacing useless characters with UNDERSCORE\r\n const segments = url.split('?');\r\n const path = segments[0]!;\r\n const query = segments[1];\r\n let uniqueName = path.replace('://', '_').replace(/\\.|\\/|\\?|=/g, '_');\r\n // Replacing last UNDERSCORE with a DOT\r\n uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf('_'))\r\n + (query ? '_' + query.replace(/\\.|\\/|\\?|=/g, '_') : '')\r\n + '.' + uniqueName.substring(uniqueName.lastIndexOf('_') + 1, uniqueName.length);\r\n // Returning the unique name\r\n return uniqueName;\r\n}\r\n\r\nfunction isVercel(): boolean {\r\n return process && process.env && String(process.env.VERCEL) === '1';\r\n}\r\n\r\n// Defining the 'urlToCachedName' function\r\nfunction urlToCachedName(options: IImageOptions, format: IImageFormat): string {\r\n // Converting the URL to a file name\r\n const fileName = urlToFileName(options.url);\r\n // Parsing the file name\r\n const parsedName = path.parse(fileName);\r\n // Defining the cache directory\r\n const cacheDir = isVercel() ?\r\n path.join('/tmp', '.cache') :\r\n path.join(process.cwd(), '.cache');\r\n // Checking if the cache directory exists\r\n if (!fs.existsSync(cacheDir)) {\r\n // Creating the cache directory\r\n fs.mkdirSync(cacheDir);\r\n }\r\n // Generating the cached name; WebP keeps the historical names so existing caches stay valid\r\n const formatSuffix = format === 'webp' ? '' : `_f_${format}`;\r\n const cachedName = options.h ?\r\n path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${formatSuffix}${parsedName.ext}`) :\r\n path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${formatSuffix}${parsedName.ext}`);\r\n // Returning the cached name\r\n return cachedName;\r\n}\r\n\r\nfunction isInvalidDomain(src: string, domains: string[]): boolean {\r\n if (IMAGE_STRATEGY === ImageStrategy.Token) {\r\n return false;\r\n }\r\n const url = new URL(src);\r\n return !domains.includes(url.hostname);\r\n}\r\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@websolutespa/bom-mixer-image",
3
- "version": "3.0.1",
3
+ "version": "4.0.0-next.1",
4
4
  "description": "Mixer Image module of the BOM Repository",
5
5
  "keywords": [
6
6
  "bom",
@@ -25,15 +25,15 @@
25
25
  },
26
26
  "dependencies": {},
27
27
  "peerDependencies": {
28
- "@websolutespa/bom-core": "^3.0.1",
28
+ "@websolutespa/bom-core": "^4.0.0-next.1",
29
29
  "next": "16.1.6",
30
30
  "sharp": "0.34.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/sharp": "^0.30.4",
34
- "@websolutespa/bom-cli": "^3.0.1",
34
+ "@websolutespa/bom-cli": "^4.0.0-next.0",
35
35
  "@websolutespa/eslint-config": "^3.0.0",
36
- "@websolutespa/test": "^3.0.1",
36
+ "@websolutespa/test": "^4.0.0-next.0",
37
37
  "@websolutespa/tsconfig": "^3.0.0",
38
38
  "eslint": "9.31.0",
39
39
  "npm-run-all": "4.1.5",
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getImageCacheHeaders, getImageFormat } from '../image/image.handler';
3
+
4
+ describe('getImageFormat', () => {
5
+ it('serves AVIF only to browsers that accept it', () => {
6
+ expect(getImageFormat('image/avif,image/webp,*/*', '/media/a.jpg')).toBe('avif');
7
+ expect(getImageFormat('image/webp,*/*', '/media/a.jpg')).toBe('webp');
8
+ expect(getImageFormat(null, '/media/a.jpg')).toBe('webp');
9
+ });
10
+
11
+ it('keeps animated GIFs in WebP', () => {
12
+ expect(getImageFormat('image/avif', '/media/a.gif')).toBe('webp');
13
+ });
14
+ });
15
+
16
+ describe('getImageCacheHeaders', () => {
17
+ it('adds a shared cache lifetime and varies by Accept', () => {
18
+ const headers = getImageCacheHeaders({ url: '/media/a.jpg', search: '' }, 'webp');
19
+
20
+ expect(headers['Cache-Control']).toBe('public, max-age=604800, s-maxage=604800, stale-while-revalidate=86400');
21
+ expect(headers['Content-Type']).toBe('image/webp');
22
+ expect(headers.Vary).toBe('Accept');
23
+ });
24
+
25
+ it('marks versioned sources immutable', () => {
26
+ const headers = getImageCacheHeaders({ url: '/media/a.jpg?v=2026-10-01', search: '?v=2026-10-01' }, 'avif');
27
+
28
+ expect(headers['Cache-Control']).toBe('public, max-age=31536000, s-maxage=31536000, immutable');
29
+ expect(headers['Content-Type']).toBe('image/avif');
30
+ });
31
+ });
@@ -16,6 +16,11 @@ const IMAGE_STRATEGY: ImageStrategy = process.env.IMAGE_STRATEGY ?
16
16
  ImageStrategy.Mixer;
17
17
 
18
18
  const IMAGE_MAX_AGE = 604800;
19
+ // Versioned sources (`?v=`, `?version=`, `?updatedAt=`) never change at the same URL.
20
+ const IMAGE_IMMUTABLE_MAX_AGE = 31536000;
21
+ const IMAGE_STALE_WHILE_REVALIDATE = 86400;
22
+ // AVIF is served to browsers that accept it unless IMAGE_AVIF=false.
23
+ const IMAGE_AVIF = process.env.IMAGE_AVIF !== 'false';
19
24
  const IMAGE_FOCAL_X = 50;
20
25
  const IMAGE_FOCAL_Y = 50;
21
26
  const IMAGE_QUALITY = 90;
@@ -32,6 +37,8 @@ export type IImageOptions = IImageResize & {
32
37
  url: string;
33
38
  };
34
39
 
40
+ type IImageFormat = 'avif' | 'webp';
41
+
35
42
  type IImageHandlerNextContext = {
36
43
  params: Promise<{ slugs?: string[] }> | { slugs?: string[] };
37
44
  };
@@ -58,19 +65,14 @@ export function imageHandler({
58
65
  }],
59
66
  });
60
67
  }
68
+ const format = getImageFormat(request.headers.accept, imageOptions.url);
61
69
  // Generating a cached name for the image
62
- const cachedName = urlToCachedName(imageOptions);
70
+ const cachedName = urlToCachedName(imageOptions, format);
63
71
  // Checking if the cached image exists
64
72
  if (fs.existsSync(cachedName)) {
65
- // console.log('imageHandler.fromCache', cachedName);
66
73
  // Reading the cached image
67
74
  const cachedImage = await getCachedImage(cachedName);
68
- // Setting the 'Content-Type' header to 'image/webp'
69
- response.setHeader('Content-Type', 'image/webp');
70
- // Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)
71
- response.setHeader('Expires', new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString());
72
- // Setting the 'Cache-Control' header to 'public' and setting 'max-age' to the maximum age of the image
73
- response.setHeader('Cache-Control', `public, max-age=${IMAGE_MAX_AGE}`);
75
+ setImageHeaders(response, imageOptions, format);
74
76
  // Ending the response and sending the cached image
75
77
  return response.send(cachedImage);
76
78
  }
@@ -90,26 +92,17 @@ export function imageHandler({
90
92
  const originalImage = sharp(originalImageBuffer, {
91
93
  animated: isGIF(imageOptions.url),
92
94
  });
93
- // Resizing the original image and converting it to WebP format
95
+ // Resizing the original image
94
96
  const resizedImage = await resizeImage(originalImage, imageOptions);
95
- // Converting resized image to WebP format
96
- const compressedImage = resizedImage
97
- .webp({
98
- quality: imageOptions.q,
99
- // nearLossless: true,
100
- });
97
+ // Converting the resized image to the negotiated format
98
+ const compressedImage = encodeImage(resizedImage, format, imageOptions.q);
101
99
  // Saving the resized image to the cache
102
100
  compressedImage.toFile(cachedName)
103
101
  .then(() => console.log('imageHandler.sharp.cached', cachedName))
104
102
  .catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));
105
103
  // Converting the resized image to a buffer
106
104
  const data = await compressedImage.toBuffer();
107
- // Setting the 'Content-Type' header to 'image/webp'
108
- response.setHeader('Content-Type', 'image/webp');
109
- // Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)
110
- response.setHeader('Expires', new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString());
111
- // Setting the 'Cache-Control' header to 'public' and setting 'max-age' to the maximum age of the image
112
- response.setHeader('Cache-Control', `public, max-age=${IMAGE_MAX_AGE}`);
105
+ setImageHeaders(response, imageOptions, format);
113
106
  // Sending data in the response
114
107
  return response.end(data);
115
108
  }
@@ -141,10 +134,11 @@ export function imageGetHandler({
141
134
  } catch (error) {
142
135
  return getJsonErrorResponse(400, error);
143
136
  }
144
- const cachedName = urlToCachedName(imageOptions);
137
+ const format = getImageFormat(request.headers.get('accept'), imageOptions.url);
138
+ const cachedName = urlToCachedName(imageOptions, format);
145
139
  if (fs.existsSync(cachedName)) {
146
140
  const cachedImage = await getCachedImage(cachedName);
147
- return getImageResponse(cachedImage);
141
+ return getImageResponse(cachedImage, imageOptions, format);
148
142
  }
149
143
  if (isInvalidRefererValue(request.headers.get('referer'))) {
150
144
  return new Response('you are not authorized to perform this action', {
@@ -160,15 +154,12 @@ export function imageGetHandler({
160
154
  animated: isGIF(imageOptions.url),
161
155
  });
162
156
  const resizedImage = await resizeImage(originalImage, imageOptions);
163
- const compressedImage = resizedImage
164
- .webp({
165
- quality: imageOptions.q,
166
- });
157
+ const compressedImage = encodeImage(resizedImage, format, imageOptions.q);
167
158
  compressedImage.toFile(cachedName)
168
159
  .then(() => console.log('imageHandler.sharp.cached', cachedName))
169
160
  .catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));
170
161
  const data = await compressedImage.toBuffer();
171
- return getImageResponse(data);
162
+ return getImageResponse(data, imageOptions, format);
172
163
  }
173
164
  return getJsonErrorResponse(404, 'image not found');
174
165
  } catch (error: any) {
@@ -185,6 +176,46 @@ function isGIF(src: string): boolean {
185
176
  return src.indexOf('.gif') !== -1;
186
177
  }
187
178
 
179
+ /**
180
+ * Picks AVIF when the browser accepts it, WebP otherwise. Animated GIFs stay
181
+ * WebP: sharp does not encode animated AVIF.
182
+ */
183
+ export function getImageFormat(accept: string | null | undefined, src: string): IImageFormat {
184
+ if (IMAGE_AVIF && !isGIF(src) && accept && accept.includes('image/avif')) {
185
+ return 'avif';
186
+ }
187
+ return 'webp';
188
+ }
189
+
190
+ function encodeImage(image: sharp.Sharp, format: IImageFormat, quality?: number): sharp.Sharp {
191
+ return format === 'avif' ? image.avif({ quality }) : image.webp({ quality });
192
+ }
193
+
194
+ function isVersionedSource(options: Pick<IImageOptions, 'url' | 'search'>): boolean {
195
+ return /[?&](v|version|updatedAt)=/.test(options.search || '') || /[?&](v|version|updatedAt)=/.test(options.url || '');
196
+ }
197
+
198
+ /**
199
+ * Cache headers for a processed image. Responses vary by `Accept` because the
200
+ * format is negotiated; versioned sources are immutable.
201
+ */
202
+ export function getImageCacheHeaders(options: Pick<IImageOptions, 'url' | 'search'>, format: IImageFormat): Record<string, string> {
203
+ const maxAge = isVersionedSource(options) ? IMAGE_IMMUTABLE_MAX_AGE : IMAGE_MAX_AGE;
204
+ const cacheControl = isVersionedSource(options) ?
205
+ `public, max-age=${maxAge}, s-maxage=${maxAge}, immutable` :
206
+ `public, max-age=${maxAge}, s-maxage=${maxAge}, stale-while-revalidate=${IMAGE_STALE_WHILE_REVALIDATE}`;
207
+ return {
208
+ 'Cache-Control': cacheControl,
209
+ 'Content-Type': `image/${format}`,
210
+ Expires: new Date(Date.now() + maxAge * 1000).toUTCString(),
211
+ Vary: 'Accept',
212
+ };
213
+ }
214
+
215
+ function setImageHeaders(response: NextApiResponse, options: IImageOptions, format: IImageFormat) {
216
+ Object.entries(getImageCacheHeaders(options, format)).forEach(([key, value]) => response.setHeader(key, value));
217
+ }
218
+
188
219
  function isInvalidReferer(request: NextApiRequest): boolean {
189
220
  const { headers } = request;
190
221
  const referer = headers.referer;
@@ -451,13 +482,9 @@ function getOriginalImage(url: string, domains: string[]): Promise<Buffer> {
451
482
  });
452
483
  }
453
484
 
454
- function getImageResponse(data: Buffer<ArrayBufferLike>): Response {
485
+ function getImageResponse(data: Buffer<ArrayBufferLike>, options: IImageOptions, format: IImageFormat): Response {
455
486
  return new Response(data as BodyInit, {
456
- headers: {
457
- 'Cache-Control': `public, max-age=${IMAGE_MAX_AGE}`,
458
- 'Content-Type': 'image/webp',
459
- Expires: new Date(Date.now() + IMAGE_MAX_AGE * 1000).toUTCString(),
460
- },
487
+ headers: getImageCacheHeaders(options, format),
461
488
  });
462
489
  }
463
490
 
@@ -507,7 +534,7 @@ function isVercel(): boolean {
507
534
  }
508
535
 
509
536
  // Defining the 'urlToCachedName' function
510
- function urlToCachedName(options: IImageOptions): string {
537
+ function urlToCachedName(options: IImageOptions, format: IImageFormat): string {
511
538
  // Converting the URL to a file name
512
539
  const fileName = urlToFileName(options.url);
513
540
  // Parsing the file name
@@ -521,10 +548,11 @@ function urlToCachedName(options: IImageOptions): string {
521
548
  // Creating the cache directory
522
549
  fs.mkdirSync(cacheDir);
523
550
  }
524
- // Generating the cached name
551
+ // Generating the cached name; WebP keeps the historical names so existing caches stay valid
552
+ const formatSuffix = format === 'webp' ? '' : `_f_${format}`;
525
553
  const cachedName = options.h ?
526
- path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${parsedName.ext}`) :
527
- path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);
554
+ path.join(cacheDir, `${parsedName.name}_w_${options.w}_h_${options.h || 0}_x_${options.x || IMAGE_FOCAL_X}_y_${options.y || IMAGE_FOCAL_Y}_q_${options.q}${formatSuffix}${parsedName.ext}`) :
555
+ path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${formatSuffix}${parsedName.ext}`);
528
556
  // Returning the cached name
529
557
  return cachedName;
530
558
  }