@websolutespa/bom-mixer-image 1.8.1 → 1.8.3
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 +14 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +90 -31
- package/dist/index.mjs +90 -31
- package/package.json +1 -1
- package/src/image/image.handler.ts +151 -51
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @websolutespa/bom-mixer-image
|
|
2
2
|
|
|
3
|
+
## 1.8.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Added: ImageStrategy.Token
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @websolutespa/bom-core@1.8.4
|
|
10
|
+
|
|
11
|
+
## 1.8.2
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Modified: imageHandler
|
|
16
|
+
|
|
3
17
|
## 1.8.1
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import { IImageResize } from '@websolutespa/bom-core';
|
|
2
|
+
|
|
3
|
+
type IImageHandlerOptions = {
|
|
2
4
|
domains: string[];
|
|
3
5
|
sizes: number[];
|
|
4
6
|
quality: number;
|
|
5
|
-
}
|
|
7
|
+
};
|
|
8
|
+
type IImageOptions = IImageResize & {
|
|
9
|
+
origin: string;
|
|
10
|
+
search: string;
|
|
11
|
+
url: string;
|
|
12
|
+
};
|
|
13
|
+
declare function imageHandler({ domains, sizes, quality, }: IImageHandlerOptions): any;
|
|
6
14
|
|
|
7
|
-
export { imageHandler };
|
|
15
|
+
export { IImageHandlerOptions, IImageOptions, imageHandler };
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
38
38
|
var fs = __toESM(require("fs"));
|
|
39
39
|
var path = __toESM(require("path"));
|
|
40
40
|
var import_sharp = __toESM(require("sharp"));
|
|
41
|
+
var import_bom_core = require("@websolutespa/bom-core");
|
|
41
42
|
|
|
42
43
|
// src/api/api.handler.ts
|
|
43
44
|
function apiHandler(handler) {
|
|
@@ -67,7 +68,10 @@ function errorHandler(error, response) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
// src/image/image.handler.ts
|
|
71
|
+
var IMAGE_STRATEGY = process.env.IMAGE_STRATEGY ? process.env.IMAGE_STRATEGY : import_bom_core.ImageStrategy.Mixer;
|
|
70
72
|
var IMAGE_MAX_AGE = 604800;
|
|
73
|
+
var IMAGE_FOCAL_X = 50;
|
|
74
|
+
var IMAGE_FOCAL_Y = 50;
|
|
71
75
|
var IMAGE_QUALITY = 90;
|
|
72
76
|
function imageHandler({
|
|
73
77
|
domains = [],
|
|
@@ -78,26 +82,17 @@ function imageHandler({
|
|
|
78
82
|
// Defining the 'get' method for handling GET requests
|
|
79
83
|
get: async (request, response) => {
|
|
80
84
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return response.status(400).send("invalid w parameter");
|
|
91
|
-
}
|
|
92
|
-
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
93
|
-
const focalX = typeof x === "string" ? parseInt(x) : 50;
|
|
94
|
-
const focalY = typeof y === "string" ? parseInt(y) : 50;
|
|
95
|
-
const imageQuality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : quality));
|
|
96
|
-
if (imageQuality !== quality) {
|
|
97
|
-
return response.status(400).send("invalid q parameter");
|
|
85
|
+
let imageOptions;
|
|
86
|
+
try {
|
|
87
|
+
imageOptions = IMAGE_STRATEGY === import_bom_core.ImageStrategy.Token ? getEncryptedImageOptions(request, { domains, sizes, quality }) : getImageOptions(request, { domains, sizes, quality });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return response.status(400).json({
|
|
90
|
+
errors: [{
|
|
91
|
+
message: error
|
|
92
|
+
}]
|
|
93
|
+
});
|
|
98
94
|
}
|
|
99
|
-
const
|
|
100
|
-
const cachedName = urlToCachedName(url, width, height, focalX, focalY, imageQuality);
|
|
95
|
+
const cachedName = urlToCachedName(imageOptions);
|
|
101
96
|
if (fs.existsSync(cachedName)) {
|
|
102
97
|
const cachedImage = await getCachedImage(cachedName);
|
|
103
98
|
response.setHeader("Content-Type", "image/webp");
|
|
@@ -108,18 +103,18 @@ function imageHandler({
|
|
|
108
103
|
const { headers } = request;
|
|
109
104
|
const referer = headers.referer;
|
|
110
105
|
const publicUrl = process.env.NEXT_PUBLIC_URL || "";
|
|
111
|
-
if (
|
|
106
|
+
if (referer && publicUrl !== "https://" && referer.indexOf(publicUrl) !== 0) {
|
|
112
107
|
return response.status(401).send("you are not authorized to perform this action");
|
|
113
108
|
}
|
|
114
|
-
if (url.indexOf("http") === -1 && isVercel()) {
|
|
115
|
-
response.redirect(307, url);
|
|
109
|
+
if (imageOptions.url.indexOf("http") === -1 && isVercel()) {
|
|
110
|
+
response.redirect(307, imageOptions.url);
|
|
116
111
|
}
|
|
117
|
-
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
112
|
+
const originalImageBuffer = await getOriginalImage(imageOptions.url, domains, request.headers);
|
|
118
113
|
if (originalImageBuffer) {
|
|
119
114
|
const originalImage = (0, import_sharp.default)(originalImageBuffer);
|
|
120
|
-
const resizedImage = await resizeImage(originalImage,
|
|
115
|
+
const resizedImage = await resizeImage(originalImage, imageOptions);
|
|
121
116
|
const compressedImage = resizedImage.webp({
|
|
122
|
-
quality:
|
|
117
|
+
quality: imageOptions.q
|
|
123
118
|
// nearLossless: true,
|
|
124
119
|
});
|
|
125
120
|
compressedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("imageHandler.sharp.error", cachedName, error));
|
|
@@ -131,12 +126,76 @@ function imageHandler({
|
|
|
131
126
|
}
|
|
132
127
|
} catch (error) {
|
|
133
128
|
console.error("imageHandler.error", error);
|
|
134
|
-
|
|
129
|
+
return response.status(error.status || 500).json({
|
|
130
|
+
errors: [{
|
|
131
|
+
message: error.message || error.name || error.statusText || "An error occurred"
|
|
132
|
+
}]
|
|
133
|
+
});
|
|
135
134
|
}
|
|
136
135
|
}
|
|
137
136
|
});
|
|
138
137
|
}
|
|
139
|
-
|
|
138
|
+
function getImageOptions(request, options) {
|
|
139
|
+
const { slugs, origin = "", search = "", w, h, x, y, q } = request.query;
|
|
140
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
141
|
+
throw "missing slugs parameter";
|
|
142
|
+
}
|
|
143
|
+
const width = typeof w === "string" ? parseInt(w) : void 0;
|
|
144
|
+
if (!width) {
|
|
145
|
+
throw "missing w parameter";
|
|
146
|
+
}
|
|
147
|
+
if (!options.sizes.includes(width)) {
|
|
148
|
+
throw "invalid w parameter";
|
|
149
|
+
}
|
|
150
|
+
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
151
|
+
const focalX = typeof x === "string" ? parseInt(x) : IMAGE_FOCAL_X;
|
|
152
|
+
const focalY = typeof y === "string" ? parseInt(y) : IMAGE_FOCAL_Y;
|
|
153
|
+
const imageQuality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : options.quality));
|
|
154
|
+
if (imageQuality !== options.quality) {
|
|
155
|
+
throw "invalid q parameter";
|
|
156
|
+
}
|
|
157
|
+
const url = origin + "/" + slugs.join("/") + search;
|
|
158
|
+
return height ? {
|
|
159
|
+
w: width,
|
|
160
|
+
h: height,
|
|
161
|
+
x: focalX,
|
|
162
|
+
y: focalY,
|
|
163
|
+
q: imageQuality,
|
|
164
|
+
src: url,
|
|
165
|
+
url,
|
|
166
|
+
origin: String(origin),
|
|
167
|
+
search: String(search)
|
|
168
|
+
} : {
|
|
169
|
+
w: width,
|
|
170
|
+
q: imageQuality,
|
|
171
|
+
src: url,
|
|
172
|
+
url,
|
|
173
|
+
origin: String(origin),
|
|
174
|
+
search: String(search)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function getEncryptedImageOptions(request, options) {
|
|
178
|
+
const { slugs, origin = "", search = "", t } = request.query;
|
|
179
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
180
|
+
throw "missing slugs parameter";
|
|
181
|
+
}
|
|
182
|
+
if (typeof t !== "string") {
|
|
183
|
+
throw "missing t parameter";
|
|
184
|
+
}
|
|
185
|
+
const imageResize = (0, import_bom_core.decryptImageResize)(t);
|
|
186
|
+
const url = origin + "/" + slugs.join("/") + search;
|
|
187
|
+
if (imageResize.src !== url) {
|
|
188
|
+
throw "invalid src parameter";
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
...imageResize,
|
|
192
|
+
url,
|
|
193
|
+
origin: String(origin),
|
|
194
|
+
search: String(search)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async function resizeImage(originalImage, imageOptions) {
|
|
198
|
+
const { w: resizeWidth, h: resizeHeight, x: focalX = IMAGE_FOCAL_X, y: focalY = IMAGE_FOCAL_Y } = imageOptions;
|
|
140
199
|
const defaultResizeOptions = () => {
|
|
141
200
|
return originalImage.resize({
|
|
142
201
|
width: resizeWidth,
|
|
@@ -146,7 +205,7 @@ async function resizeImage(originalImage, resizeWidth, resizeHeight, focalX, foc
|
|
|
146
205
|
withoutEnlargement: true
|
|
147
206
|
});
|
|
148
207
|
};
|
|
149
|
-
if (resizeHeight && (focalX !==
|
|
208
|
+
if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {
|
|
150
209
|
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
151
210
|
if (originalWidth && originalHeight) {
|
|
152
211
|
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
@@ -242,14 +301,14 @@ function urlToFileName(url) {
|
|
|
242
301
|
function isVercel() {
|
|
243
302
|
return process && process.env && String(process.env.VERCEL) === "1";
|
|
244
303
|
}
|
|
245
|
-
function urlToCachedName(
|
|
246
|
-
const fileName = urlToFileName(url);
|
|
304
|
+
function urlToCachedName(options) {
|
|
305
|
+
const fileName = urlToFileName(options.url);
|
|
247
306
|
const parsedName = path.parse(fileName);
|
|
248
307
|
const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
|
|
249
308
|
if (!fs.existsSync(cacheDir)) {
|
|
250
309
|
fs.mkdirSync(cacheDir);
|
|
251
310
|
}
|
|
252
|
-
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${
|
|
311
|
+
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}`);
|
|
253
312
|
return cachedName;
|
|
254
313
|
}
|
|
255
314
|
function checkUrlValidity(src, domains) {
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import sharp from "sharp";
|
|
5
|
+
import { decryptImageResize, ImageStrategy } from "@websolutespa/bom-core";
|
|
5
6
|
|
|
6
7
|
// src/api/api.handler.ts
|
|
7
8
|
function apiHandler(handler) {
|
|
@@ -31,7 +32,10 @@ function errorHandler(error, response) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
// src/image/image.handler.ts
|
|
35
|
+
var IMAGE_STRATEGY = process.env.IMAGE_STRATEGY ? process.env.IMAGE_STRATEGY : ImageStrategy.Mixer;
|
|
34
36
|
var IMAGE_MAX_AGE = 604800;
|
|
37
|
+
var IMAGE_FOCAL_X = 50;
|
|
38
|
+
var IMAGE_FOCAL_Y = 50;
|
|
35
39
|
var IMAGE_QUALITY = 90;
|
|
36
40
|
function imageHandler({
|
|
37
41
|
domains = [],
|
|
@@ -42,26 +46,17 @@ function imageHandler({
|
|
|
42
46
|
// Defining the 'get' method for handling GET requests
|
|
43
47
|
get: async (request, response) => {
|
|
44
48
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return response.status(400).send("invalid w parameter");
|
|
55
|
-
}
|
|
56
|
-
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
57
|
-
const focalX = typeof x === "string" ? parseInt(x) : 50;
|
|
58
|
-
const focalY = typeof y === "string" ? parseInt(y) : 50;
|
|
59
|
-
const imageQuality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : quality));
|
|
60
|
-
if (imageQuality !== quality) {
|
|
61
|
-
return response.status(400).send("invalid q parameter");
|
|
49
|
+
let imageOptions;
|
|
50
|
+
try {
|
|
51
|
+
imageOptions = IMAGE_STRATEGY === ImageStrategy.Token ? getEncryptedImageOptions(request, { domains, sizes, quality }) : getImageOptions(request, { domains, sizes, quality });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return response.status(400).json({
|
|
54
|
+
errors: [{
|
|
55
|
+
message: error
|
|
56
|
+
}]
|
|
57
|
+
});
|
|
62
58
|
}
|
|
63
|
-
const
|
|
64
|
-
const cachedName = urlToCachedName(url, width, height, focalX, focalY, imageQuality);
|
|
59
|
+
const cachedName = urlToCachedName(imageOptions);
|
|
65
60
|
if (fs.existsSync(cachedName)) {
|
|
66
61
|
const cachedImage = await getCachedImage(cachedName);
|
|
67
62
|
response.setHeader("Content-Type", "image/webp");
|
|
@@ -72,18 +67,18 @@ function imageHandler({
|
|
|
72
67
|
const { headers } = request;
|
|
73
68
|
const referer = headers.referer;
|
|
74
69
|
const publicUrl = process.env.NEXT_PUBLIC_URL || "";
|
|
75
|
-
if (
|
|
70
|
+
if (referer && publicUrl !== "https://" && referer.indexOf(publicUrl) !== 0) {
|
|
76
71
|
return response.status(401).send("you are not authorized to perform this action");
|
|
77
72
|
}
|
|
78
|
-
if (url.indexOf("http") === -1 && isVercel()) {
|
|
79
|
-
response.redirect(307, url);
|
|
73
|
+
if (imageOptions.url.indexOf("http") === -1 && isVercel()) {
|
|
74
|
+
response.redirect(307, imageOptions.url);
|
|
80
75
|
}
|
|
81
|
-
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
76
|
+
const originalImageBuffer = await getOriginalImage(imageOptions.url, domains, request.headers);
|
|
82
77
|
if (originalImageBuffer) {
|
|
83
78
|
const originalImage = sharp(originalImageBuffer);
|
|
84
|
-
const resizedImage = await resizeImage(originalImage,
|
|
79
|
+
const resizedImage = await resizeImage(originalImage, imageOptions);
|
|
85
80
|
const compressedImage = resizedImage.webp({
|
|
86
|
-
quality:
|
|
81
|
+
quality: imageOptions.q
|
|
87
82
|
// nearLossless: true,
|
|
88
83
|
});
|
|
89
84
|
compressedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("imageHandler.sharp.error", cachedName, error));
|
|
@@ -95,12 +90,76 @@ function imageHandler({
|
|
|
95
90
|
}
|
|
96
91
|
} catch (error) {
|
|
97
92
|
console.error("imageHandler.error", error);
|
|
98
|
-
|
|
93
|
+
return response.status(error.status || 500).json({
|
|
94
|
+
errors: [{
|
|
95
|
+
message: error.message || error.name || error.statusText || "An error occurred"
|
|
96
|
+
}]
|
|
97
|
+
});
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
});
|
|
102
101
|
}
|
|
103
|
-
|
|
102
|
+
function getImageOptions(request, options) {
|
|
103
|
+
const { slugs, origin = "", search = "", w, h, x, y, q } = request.query;
|
|
104
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
105
|
+
throw "missing slugs parameter";
|
|
106
|
+
}
|
|
107
|
+
const width = typeof w === "string" ? parseInt(w) : void 0;
|
|
108
|
+
if (!width) {
|
|
109
|
+
throw "missing w parameter";
|
|
110
|
+
}
|
|
111
|
+
if (!options.sizes.includes(width)) {
|
|
112
|
+
throw "invalid w parameter";
|
|
113
|
+
}
|
|
114
|
+
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
115
|
+
const focalX = typeof x === "string" ? parseInt(x) : IMAGE_FOCAL_X;
|
|
116
|
+
const focalY = typeof y === "string" ? parseInt(y) : IMAGE_FOCAL_Y;
|
|
117
|
+
const imageQuality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : options.quality));
|
|
118
|
+
if (imageQuality !== options.quality) {
|
|
119
|
+
throw "invalid q parameter";
|
|
120
|
+
}
|
|
121
|
+
const url = origin + "/" + slugs.join("/") + search;
|
|
122
|
+
return height ? {
|
|
123
|
+
w: width,
|
|
124
|
+
h: height,
|
|
125
|
+
x: focalX,
|
|
126
|
+
y: focalY,
|
|
127
|
+
q: imageQuality,
|
|
128
|
+
src: url,
|
|
129
|
+
url,
|
|
130
|
+
origin: String(origin),
|
|
131
|
+
search: String(search)
|
|
132
|
+
} : {
|
|
133
|
+
w: width,
|
|
134
|
+
q: imageQuality,
|
|
135
|
+
src: url,
|
|
136
|
+
url,
|
|
137
|
+
origin: String(origin),
|
|
138
|
+
search: String(search)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function getEncryptedImageOptions(request, options) {
|
|
142
|
+
const { slugs, origin = "", search = "", t } = request.query;
|
|
143
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
144
|
+
throw "missing slugs parameter";
|
|
145
|
+
}
|
|
146
|
+
if (typeof t !== "string") {
|
|
147
|
+
throw "missing t parameter";
|
|
148
|
+
}
|
|
149
|
+
const imageResize = decryptImageResize(t);
|
|
150
|
+
const url = origin + "/" + slugs.join("/") + search;
|
|
151
|
+
if (imageResize.src !== url) {
|
|
152
|
+
throw "invalid src parameter";
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
...imageResize,
|
|
156
|
+
url,
|
|
157
|
+
origin: String(origin),
|
|
158
|
+
search: String(search)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function resizeImage(originalImage, imageOptions) {
|
|
162
|
+
const { w: resizeWidth, h: resizeHeight, x: focalX = IMAGE_FOCAL_X, y: focalY = IMAGE_FOCAL_Y } = imageOptions;
|
|
104
163
|
const defaultResizeOptions = () => {
|
|
105
164
|
return originalImage.resize({
|
|
106
165
|
width: resizeWidth,
|
|
@@ -110,7 +169,7 @@ async function resizeImage(originalImage, resizeWidth, resizeHeight, focalX, foc
|
|
|
110
169
|
withoutEnlargement: true
|
|
111
170
|
});
|
|
112
171
|
};
|
|
113
|
-
if (resizeHeight && (focalX !==
|
|
172
|
+
if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {
|
|
114
173
|
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
115
174
|
if (originalWidth && originalHeight) {
|
|
116
175
|
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
@@ -206,14 +265,14 @@ function urlToFileName(url) {
|
|
|
206
265
|
function isVercel() {
|
|
207
266
|
return process && process.env && String(process.env.VERCEL) === "1";
|
|
208
267
|
}
|
|
209
|
-
function urlToCachedName(
|
|
210
|
-
const fileName = urlToFileName(url);
|
|
268
|
+
function urlToCachedName(options) {
|
|
269
|
+
const fileName = urlToFileName(options.url);
|
|
211
270
|
const parsedName = path.parse(fileName);
|
|
212
271
|
const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
|
|
213
272
|
if (!fs.existsSync(cacheDir)) {
|
|
214
273
|
fs.mkdirSync(cacheDir);
|
|
215
274
|
}
|
|
216
|
-
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${
|
|
275
|
+
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}`);
|
|
217
276
|
return cachedName;
|
|
218
277
|
}
|
|
219
278
|
function checkUrlValidity(src, domains) {
|
package/package.json
CHANGED
|
@@ -8,63 +8,55 @@ import * as path from 'path';
|
|
|
8
8
|
// Importing 'sharp' for image processing
|
|
9
9
|
import sharp from 'sharp';
|
|
10
10
|
// Importing 'apiHandler' from '../api/api.handler'
|
|
11
|
+
import { decryptImageResize, IImageResize, ImageStrategy } from '@websolutespa/bom-core';
|
|
11
12
|
import { apiHandler } from '../api/api.handler';
|
|
12
13
|
|
|
14
|
+
const IMAGE_STRATEGY: ImageStrategy = process.env.IMAGE_STRATEGY ?
|
|
15
|
+
process.env.IMAGE_STRATEGY as ImageStrategy :
|
|
16
|
+
ImageStrategy.Mixer;
|
|
17
|
+
|
|
13
18
|
const IMAGE_MAX_AGE = 604800;
|
|
14
19
|
const IMAGE_FOCAL_X = 50;
|
|
15
20
|
const IMAGE_FOCAL_Y = 50;
|
|
16
21
|
const IMAGE_QUALITY = 90;
|
|
17
22
|
|
|
23
|
+
export type IImageHandlerOptions = {
|
|
24
|
+
domains: string[];
|
|
25
|
+
sizes: number[];
|
|
26
|
+
quality: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type IImageOptions = IImageResize & {
|
|
30
|
+
origin: string;
|
|
31
|
+
search: string;
|
|
32
|
+
url: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
18
35
|
// Exporting the 'imageHandler' function
|
|
19
36
|
export function imageHandler({
|
|
20
37
|
domains = [],
|
|
21
38
|
sizes = [],
|
|
22
39
|
quality = IMAGE_QUALITY,
|
|
23
|
-
}: {
|
|
24
|
-
domains: string[];
|
|
25
|
-
sizes: number[];
|
|
26
|
-
quality: number;
|
|
27
|
-
}) {
|
|
40
|
+
}: IImageHandlerOptions) {
|
|
28
41
|
// Returning the 'apiHandler' function with a configuration object
|
|
29
42
|
return apiHandler({
|
|
30
43
|
// Defining the 'get' method for handling GET requests
|
|
31
44
|
get: async (request: NextApiRequest, response: NextApiResponse) => {
|
|
32
45
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// Logging an error message and returning a 400 status code with a message
|
|
45
|
-
// console.error('imageHandler', 'missing w parameter');
|
|
46
|
-
return response.status(400).send('missing w parameter');
|
|
47
|
-
}
|
|
48
|
-
// if (width > 2048) {
|
|
49
|
-
if (!sizes.includes(width)) {
|
|
50
|
-
return response.status(400).send('invalid w parameter');
|
|
51
|
-
}
|
|
52
|
-
// Parsing 'h' to an integer and assigning it to 'height'
|
|
53
|
-
const height = typeof h === 'string' ? parseInt(h) : undefined;
|
|
54
|
-
// Parsing 'x' to an integer and assigning it to 'focalX'
|
|
55
|
-
const focalX = typeof x === 'string' ? parseInt(x) : 50;
|
|
56
|
-
// Parsing 'y' to an integer and assigning it to 'focalY'
|
|
57
|
-
const focalY = typeof y === 'string' ? parseInt(y) : 50;
|
|
58
|
-
// Parsing 'q' to an integer, ensuring it's between 30 and 100, and assigning it to 'imageQuality'
|
|
59
|
-
const imageQuality = Math.max(30, Math.min(100, (typeof q === 'string' ? parseInt(q) : quality)));
|
|
60
|
-
// if (width > 2048) {
|
|
61
|
-
if (imageQuality !== quality) {
|
|
62
|
-
return response.status(400).send('invalid q parameter');
|
|
46
|
+
let imageOptions: IImageOptions;
|
|
47
|
+
try {
|
|
48
|
+
imageOptions = (IMAGE_STRATEGY === ImageStrategy.Token ?
|
|
49
|
+
getEncryptedImageOptions(request, { domains, sizes, quality }) :
|
|
50
|
+
getImageOptions(request, { domains, sizes, quality }));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return response.status(400).json({
|
|
53
|
+
errors: [{
|
|
54
|
+
message: error,
|
|
55
|
+
}],
|
|
56
|
+
});
|
|
63
57
|
}
|
|
64
|
-
// Generating url
|
|
65
|
-
const url = origin + '/' + slugs.join('/') + search;
|
|
66
58
|
// Generating a cached name for the image
|
|
67
|
-
const cachedName = urlToCachedName(
|
|
59
|
+
const cachedName = urlToCachedName(imageOptions);
|
|
68
60
|
// Checking if the cached image exists
|
|
69
61
|
if (fs.existsSync(cachedName)) {
|
|
70
62
|
// console.log('imageHandler.fromCache', cachedName);
|
|
@@ -84,25 +76,29 @@ export function imageHandler({
|
|
|
84
76
|
const referer = headers.referer;
|
|
85
77
|
const publicUrl = process.env.NEXT_PUBLIC_URL || '';
|
|
86
78
|
// console.log('referef', referer, 'publicUrl', publicUrl);
|
|
87
|
-
if (
|
|
79
|
+
if (
|
|
80
|
+
referer &&
|
|
81
|
+
publicUrl !== 'https://' &&
|
|
82
|
+
referer.indexOf(publicUrl) !== 0
|
|
83
|
+
) {
|
|
88
84
|
return response.status(401).send('you are not authorized to perform this action');
|
|
89
85
|
}
|
|
90
86
|
// Temporarily fix Vercel local image issue
|
|
91
|
-
if (url.indexOf('http') === -1 && isVercel()) {
|
|
92
|
-
response.redirect(307, url);
|
|
87
|
+
if (imageOptions.url.indexOf('http') === -1 && isVercel()) {
|
|
88
|
+
response.redirect(307, imageOptions.url);
|
|
93
89
|
}
|
|
94
90
|
// Reading the original image
|
|
95
|
-
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
91
|
+
const originalImageBuffer = await getOriginalImage(imageOptions.url, domains, request.headers);
|
|
96
92
|
// Checking if the original image exists
|
|
97
93
|
if (originalImageBuffer) {
|
|
98
94
|
// Original image
|
|
99
95
|
const originalImage = sharp(originalImageBuffer);
|
|
100
96
|
// Resizing the original image and converting it to WebP format
|
|
101
|
-
const resizedImage = await resizeImage(originalImage,
|
|
97
|
+
const resizedImage = await resizeImage(originalImage, imageOptions);
|
|
102
98
|
// Converting resized image to WebP format
|
|
103
99
|
const compressedImage = resizedImage
|
|
104
100
|
.webp({
|
|
105
|
-
quality:
|
|
101
|
+
quality: imageOptions.q,
|
|
106
102
|
// nearLossless: true,
|
|
107
103
|
});
|
|
108
104
|
// Saving the resized image to the cache
|
|
@@ -120,16 +116,118 @@ export function imageHandler({
|
|
|
120
116
|
// Sending data in the response
|
|
121
117
|
return response.end(data);
|
|
122
118
|
}
|
|
123
|
-
} catch (error) {
|
|
119
|
+
} catch (error: any) {
|
|
124
120
|
// Logging an error message and throwing the error
|
|
125
121
|
console.error('imageHandler.error', error);
|
|
126
|
-
|
|
122
|
+
return response.status(error.status || 500).json({
|
|
123
|
+
errors: [{
|
|
124
|
+
message: error.message || error.name || error.statusText || 'An error occurred',
|
|
125
|
+
}],
|
|
126
|
+
});
|
|
127
127
|
}
|
|
128
128
|
},
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
function getImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {
|
|
133
|
+
// Destructuring 'slugs', 'origin', 'search', 'w', 'h', 'x', 'y', 'q' from the request query
|
|
134
|
+
const { slugs, origin = '', search = '', w, h, x, y, q } = request.query;
|
|
135
|
+
// Checking slugs
|
|
136
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
137
|
+
// console.error('imageHandler', 'missing slugs parameter');
|
|
138
|
+
throw 'missing slugs parameter';
|
|
139
|
+
}
|
|
140
|
+
// Parsing 'w' to an integer and assigning it to 'width'
|
|
141
|
+
const width = typeof w === 'string' ? parseInt(w) : undefined;
|
|
142
|
+
// Checking if 'width' is not defined
|
|
143
|
+
if (!width) {
|
|
144
|
+
// Logging an error message and returning a 400 status code with a message
|
|
145
|
+
// console.error('imageHandler', 'missing w parameter');
|
|
146
|
+
throw 'missing w parameter';
|
|
147
|
+
}
|
|
148
|
+
// if (width > 2048) {
|
|
149
|
+
if (!options.sizes.includes(width)) {
|
|
150
|
+
throw 'invalid w parameter';
|
|
151
|
+
}
|
|
152
|
+
// Parsing 'h' to an integer and assigning it to 'height'
|
|
153
|
+
const height = typeof h === 'string' ? parseInt(h) : undefined;
|
|
154
|
+
// Parsing 'x' to an integer and assigning it to 'focalX'
|
|
155
|
+
const focalX = typeof x === 'string' ? parseInt(x) : IMAGE_FOCAL_X;
|
|
156
|
+
// Parsing 'y' to an integer and assigning it to 'focalY'
|
|
157
|
+
const focalY = typeof y === 'string' ? parseInt(y) : IMAGE_FOCAL_Y;
|
|
158
|
+
// Parsing 'q' to an integer, ensuring it's between 30 and 100, and assigning it to 'imageQuality'
|
|
159
|
+
const imageQuality = Math.max(30, Math.min(100, (typeof q === 'string' ? parseInt(q) : options.quality)));
|
|
160
|
+
// if (width > 2048) {
|
|
161
|
+
if (imageQuality !== options.quality) {
|
|
162
|
+
throw 'invalid q parameter';
|
|
163
|
+
}
|
|
164
|
+
// Generating url
|
|
165
|
+
const url = origin + '/' + slugs.join('/') + search;
|
|
166
|
+
//
|
|
167
|
+
return height ? {
|
|
168
|
+
w: width,
|
|
169
|
+
h: height,
|
|
170
|
+
x: focalX,
|
|
171
|
+
y: focalY,
|
|
172
|
+
q: imageQuality,
|
|
173
|
+
src: url,
|
|
174
|
+
url: url,
|
|
175
|
+
origin: String(origin),
|
|
176
|
+
search: String(search),
|
|
177
|
+
} : {
|
|
178
|
+
w: width,
|
|
179
|
+
q: imageQuality,
|
|
180
|
+
src: url,
|
|
181
|
+
url: url,
|
|
182
|
+
origin: String(origin),
|
|
183
|
+
search: String(search),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getEncryptedImageOptions(request: NextApiRequest, options: IImageHandlerOptions): IImageOptions {
|
|
188
|
+
// Destructuring 'slugs', 'origin', 'search', 't' from the request query
|
|
189
|
+
const { slugs, origin = '', search = '', t } = request.query;
|
|
190
|
+
// Checking slugs
|
|
191
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
192
|
+
// console.error('imageHandler', 'missing slugs parameter');
|
|
193
|
+
throw 'missing slugs parameter';
|
|
194
|
+
}
|
|
195
|
+
if (typeof t !== 'string') {
|
|
196
|
+
throw 'missing t parameter';
|
|
197
|
+
}
|
|
198
|
+
const imageResize = decryptImageResize(t);
|
|
199
|
+
/*
|
|
200
|
+
// Checking if 'w' is not defined
|
|
201
|
+
if (!imageResize.w) {
|
|
202
|
+
throw 'missing w parameter';
|
|
203
|
+
}
|
|
204
|
+
// if (width > 2048) {
|
|
205
|
+
if (!options.sizes.includes(imageResize.w)) {
|
|
206
|
+
throw 'invalid w parameter';
|
|
207
|
+
}
|
|
208
|
+
// Ensuring 'q' is between 30 and 100, and assigning it to 'imageQuality'
|
|
209
|
+
const imageQuality = Math.max(30, Math.min(100, imageResize.q || options.quality));
|
|
210
|
+
if (imageQuality !== options.quality) {
|
|
211
|
+
throw 'invalid q parameter';
|
|
212
|
+
}
|
|
213
|
+
*/
|
|
214
|
+
// Generating url
|
|
215
|
+
const url = origin + '/' + slugs.join('/') + search;
|
|
216
|
+
//
|
|
217
|
+
if (imageResize.src !== url) {
|
|
218
|
+
throw 'invalid src parameter';
|
|
219
|
+
}
|
|
220
|
+
//
|
|
221
|
+
return {
|
|
222
|
+
...imageResize,
|
|
223
|
+
url: url,
|
|
224
|
+
origin: String(origin),
|
|
225
|
+
search: String(search),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function resizeImage(originalImage: sharp.Sharp, imageOptions: IImageOptions): Promise<sharp.Sharp> {
|
|
230
|
+
const { w: resizeWidth, h: resizeHeight, x: focalX = IMAGE_FOCAL_X, y: focalY = IMAGE_FOCAL_Y } = imageOptions;
|
|
133
231
|
const defaultResizeOptions = () => {
|
|
134
232
|
return originalImage.resize({
|
|
135
233
|
width: resizeWidth,
|
|
@@ -139,7 +237,7 @@ async function resizeImage(originalImage: sharp.Sharp, resizeWidth: number, resi
|
|
|
139
237
|
withoutEnlargement: true,
|
|
140
238
|
});
|
|
141
239
|
};
|
|
142
|
-
if (resizeHeight && (focalX !==
|
|
240
|
+
if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {
|
|
143
241
|
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
144
242
|
if (originalWidth && originalHeight) {
|
|
145
243
|
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
@@ -287,9 +385,9 @@ function isVercel(): boolean {
|
|
|
287
385
|
}
|
|
288
386
|
|
|
289
387
|
// Defining the 'urlToCachedName' function
|
|
290
|
-
function urlToCachedName(
|
|
388
|
+
function urlToCachedName(options: IImageOptions): string {
|
|
291
389
|
// Converting the URL to a file name
|
|
292
|
-
const fileName = urlToFileName(url);
|
|
390
|
+
const fileName = urlToFileName(options.url);
|
|
293
391
|
// Parsing the file name
|
|
294
392
|
const parsedName = path.parse(fileName);
|
|
295
393
|
// Defining the cache directory
|
|
@@ -302,7 +400,9 @@ function urlToCachedName(url: string, width: number, height: number | undefined,
|
|
|
302
400
|
fs.mkdirSync(cacheDir);
|
|
303
401
|
}
|
|
304
402
|
// Generating the cached name
|
|
305
|
-
const cachedName =
|
|
403
|
+
const cachedName = options.h ?
|
|
404
|
+
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}`) :
|
|
405
|
+
path.join(cacheDir, `${parsedName.name}_w_${options.w}_q_${options.q}${parsedName.ext}`);
|
|
306
406
|
// Returning the cached name
|
|
307
407
|
return cachedName;
|
|
308
408
|
}
|