@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 +17 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -21
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests/image.handler.test.ts +31 -0
- package/src/image/image.handler.ts +66 -38
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/image/image.handler.ts"],"sourcesContent":[],"mappings":";;;;
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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":";;;;;;;AAQA,SAAgB,WAAWA,SAA2B;AACpD,QAAO,OAAOC,SAAyBC,aAA4C;EACjF,MAAMC,UAAuB,QAAQ,UAAU,OAAO,aAAa;AAEnE,MAAI,CAAC,QAAQ,QACX,QAAO,SAAS,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,YAAY,CAAC,CAAC;AAEjE,MAAI;GAIF,MAAM,QAAQ,QAAQ,SAAS,SAAS;EACzC,SAAQ,OAAO;AAGd,UAAO,aAAa,OAAO,SAAS;EACrC;CACF;AACF;AAED,SAAgB,aAAaC,OAAYF,UAAgD;AACvF,KAAI,OAAQ,UAAW,UAAU;EAE/B,MAAM,QAAQ,MAAM,aAAa,CAAC,SAAS,YAAY;EACvD,MAAM,aAAa,QAAQ,MAAM;AACjC,SAAO,SAAS,OAAO,WAAW,CAAC,KAAK,EAAE,SAAS,MAAO,EAAC;CAC5D;AACD,KAAI,MAAM,SAAS,oBAEjB,QAAO,SAAS,OAAO,IAAI,CAAC,KAAK,EAAE,SAAS,gBAAiB,EAAC;CAGhE,QAAQ,MAAM,MAAM;AACpB,QAAO,SAAS,OAAO,IAAI,CAAC,KAAK,EAAE,SAAS,MAAM,QAAS,EAAC;AAC7D;;;;AC7BD,MAAMG,iBAAgC,QAAQ,IAAI,iBAChD,QAAQ,IAAI,iBACZ,cAAc;AAEhB,MAAM,gBAAgB;AAEtB,MAAM,0BAA0B;AAChC,MAAM,+BAA+B;AAErC,MAAM,aAAa,QAAQ,IAAI,eAAe;AAC9C,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AAoBtB,SAAgB,aAAa,EAC3B,UAAU,CAAE,GACZ,QAAQ,CAAE,GACV,UAAU,eACW,EAAE;AAEvB,QAAO,WAAW,EAEhB,KAAK,OAAOC,SAAyBC,aAA8B;AACjE,MAAI;GACF,IAAIC;AACJ,OAAI;IACF,eAAgB,mBAAmB,cAAc,QAC/C,yBAAyB,SAAS;KAAE;KAAS;KAAO;IAAS,EAAC,GAC9D,gBAAgB,SAAS;KAAE;KAAS;KAAO;IAAS,EAAC;GACxD,SAAQ,OAAO;AACd,WAAO,SAAS,OAAO,IAAI,CAAC,KAAK,EAC/B,QAAQ,CAAC,EACP,SAAS,MACV,CAAC,EACH,EAAC;GACH;GACD,MAAM,SAAS,eAAe,QAAQ,QAAQ,QAAQ,aAAa,IAAI;GAEvE,MAAM,aAAa,gBAAgB,cAAc,OAAO;AAExD,OAAI,GAAG,WAAW,WAAW,EAAE;IAE7B,MAAM,cAAc,MAAM,eAAe,WAAW;IACpD,gBAAgB,UAAU,cAAc,OAAO;AAE/C,WAAO,SAAS,KAAK,YAAY;GAClC;AAED,OAAI,iBAAiB,QAAQ,CAC3B,QAAO,SAAS,OAAO,IAAI,CAAC,KAAK,gDAAgD;AAGnF,OAAI,aAAa,IAAI,QAAQ,OAAO,KAAK,MAAM,UAAU,EACvD,SAAS,SAAS,KAAK,aAAa,IAAI;GAG1C,MAAM,sBAAsB,MAAM,iBAAiB,aAAa,KAAK,QAAQ;AAE7E,OAAI,qBAAqB;IAEvB,MAAM,gBAAgB,MAAM,qBAAqB,EAC/C,UAAU,MAAM,aAAa,IAAI,CAClC,EAAC;IAEF,MAAM,eAAe,MAAM,YAAY,eAAe,aAAa;IAEnE,MAAM,kBAAkB,YAAY,cAAc,QAAQ,aAAa,EAAE;IAEzE,gBAAgB,OAAO,WAAW,CAC/B,KAAK,MAAM,QAAQ,IAAI,6BAA6B,WAAW,CAAC,CAChE,MAAM,CAACC,UAAe,QAAQ,MAAM,4BAA4B,YAAY,MAAM,CAAC;IAEtF,MAAM,OAAO,MAAM,gBAAgB,UAAU;IAC7C,gBAAgB,UAAU,cAAc,OAAO;AAE/C,WAAO,SAAS,IAAI,KAAK;GAC1B;EACF,SAAQA,OAAY;GAEnB,QAAQ,MAAM,sBAAsB,MAAM;AAC1C,UAAO,SAAS,OAAO,MAAM,UAAU,IAAI,CAAC,KAAK,EAC/C,QAAQ,CAAC,EACP,SAAS,MAAM,WAAW,MAAM,QAAQ,MAAM,cAAc,oBAC7D,CAAC,EACH,EAAC;EACH;CACF,EACF,EAAC;AACH;AAED,SAAgB,gBAAgB,EAC9B,UAAU,CAAE,GACZ,QAAQ,CAAE,GACV,UAAU,eACW,EAAE;AACvB,QAAO,OAAOC,SAAsBC,YAAyD;AAC3F,MAAI;GACF,IAAIH;AACJ,OAAI;IACF,eAAgB,mBAAmB,cAAc,QAC/C,MAAM,6BAA6B,SAAS,SAAS;KAAE;KAAS;KAAO;IAAS,EAAC,GACjF,MAAM,oBAAoB,SAAS,SAAS;KAAE;KAAS;KAAO;IAAS,EAAC;GAC3E,SAAQ,OAAO;AACd,WAAO,qBAAqB,KAAK,MAAM;GACxC;GACD,MAAM,SAAS,eAAe,QAAQ,QAAQ,IAAI,SAAS,EAAE,aAAa,IAAI;GAC9E,MAAM,aAAa,gBAAgB,cAAc,OAAO;AACxD,OAAI,GAAG,WAAW,WAAW,EAAE;IAC7B,MAAM,cAAc,MAAM,eAAe,WAAW;AACpD,WAAO,iBAAiB,aAAa,cAAc,OAAO;GAC3D;AACD,OAAI,sBAAsB,QAAQ,QAAQ,IAAI,UAAU,CAAC,CACvD,QAAO,IAAI,SAAS,iDAAiD,EACnE,QAAQ,IACT;AAEH,OAAI,aAAa,IAAI,QAAQ,OAAO,KAAK,MAAM,UAAU,CACvD,QAAO,aAAa,SAAS,IAAI,IAAI,aAAa,KAAK,QAAQ,MAAM,IAAI;GAE3E,MAAM,sBAAsB,MAAM,iBAAiB,aAAa,KAAK,QAAQ;AAC7E,OAAI,qBAAqB;IACvB,MAAM,gBAAgB,MAAM,qBAAqB,EAC/C,UAAU,MAAM,aAAa,IAAI,CAClC,EAAC;IACF,MAAM,eAAe,MAAM,YAAY,eAAe,aAAa;IACnE,MAAM,kBAAkB,YAAY,cAAc,QAAQ,aAAa,EAAE;IACzE,gBAAgB,OAAO,WAAW,CAC/B,KAAK,MAAM,QAAQ,IAAI,6BAA6B,WAAW,CAAC,CAChE,MAAM,CAACC,UAAe,QAAQ,MAAM,4BAA4B,YAAY,MAAM,CAAC;IACtF,MAAM,OAAO,MAAM,gBAAgB,UAAU;AAC7C,WAAO,iBAAiB,MAAM,cAAc,OAAO;GACpD;AACD,UAAO,qBAAqB,KAAK,kBAAkB;EACpD,SAAQA,OAAY;GACnB,QAAQ,MAAM,sBAAsB,MAAM;AAC1C,UAAO,qBACL,MAAM,UAAU,KAChB,MAAM,WAAW,MAAM,QAAQ,MAAM,cAAc,oBACpD;EACF;CACF;AACF;AAED,SAAS,MAAMG,KAAsB;AACnC,QAAO,IAAI,QAAQ,OAAO,KAAK;AAChC;;;;;;;;AAMD,SAAgB,eAAeC,QAAmCD,KAA2B;AAC3F,KAAI,cAAc,CAAC,MAAM,IAAI,IAAI,UAAU,OAAO,SAAS,aAAa,CACtE,QAAO;AAET,QAAO;AACR;AAED,SAAS,YAAYE,OAAoBC,QAAsBC,SAA+B;AAC5F,QAAO,WAAW,SAAS,MAAM,KAAK,EAAE,QAAS,EAAC,GAAG,MAAM,KAAK,EAAE,QAAS,EAAC;AAC7E;AAED,SAAS,kBAAkBC,SAAyD;AAClF,QAAO,6BAA6B,KAAK,QAAQ,UAAU,GAAG,IAAI,6BAA6B,KAAK,QAAQ,OAAO,GAAG;AACvH;;;;;;;;AAMD,SAAgB,qBAAqBA,SAAgDF,QAA8C;CACjI,MAAM,SAAS,kBAAkB,QAAQ,GAAG,0BAA0B;CACtE,MAAM,eAAe,kBAAkB,QAAQ,GAC7C,CAAC,gBAAgB,EAAE,OAAO,WAAW,EAAE,OAAO,WAAW,CAAC,GAC1D,CAAC,gBAAgB,EAAE,OAAO,WAAW,EAAE,OAAO,yBAAyB,EAAE,8BAA8B;AACzG,QAAO;EACL,iBAAiB;EACjB,gBAAgB,CAAC,MAAM,EAAE,QAAQ;EACjC,SAAS,IAAI,KAAK,KAAK,KAAK,GAAG,SAAS,KAAM,aAAa;EAC3D,MAAM;CACP;AACF;AAED,SAAS,gBAAgBR,UAA2BW,SAAwBH,QAAsB;CAChG,OAAO,QAAQ,qBAAqB,SAAS,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,KAAK,MAAM,KAAK,SAAS,UAAU,KAAK,MAAM,CAAC;AAChH;AAED,SAAS,iBAAiBT,SAAkC;CAC1D,MAAM,EAAE,SAAS,GAAG;CACpB,MAAM,UAAU,QAAQ;AACxB,QAAO,sBAAsB,QAAQ;AACtC;AAED,SAAS,sBAAsBa,SAAkC;CAC/D,MAAM,YAAY,QAAQ,IAAI,mBAAmB;AAEjD,KACE,mBAAmB,cAAc,SACjC,WACA,cAAc,cACd,QAAQ,QAAQ,UAAU,KAAK,EAE/B,QAAO;AAET,QAAO;AACR;AAED,eAAe,oBACbT,SACAC,SACAS,SACwB;CACxB,MAAM,SAAS,MAAM,QAAQ;CAC7B,MAAM,QAAQ,OAAO,YAAY,QAAQ,QAAQ,aAAa,SAAS,CAAC;AACxE,QAAO,yBAAyB;EAC9B,GAAG;EACH,OAAO,OAAO;CACf,GAAE,QAAQ;AACZ;AAED,SAAS,gBAAgBd,SAAyBc,SAA8C;AAC9F,QAAO,yBAAyB,QAAQ,OAAO,QAAQ;AACxD;AAED,SAAS,yBACPC,OAGAD,SACe;CAEf,MAAM,EAAE,OAAO,SAAS,IAAI,SAAS,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG;AAE3D,KAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,MAAM,WAAW,EAE5C,OAAM;CAGR,MAAM,QAAQ,OAAO,MAAM,WAAW,SAAS,EAAE,GAAG;AAEpD,KAAI,CAAC,MAGH,OAAM;AAGR,KAAI,CAAC,QAAQ,MAAM,SAAS,MAAM,CAChC,OAAM;CAGR,MAAM,SAAS,OAAO,MAAM,WAAW,SAAS,EAAE,GAAG;CAErD,MAAM,SAAS,OAAO,MAAM,WAAW,SAAS,EAAE,GAAG;CAErD,MAAM,SAAS,OAAO,MAAM,WAAW,SAAS,EAAE,GAAG;CAErD,MAAM,eAAe,KAAK,IAAI,IAAI,KAAK,IAAI,KAAM,OAAO,MAAM,WAAW,SAAS,EAAE,GAAG,QAAQ,QAAS,CAAC;AAEzG,KAAI,iBAAiB,QAAQ,QAC3B,OAAM;CAGR,MAAM,MAAM,SAAS,MAAM,MAAM,KAAK,IAAI,GAAG;AAE7C,QAAO,SAAS;EACd,GAAG;EACH,GAAG;EACH,GAAG;EACH,GAAG;EACH,GAAG;EACH,KAAK;EACA;EACL,QAAQ,OAAO,OAAO;EACtB,QAAQ,OAAO,OAAO;CACvB,IAAG;EACF,GAAG;EACH,GAAG;EACH,KAAK;EACA;EACL,QAAQ,OAAO,OAAO;EACtB,QAAQ,OAAO,OAAO;CACvB;AACF;AAED,eAAe,6BACbV,SACAC,SACAS,SACwB;CACxB,MAAM,SAAS,MAAM,QAAQ;CAC7B,MAAM,QAAQ,OAAO,YAAY,QAAQ,QAAQ,aAAa,SAAS,CAAC;AACxE,QAAO,kCAAkC;EACvC,GAAG;EACH,OAAO,OAAO;CACf,GAAE,QAAQ;AACZ;AAED,SAAS,yBAAyBd,SAAyBc,SAA8C;AACvG,QAAO,kCAAkC,QAAQ,OAAO,QAAQ;AACjE;AAED,SAAS,kCACPE,OAGAF,SACe;CAEf,MAAM,EAAE,OAAO,SAAS,IAAI,SAAS,IAAI,GAAG,GAAG;AAE/C,KAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,MAAM,WAAW,EAE5C,OAAM;AAER,KAAI,OAAO,MAAM,SACf,OAAM;CAER,MAAM,cAAc,mBAAmB,EAAE;CAgBzC,YAAY,MAAM,mBAAmB,YAAY,IAAI;CAErD,MAAM,MAAM,mBAAmB,SAAS,MAAM,MAAM,KAAK,IAAI,GAAG,OAAO;AAEvE,KAAI,YAAY,QAAQ,IACtB,OAAM,CAAC,sBAAsB,EAAE,YAAY,IAAI,CAAC,EAAE,KAAK;AAGzD,QAAO;EACL,GAAG;EACE;EACL,QAAQ,OAAO,OAAO;EACtB,QAAQ,OAAO,OAAO;CACvB;AACF;AAED,eAAe,YAAYG,eAA4Bf,cAAmD;CACxG,MAAM,EAAE,GAAG,aAAa,GAAG,cAAc,GAAG,SAAS,eAAe,GAAG,SAAS,eAAe,GAAG;CAClG,MAAM,uBAAuB,MAAM;AACjC,SAAO,cAAc,OAAO;GAC1B,OAAO;GACP,QAAQ,eAAe,eAAe;GACtC,KAAK,MAAM,IAAI;GACf,UAAU,MAAM,SAAS;GACzB,oBAAoB;EACrB,EAAC;CACH;AACD,KAAI,iBAAiB,WAAW,iBAAiB,WAAW,gBAAgB;EAC1E,MAAM,EAAE,OAAO,eAAe,QAAQ,gBAAgB,GAAG,MAAM,cAAc,UAAU;AACvF,MAAI,iBAAiB,gBAAgB;GACnC,MAAM,oBAAoB,cAAc;GACxC,MAAM,sBAAsB,gBAAgB;GAC5C,MAAM,mBAAmB,oBAAoB;GAC7C,MAAM,gBAAgB,cAAc,OAAO;GAE3C,MAAM,cAAc,cAAc,OAAO,mBAAmB;IAC1D,QAAQ;IACR,KAAK,MAAM,IAAI;GAChB,IAAG;IACF,OAAO;IACP,KAAK,MAAM,IAAI;GAChB,EAAC;GACF,MAAM,EAAE,MAAM,iBAAiB,GAAG,MAAM,YAAY,SAAS,EAAE,mBAAmB,KAAM,EAAC;GAEzF,MAAM,eAAe,eAAe,gBAAgB;GACpD,MAAM,aAAa,gBAAgB,QAAQ;GAC3C,MAAM,gBAAgB,KAAK,MACzB,gBAAgB,SAAS,SAAS,OAAO,eAAe,EACzD;GAED,MAAM,WAAW,KAAK,IAAI,KAAK,IAAI,GAAG,cAAc,EAAE,WAAW;GAEjE,MAAM,gBAAgB,gBAAgB,gBAAgB;GACtD,MAAM,aAAa,gBAAgB,SAAS;GAC5C,MAAM,eAAe,KAAK,MACxB,gBAAgB,UAAU,SAAS,OAAO,gBAAgB,EAC3D;GAED,MAAM,WAAW,KAAK,IAAI,KAAK,IAAI,GAAG,aAAa,EAAE,WAAW;AAEhE,UAAO,YAAY,QAAQ;IACzB,MAAM;IACN,KAAK;IACL,OAAO;IACP,QAAQ;GACT,EAAC;EACH,MACC,QAAO,sBAAsB;CAEhC,MACC,QAAO,sBAAsB;AAEhC;AAGD,SAAS,iBAAiBgB,KAAaC,SAAoC;AAEzE,QAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAEtC,MAAI,IAAI,QAAQ,OAAO,KAAK,GAAG;AAC7B,OAAI,gBAAgB,KAAK,QAAQ,CAC/B,QAAO,OAAO;IAAE,QAAQ;IAAK,SAAS;GAAkB,EAAC;GAG3D,MAAM,KAAK;IACT,QAAQ;IACR,MAAM;IACN,SAAS,EACP,+BAA+B,IAChC;GACF,EAAC,CACC,KAAK,cAAY;AAChB,QAAI,CAAC,SAAS,GACZ,QAAO,OAAO;KAAE,QAAQ,SAAS;KAAQ,SAAS,SAAS;IAAY,EAAC;IAE1E,SAAS,aAAa,CAAC,KAAK,YAAU;AACpC,YAAO,QAAQ,OAAO,KAAK,OAAO,CAAC;IACpC,EAAC,CAAC,MAAM,WAAS;AAChB,YAAO,OAAO,MAAM;IACrB,EAAC;GACH,EAAC,CACD,MAAM,WAAS;AAEd,WAAO,OAAO,MAAM;GACrB,EAAC;EACL,OAAM;GAEL,MAAM,WAAW,KAAK,KAAK,QAAQ,KAAK,EAAE,UAAU,IAAI;GAExD,GAAG,SAAS,UAAU,CAAC,OAAO,SAAS;AACrC,QAAI,MACF,QAAO,OAAO,MAAM;AAGtB,WAAO,QAAQ,KAAK;GACrB,EAAC;EACH;CACF;AACF;AAED,SAAS,iBAAiBC,MAA+BR,SAAwBH,QAAgC;AAC/G,QAAO,IAAI,SAAS,MAAkB,EACpC,SAAS,qBAAqB,SAAS,OAAO,CAC/C;AACF;AAED,SAAS,qBAAqBY,QAAgBC,SAA4B;AACxE,QAAO,SAAS,KAAK,EACnB,QAAQ,CAAC,EACP,QACD,CAAC,EACH,GAAE,EACD,OACD,EAAC;AACH;AAGD,SAAS,eAAeC,UAAmC;AAEzD,QAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;EAEtC,GAAG,SAAS,UAAU,CAAC,OAAO,SAAS;AACrC,OAAI,MAEF,QAAO,OAAO,MAAM;AAGtB,UAAO,QAAQ,KAAK;EACrB,EAAC;CACH;AACF;AAGD,SAAS,cAAcL,KAAqB;CAE1C,MAAM,WAAW,IAAI,MAAM,IAAI;CAC/B,MAAMM,SAAO,SAAS;CACtB,MAAM,QAAQ,SAAS;CACvB,IAAI,aAAaA,OAAK,QAAQ,OAAO,IAAI,CAAC,QAAQ,eAAe,IAAI;CAErE,aAAa,WAAW,UAAU,GAAG,WAAW,YAAY,IAAI,CAAC,IAC5D,QAAQ,MAAM,MAAM,QAAQ,eAAe,IAAI,GAAG,MACnD,MAAM,WAAW,UAAU,WAAW,YAAY,IAAI,GAAG,GAAG,WAAW,OAAO;AAElF,QAAO;AACR;AAED,SAAS,WAAoB;AAC3B,QAAO,WAAW,QAAQ,OAAO,OAAO,QAAQ,IAAI,OAAO,KAAK;AACjE;AAGD,SAAS,gBAAgBZ,SAAwBH,QAA8B;CAE7E,MAAM,WAAW,cAAc,QAAQ,IAAI;CAE3C,MAAM,aAAa,KAAK,MAAM,SAAS;CAEvC,MAAM,WAAW,UAAU,GACzB,KAAK,KAAK,QAAQ,SAAS,GAC3B,KAAK,KAAK,QAAQ,KAAK,EAAE,SAAS;AAEpC,KAAI,CAAC,GAAG,WAAW,SAAS,EAE1B,GAAG,UAAU,SAAS;CAGxB,MAAM,eAAe,WAAW,SAAS,KAAK,CAAC,GAAG,EAAE,QAAQ;CAC5D,MAAM,aAAa,QAAQ,IACzB,KAAK,KAAK,UAAU,GAAG,WAAW,KAAK,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,KAAK,EAAE,GAAG,EAAE,QAAQ,KAAK,cAAc,GAAG,EAAE,QAAQ,KAAK,cAAc,GAAG,EAAE,QAAQ,IAAI,eAAe,WAAW,KAAK,CAAC,GAC3L,KAAK,KAAK,UAAU,GAAG,WAAW,KAAK,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,eAAe,WAAW,KAAK,CAAC;AAEzG,QAAO;AACR;AAED,SAAS,gBAAgBH,KAAaa,SAA4B;AAChE,KAAI,mBAAmB,cAAc,MACnC,QAAO;CAET,MAAM,MAAM,IAAI,IAAI;AACpB,QAAO,CAAC,QAAQ,SAAS,IAAI,SAAS;AACvC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@websolutespa/bom-mixer-image",
|
|
3
|
-
"version": "
|
|
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": "^
|
|
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": "^
|
|
34
|
+
"@websolutespa/bom-cli": "^4.0.0-next.0",
|
|
35
35
|
"@websolutespa/eslint-config": "^3.0.0",
|
|
36
|
-
"@websolutespa/test": "^
|
|
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
|
-
|
|
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
|
|
95
|
+
// Resizing the original image
|
|
94
96
|
const resizedImage = await resizeImage(originalImage, imageOptions);
|
|
95
|
-
// Converting resized image to
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|