@websolutespa/bom-mixer-image 1.0.9 → 1.8.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 +13 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +90 -25
- package/dist/index.mjs +90 -25
- package/package.json +2 -2
- package/src/image/image.handler.ts +118 -31
package/CHANGELOG.md
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -68,25 +68,36 @@ function errorHandler(error, response) {
|
|
|
68
68
|
|
|
69
69
|
// src/image/image.handler.ts
|
|
70
70
|
var IMAGE_MAX_AGE = 604800;
|
|
71
|
-
|
|
71
|
+
var IMAGE_QUALITY = 90;
|
|
72
|
+
function imageHandler({
|
|
73
|
+
domains = [],
|
|
74
|
+
sizes = [],
|
|
75
|
+
quality = IMAGE_QUALITY
|
|
76
|
+
}) {
|
|
72
77
|
return apiHandler({
|
|
73
78
|
// Defining the 'get' method for handling GET requests
|
|
74
79
|
get: async (request, response) => {
|
|
75
80
|
try {
|
|
76
|
-
const {
|
|
77
|
-
if (
|
|
78
|
-
return response.status(400).send("missing
|
|
81
|
+
const { slugs, origin = "", search = "", w, h, x, y, q } = request.query;
|
|
82
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
83
|
+
return response.status(400).send("missing slugs parameter");
|
|
79
84
|
}
|
|
80
85
|
const width = typeof w === "string" ? parseInt(w) : void 0;
|
|
81
86
|
if (!width) {
|
|
82
87
|
return response.status(400).send("missing w parameter");
|
|
83
88
|
}
|
|
84
|
-
if (width
|
|
89
|
+
if (!sizes.includes(width)) {
|
|
85
90
|
return response.status(400).send("invalid w parameter");
|
|
86
91
|
}
|
|
87
92
|
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
88
|
-
const
|
|
89
|
-
const
|
|
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");
|
|
98
|
+
}
|
|
99
|
+
const url = origin + "/" + slugs.join("/") + search;
|
|
100
|
+
const cachedName = urlToCachedName(url, width, height, focalX, focalY, imageQuality);
|
|
90
101
|
if (fs.existsSync(cachedName)) {
|
|
91
102
|
const cachedImage = await getCachedImage(cachedName);
|
|
92
103
|
response.setHeader("Content-Type", "image/webp");
|
|
@@ -94,23 +105,25 @@ function imageHandler({ domains = [] }) {
|
|
|
94
105
|
response.setHeader("Cache-Control", `public, max-age=${IMAGE_MAX_AGE}`);
|
|
95
106
|
return response.send(cachedImage);
|
|
96
107
|
}
|
|
108
|
+
const { headers } = request;
|
|
109
|
+
const referer = headers.referer;
|
|
110
|
+
const publicUrl = process.env.NEXT_PUBLIC_URL || "";
|
|
111
|
+
if (!referer || !publicUrl || referer.indexOf(publicUrl) !== 0) {
|
|
112
|
+
return response.status(401).send("you are not authorized to perform this action");
|
|
113
|
+
}
|
|
97
114
|
if (url.indexOf("http") === -1 && isVercel()) {
|
|
98
115
|
response.redirect(307, url);
|
|
99
116
|
}
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
position: import_sharp.default.strategy.entropy,
|
|
107
|
-
withoutEnlargement: true
|
|
108
|
-
}).webp({
|
|
109
|
-
quality
|
|
117
|
+
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
118
|
+
if (originalImageBuffer) {
|
|
119
|
+
const originalImage = (0, import_sharp.default)(originalImageBuffer);
|
|
120
|
+
const resizedImage = await resizeImage(originalImage, width, height, focalX, focalY);
|
|
121
|
+
const compressedImage = resizedImage.webp({
|
|
122
|
+
quality: imageQuality
|
|
110
123
|
// nearLossless: true,
|
|
111
124
|
});
|
|
112
|
-
|
|
113
|
-
const data = await
|
|
125
|
+
compressedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("imageHandler.sharp.error", cachedName, error));
|
|
126
|
+
const data = await compressedImage.toBuffer();
|
|
114
127
|
response.setHeader("Content-Type", "image/webp");
|
|
115
128
|
response.setHeader("Expires", new Date(Date.now() + IMAGE_MAX_AGE * 1e3).toUTCString());
|
|
116
129
|
response.setHeader("Cache-Control", `public, max-age=${IMAGE_MAX_AGE}`);
|
|
@@ -123,12 +136,61 @@ function imageHandler({ domains = [] }) {
|
|
|
123
136
|
}
|
|
124
137
|
});
|
|
125
138
|
}
|
|
139
|
+
async function resizeImage(originalImage, resizeWidth, resizeHeight, focalX, focalY) {
|
|
140
|
+
const defaultResizeOptions = () => {
|
|
141
|
+
return originalImage.resize({
|
|
142
|
+
width: resizeWidth,
|
|
143
|
+
height: resizeHeight ? resizeHeight : void 0,
|
|
144
|
+
fit: import_sharp.default.fit.cover,
|
|
145
|
+
position: import_sharp.default.strategy.attention,
|
|
146
|
+
withoutEnlargement: true
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
if (resizeHeight && (focalX !== 50 || focalY !== 50)) {
|
|
150
|
+
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
151
|
+
if (originalWidth && originalHeight) {
|
|
152
|
+
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
153
|
+
const originalAspectRatio = originalWidth / originalHeight;
|
|
154
|
+
const prioritizeHeight = resizeAspectRatio < originalAspectRatio;
|
|
155
|
+
const imageToResize = originalImage.clone();
|
|
156
|
+
const scaledImage = imageToResize.resize(prioritizeHeight ? {
|
|
157
|
+
height: resizeHeight,
|
|
158
|
+
fit: import_sharp.default.fit.cover
|
|
159
|
+
} : {
|
|
160
|
+
width: resizeWidth,
|
|
161
|
+
fit: import_sharp.default.fit.cover
|
|
162
|
+
});
|
|
163
|
+
const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true });
|
|
164
|
+
const extractWidth = resizeWidth ?? scaledImageInfo.width;
|
|
165
|
+
const maxOffsetX = scaledImageInfo.width - extractWidth;
|
|
166
|
+
const leftFocalEdge = Math.round(
|
|
167
|
+
scaledImageInfo.width * (focalX / 100) - extractWidth / 2
|
|
168
|
+
);
|
|
169
|
+
const extractX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX);
|
|
170
|
+
const extractHeight = resizeHeight ?? scaledImageInfo.height;
|
|
171
|
+
const maxOffsetY = scaledImageInfo.height - extractHeight;
|
|
172
|
+
const topFocalEdge = Math.round(
|
|
173
|
+
scaledImageInfo.height * (focalY / 100) - extractHeight / 2
|
|
174
|
+
);
|
|
175
|
+
const extractY = Math.min(Math.max(0, topFocalEdge), maxOffsetY);
|
|
176
|
+
return scaledImage.extract({
|
|
177
|
+
left: extractX,
|
|
178
|
+
top: extractY,
|
|
179
|
+
width: extractWidth,
|
|
180
|
+
height: extractHeight
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
return defaultResizeOptions();
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
return defaultResizeOptions();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
126
189
|
function getOriginalImage(url, domains, headers, skipUrlValidity = false) {
|
|
127
190
|
return new Promise((resolve, reject) => {
|
|
128
191
|
if (url.indexOf("http") === 0) {
|
|
129
192
|
if (!skipUrlValidity && !checkUrlValidity(url, domains)) {
|
|
130
|
-
|
|
131
|
-
return reject({ status: 401, message: "invalid url" });
|
|
193
|
+
return reject({ status: 401, message: "invalid origin" });
|
|
132
194
|
}
|
|
133
195
|
fetch(url, {
|
|
134
196
|
method: "GET",
|
|
@@ -170,21 +232,24 @@ function getCachedImage(pathname) {
|
|
|
170
232
|
});
|
|
171
233
|
}
|
|
172
234
|
function urlToFileName(url) {
|
|
173
|
-
|
|
174
|
-
|
|
235
|
+
const segments = url.split("?");
|
|
236
|
+
const path2 = segments[0];
|
|
237
|
+
const query = segments[1];
|
|
238
|
+
let uniqueName = path2.replace("://", "_").replace(/\.|\/|\?|=/g, "_");
|
|
239
|
+
uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf("_")) + (query ? "_" + query.replace(/\.|\/|\?|=/g, "_") : "") + "." + uniqueName.substring(uniqueName.lastIndexOf("_") + 1, uniqueName.length);
|
|
175
240
|
return uniqueName;
|
|
176
241
|
}
|
|
177
242
|
function isVercel() {
|
|
178
243
|
return process && process.env && String(process.env.VERCEL) === "1";
|
|
179
244
|
}
|
|
180
|
-
function urlToCachedName(url, width, height, quality) {
|
|
245
|
+
function urlToCachedName(url, width, height, focalX, focalY, quality) {
|
|
181
246
|
const fileName = urlToFileName(url);
|
|
182
247
|
const parsedName = path.parse(fileName);
|
|
183
248
|
const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
|
|
184
249
|
if (!fs.existsSync(cacheDir)) {
|
|
185
250
|
fs.mkdirSync(cacheDir);
|
|
186
251
|
}
|
|
187
|
-
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_q_${quality}${parsedName.ext}`);
|
|
252
|
+
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_x_${focalX || 50}_y_${focalY || 50}_q_${quality}${parsedName.ext}`);
|
|
188
253
|
return cachedName;
|
|
189
254
|
}
|
|
190
255
|
function checkUrlValidity(src, domains) {
|
package/dist/index.mjs
CHANGED
|
@@ -32,25 +32,36 @@ function errorHandler(error, response) {
|
|
|
32
32
|
|
|
33
33
|
// src/image/image.handler.ts
|
|
34
34
|
var IMAGE_MAX_AGE = 604800;
|
|
35
|
-
|
|
35
|
+
var IMAGE_QUALITY = 90;
|
|
36
|
+
function imageHandler({
|
|
37
|
+
domains = [],
|
|
38
|
+
sizes = [],
|
|
39
|
+
quality = IMAGE_QUALITY
|
|
40
|
+
}) {
|
|
36
41
|
return apiHandler({
|
|
37
42
|
// Defining the 'get' method for handling GET requests
|
|
38
43
|
get: async (request, response) => {
|
|
39
44
|
try {
|
|
40
|
-
const {
|
|
41
|
-
if (
|
|
42
|
-
return response.status(400).send("missing
|
|
45
|
+
const { slugs, origin = "", search = "", w, h, x, y, q } = request.query;
|
|
46
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
47
|
+
return response.status(400).send("missing slugs parameter");
|
|
43
48
|
}
|
|
44
49
|
const width = typeof w === "string" ? parseInt(w) : void 0;
|
|
45
50
|
if (!width) {
|
|
46
51
|
return response.status(400).send("missing w parameter");
|
|
47
52
|
}
|
|
48
|
-
if (width
|
|
53
|
+
if (!sizes.includes(width)) {
|
|
49
54
|
return response.status(400).send("invalid w parameter");
|
|
50
55
|
}
|
|
51
56
|
const height = typeof h === "string" ? parseInt(h) : void 0;
|
|
52
|
-
const
|
|
53
|
-
const
|
|
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");
|
|
62
|
+
}
|
|
63
|
+
const url = origin + "/" + slugs.join("/") + search;
|
|
64
|
+
const cachedName = urlToCachedName(url, width, height, focalX, focalY, imageQuality);
|
|
54
65
|
if (fs.existsSync(cachedName)) {
|
|
55
66
|
const cachedImage = await getCachedImage(cachedName);
|
|
56
67
|
response.setHeader("Content-Type", "image/webp");
|
|
@@ -58,23 +69,25 @@ function imageHandler({ domains = [] }) {
|
|
|
58
69
|
response.setHeader("Cache-Control", `public, max-age=${IMAGE_MAX_AGE}`);
|
|
59
70
|
return response.send(cachedImage);
|
|
60
71
|
}
|
|
72
|
+
const { headers } = request;
|
|
73
|
+
const referer = headers.referer;
|
|
74
|
+
const publicUrl = process.env.NEXT_PUBLIC_URL || "";
|
|
75
|
+
if (!referer || !publicUrl || referer.indexOf(publicUrl) !== 0) {
|
|
76
|
+
return response.status(401).send("you are not authorized to perform this action");
|
|
77
|
+
}
|
|
61
78
|
if (url.indexOf("http") === -1 && isVercel()) {
|
|
62
79
|
response.redirect(307, url);
|
|
63
80
|
}
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
position: sharp.strategy.entropy,
|
|
71
|
-
withoutEnlargement: true
|
|
72
|
-
}).webp({
|
|
73
|
-
quality
|
|
81
|
+
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
82
|
+
if (originalImageBuffer) {
|
|
83
|
+
const originalImage = sharp(originalImageBuffer);
|
|
84
|
+
const resizedImage = await resizeImage(originalImage, width, height, focalX, focalY);
|
|
85
|
+
const compressedImage = resizedImage.webp({
|
|
86
|
+
quality: imageQuality
|
|
74
87
|
// nearLossless: true,
|
|
75
88
|
});
|
|
76
|
-
|
|
77
|
-
const data = await
|
|
89
|
+
compressedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("imageHandler.sharp.error", cachedName, error));
|
|
90
|
+
const data = await compressedImage.toBuffer();
|
|
78
91
|
response.setHeader("Content-Type", "image/webp");
|
|
79
92
|
response.setHeader("Expires", new Date(Date.now() + IMAGE_MAX_AGE * 1e3).toUTCString());
|
|
80
93
|
response.setHeader("Cache-Control", `public, max-age=${IMAGE_MAX_AGE}`);
|
|
@@ -87,12 +100,61 @@ function imageHandler({ domains = [] }) {
|
|
|
87
100
|
}
|
|
88
101
|
});
|
|
89
102
|
}
|
|
103
|
+
async function resizeImage(originalImage, resizeWidth, resizeHeight, focalX, focalY) {
|
|
104
|
+
const defaultResizeOptions = () => {
|
|
105
|
+
return originalImage.resize({
|
|
106
|
+
width: resizeWidth,
|
|
107
|
+
height: resizeHeight ? resizeHeight : void 0,
|
|
108
|
+
fit: sharp.fit.cover,
|
|
109
|
+
position: sharp.strategy.attention,
|
|
110
|
+
withoutEnlargement: true
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
if (resizeHeight && (focalX !== 50 || focalY !== 50)) {
|
|
114
|
+
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
115
|
+
if (originalWidth && originalHeight) {
|
|
116
|
+
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
117
|
+
const originalAspectRatio = originalWidth / originalHeight;
|
|
118
|
+
const prioritizeHeight = resizeAspectRatio < originalAspectRatio;
|
|
119
|
+
const imageToResize = originalImage.clone();
|
|
120
|
+
const scaledImage = imageToResize.resize(prioritizeHeight ? {
|
|
121
|
+
height: resizeHeight,
|
|
122
|
+
fit: sharp.fit.cover
|
|
123
|
+
} : {
|
|
124
|
+
width: resizeWidth,
|
|
125
|
+
fit: sharp.fit.cover
|
|
126
|
+
});
|
|
127
|
+
const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true });
|
|
128
|
+
const extractWidth = resizeWidth ?? scaledImageInfo.width;
|
|
129
|
+
const maxOffsetX = scaledImageInfo.width - extractWidth;
|
|
130
|
+
const leftFocalEdge = Math.round(
|
|
131
|
+
scaledImageInfo.width * (focalX / 100) - extractWidth / 2
|
|
132
|
+
);
|
|
133
|
+
const extractX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX);
|
|
134
|
+
const extractHeight = resizeHeight ?? scaledImageInfo.height;
|
|
135
|
+
const maxOffsetY = scaledImageInfo.height - extractHeight;
|
|
136
|
+
const topFocalEdge = Math.round(
|
|
137
|
+
scaledImageInfo.height * (focalY / 100) - extractHeight / 2
|
|
138
|
+
);
|
|
139
|
+
const extractY = Math.min(Math.max(0, topFocalEdge), maxOffsetY);
|
|
140
|
+
return scaledImage.extract({
|
|
141
|
+
left: extractX,
|
|
142
|
+
top: extractY,
|
|
143
|
+
width: extractWidth,
|
|
144
|
+
height: extractHeight
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
return defaultResizeOptions();
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
return defaultResizeOptions();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
90
153
|
function getOriginalImage(url, domains, headers, skipUrlValidity = false) {
|
|
91
154
|
return new Promise((resolve, reject) => {
|
|
92
155
|
if (url.indexOf("http") === 0) {
|
|
93
156
|
if (!skipUrlValidity && !checkUrlValidity(url, domains)) {
|
|
94
|
-
|
|
95
|
-
return reject({ status: 401, message: "invalid url" });
|
|
157
|
+
return reject({ status: 401, message: "invalid origin" });
|
|
96
158
|
}
|
|
97
159
|
fetch(url, {
|
|
98
160
|
method: "GET",
|
|
@@ -134,21 +196,24 @@ function getCachedImage(pathname) {
|
|
|
134
196
|
});
|
|
135
197
|
}
|
|
136
198
|
function urlToFileName(url) {
|
|
137
|
-
|
|
138
|
-
|
|
199
|
+
const segments = url.split("?");
|
|
200
|
+
const path2 = segments[0];
|
|
201
|
+
const query = segments[1];
|
|
202
|
+
let uniqueName = path2.replace("://", "_").replace(/\.|\/|\?|=/g, "_");
|
|
203
|
+
uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf("_")) + (query ? "_" + query.replace(/\.|\/|\?|=/g, "_") : "") + "." + uniqueName.substring(uniqueName.lastIndexOf("_") + 1, uniqueName.length);
|
|
139
204
|
return uniqueName;
|
|
140
205
|
}
|
|
141
206
|
function isVercel() {
|
|
142
207
|
return process && process.env && String(process.env.VERCEL) === "1";
|
|
143
208
|
}
|
|
144
|
-
function urlToCachedName(url, width, height, quality) {
|
|
209
|
+
function urlToCachedName(url, width, height, focalX, focalY, quality) {
|
|
145
210
|
const fileName = urlToFileName(url);
|
|
146
211
|
const parsedName = path.parse(fileName);
|
|
147
212
|
const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
|
|
148
213
|
if (!fs.existsSync(cacheDir)) {
|
|
149
214
|
fs.mkdirSync(cacheDir);
|
|
150
215
|
}
|
|
151
|
-
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_q_${quality}${parsedName.ext}`);
|
|
216
|
+
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_x_${focalX || 50}_y_${focalY || 50}_q_${quality}${parsedName.ext}`);
|
|
152
217
|
return cachedName;
|
|
153
218
|
}
|
|
154
219
|
function checkUrlValidity(src, domains) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@websolutespa/bom-mixer-image",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Mixer Image module of the BOM Repository",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bom",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"lint": "eslint *.ts*",
|
|
17
17
|
"test": "jest",
|
|
18
|
-
"test-watch": "npm test -- --watch",
|
|
18
|
+
"test-watch": "npm test -- --watch --silent=false",
|
|
19
19
|
"coverage:integration": "npm run test -- --coverage",
|
|
20
20
|
"coverage": "run-s -c --silent coverage:*",
|
|
21
21
|
"compile": "tsup ./src/index.ts --format esm,cjs --dts",
|
|
@@ -11,21 +11,31 @@ import sharp from 'sharp';
|
|
|
11
11
|
import { apiHandler } from '../api/api.handler';
|
|
12
12
|
|
|
13
13
|
const IMAGE_MAX_AGE = 604800;
|
|
14
|
+
const IMAGE_FOCAL_X = 50;
|
|
15
|
+
const IMAGE_FOCAL_Y = 50;
|
|
16
|
+
const IMAGE_QUALITY = 90;
|
|
14
17
|
|
|
15
18
|
// Exporting the 'imageHandler' function
|
|
16
|
-
export function imageHandler({
|
|
19
|
+
export function imageHandler({
|
|
20
|
+
domains = [],
|
|
21
|
+
sizes = [],
|
|
22
|
+
quality = IMAGE_QUALITY,
|
|
23
|
+
}: {
|
|
24
|
+
domains: string[];
|
|
25
|
+
sizes: number[];
|
|
26
|
+
quality: number;
|
|
27
|
+
}) {
|
|
17
28
|
// Returning the 'apiHandler' function with a configuration object
|
|
18
29
|
return apiHandler({
|
|
19
30
|
// Defining the 'get' method for handling GET requests
|
|
20
31
|
get: async (request: NextApiRequest, response: NextApiResponse) => {
|
|
21
32
|
try {
|
|
22
|
-
// Destructuring 'url', 'w', 'h', 'q' from the request query
|
|
23
|
-
const {
|
|
24
|
-
// Checking
|
|
25
|
-
if (
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
return response.status(400).send('missing url parameter');
|
|
33
|
+
// Destructuring 'url', 'w', 'h', 'x', 'y', 'q' from the request query
|
|
34
|
+
const { slugs, origin = '', search = '', w, h, x, y, q } = request.query;
|
|
35
|
+
// Checking slugs
|
|
36
|
+
if (!Array.isArray(slugs) || slugs.length === 0) {
|
|
37
|
+
// console.error('imageHandler', 'missing slugs parameter');
|
|
38
|
+
return response.status(400).send('missing slugs parameter');
|
|
29
39
|
}
|
|
30
40
|
// Parsing 'w' to an integer and assigning it to 'width'
|
|
31
41
|
const width = typeof w === 'string' ? parseInt(w) : undefined;
|
|
@@ -35,15 +45,26 @@ export function imageHandler({ domains = [] }: { domains: string[] }) {
|
|
|
35
45
|
// console.error('imageHandler', 'missing w parameter');
|
|
36
46
|
return response.status(400).send('missing w parameter');
|
|
37
47
|
}
|
|
38
|
-
if (width > 2048) {
|
|
48
|
+
// if (width > 2048) {
|
|
49
|
+
if (!sizes.includes(width)) {
|
|
39
50
|
return response.status(400).send('invalid w parameter');
|
|
40
51
|
}
|
|
41
52
|
// Parsing 'h' to an integer and assigning it to 'height'
|
|
42
53
|
const height = typeof h === 'string' ? parseInt(h) : undefined;
|
|
43
|
-
// Parsing '
|
|
44
|
-
const
|
|
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');
|
|
63
|
+
}
|
|
64
|
+
// Generating url
|
|
65
|
+
const url = origin + '/' + slugs.join('/') + search;
|
|
45
66
|
// Generating a cached name for the image
|
|
46
|
-
const cachedName = urlToCachedName(url, width, height,
|
|
67
|
+
const cachedName = urlToCachedName(url, width, height, focalX, focalY, imageQuality);
|
|
47
68
|
// Checking if the cached image exists
|
|
48
69
|
if (fs.existsSync(cachedName)) {
|
|
49
70
|
// console.log('imageHandler.fromCache', cachedName);
|
|
@@ -58,33 +79,38 @@ export function imageHandler({ domains = [] }: { domains: string[] }) {
|
|
|
58
79
|
// Ending the response and sending the cached image
|
|
59
80
|
return response.send(cachedImage);
|
|
60
81
|
}
|
|
82
|
+
// Check referrer
|
|
83
|
+
const { headers } = request;
|
|
84
|
+
const referer = headers.referer;
|
|
85
|
+
const publicUrl = process.env.NEXT_PUBLIC_URL || '';
|
|
86
|
+
// console.log('referef', referer, 'publicUrl', publicUrl);
|
|
87
|
+
if (!referer || !publicUrl || referer.indexOf(publicUrl) !== 0) {
|
|
88
|
+
return response.status(401).send('you are not authorized to perform this action');
|
|
89
|
+
}
|
|
61
90
|
// Temporarily fix Vercel local image issue
|
|
62
91
|
if (url.indexOf('http') === -1 && isVercel()) {
|
|
63
92
|
response.redirect(307, url);
|
|
64
93
|
}
|
|
65
94
|
// Reading the original image
|
|
66
|
-
const
|
|
95
|
+
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
67
96
|
// Checking if the original image exists
|
|
68
|
-
if (
|
|
97
|
+
if (originalImageBuffer) {
|
|
98
|
+
// Original image
|
|
99
|
+
const originalImage = sharp(originalImageBuffer);
|
|
69
100
|
// Resizing the original image and converting it to WebP format
|
|
70
|
-
const resizedImage =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
height: height ? height : undefined,
|
|
74
|
-
fit: sharp.fit.cover,
|
|
75
|
-
position: sharp.strategy.entropy,
|
|
76
|
-
withoutEnlargement: true,
|
|
77
|
-
})
|
|
101
|
+
const resizedImage = await resizeImage(originalImage, width, height, focalX, focalY);
|
|
102
|
+
// Converting resized image to WebP format
|
|
103
|
+
const compressedImage = resizedImage
|
|
78
104
|
.webp({
|
|
79
|
-
quality,
|
|
105
|
+
quality: imageQuality,
|
|
80
106
|
// nearLossless: true,
|
|
81
107
|
});
|
|
82
108
|
// Saving the resized image to the cache
|
|
83
|
-
|
|
109
|
+
compressedImage.toFile(cachedName)
|
|
84
110
|
.then(() => console.log('imageHandler.sharp.cached', cachedName))
|
|
85
|
-
.catch((error: any) => console.error('error
|
|
111
|
+
.catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));
|
|
86
112
|
// Converting the resized image to a buffer
|
|
87
|
-
const data = await
|
|
113
|
+
const data = await compressedImage.toBuffer();
|
|
88
114
|
// Setting the 'Content-Type' header to 'image/webp'
|
|
89
115
|
response.setHeader('Content-Type', 'image/webp');
|
|
90
116
|
// Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)
|
|
@@ -103,6 +129,63 @@ export function imageHandler({ domains = [] }: { domains: string[] }) {
|
|
|
103
129
|
});
|
|
104
130
|
}
|
|
105
131
|
|
|
132
|
+
async function resizeImage(originalImage: sharp.Sharp, resizeWidth: number, resizeHeight: number | undefined, focalX: number, focalY: number): Promise<sharp.Sharp> {
|
|
133
|
+
const defaultResizeOptions = () => {
|
|
134
|
+
return originalImage.resize({
|
|
135
|
+
width: resizeWidth,
|
|
136
|
+
height: resizeHeight ? resizeHeight : undefined,
|
|
137
|
+
fit: sharp.fit.cover,
|
|
138
|
+
position: sharp.strategy.attention,
|
|
139
|
+
withoutEnlargement: true,
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
if (resizeHeight && (focalX !== 50 || focalY !== 50)) {
|
|
143
|
+
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
144
|
+
if (originalWidth && originalHeight) {
|
|
145
|
+
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
146
|
+
const originalAspectRatio = originalWidth / originalHeight;
|
|
147
|
+
const prioritizeHeight = resizeAspectRatio < originalAspectRatio;
|
|
148
|
+
const imageToResize = originalImage.clone();
|
|
149
|
+
// Scale the image up or down to fit the resize dimensions
|
|
150
|
+
const scaledImage = imageToResize.resize(prioritizeHeight ? {
|
|
151
|
+
height: resizeHeight,
|
|
152
|
+
fit: sharp.fit.cover,
|
|
153
|
+
} : {
|
|
154
|
+
width: resizeWidth,
|
|
155
|
+
fit: sharp.fit.cover,
|
|
156
|
+
});
|
|
157
|
+
const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true });
|
|
158
|
+
// extractWidth
|
|
159
|
+
const extractWidth = resizeWidth ?? scaledImageInfo.width;
|
|
160
|
+
const maxOffsetX = scaledImageInfo.width - extractWidth;
|
|
161
|
+
const leftFocalEdge = Math.round(
|
|
162
|
+
scaledImageInfo.width * (focalX / 100) - extractWidth / 2
|
|
163
|
+
);
|
|
164
|
+
// extractX
|
|
165
|
+
const extractX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX);
|
|
166
|
+
// extractHeight
|
|
167
|
+
const extractHeight = resizeHeight ?? scaledImageInfo.height;
|
|
168
|
+
const maxOffsetY = scaledImageInfo.height - extractHeight;
|
|
169
|
+
const topFocalEdge = Math.round(
|
|
170
|
+
scaledImageInfo.height * (focalY / 100) - extractHeight / 2
|
|
171
|
+
);
|
|
172
|
+
// extractY
|
|
173
|
+
const extractY = Math.min(Math.max(0, topFocalEdge), maxOffsetY);
|
|
174
|
+
// Extract the focal area from the scaled image
|
|
175
|
+
return scaledImage.extract({
|
|
176
|
+
left: extractX,
|
|
177
|
+
top: extractY,
|
|
178
|
+
width: extractWidth,
|
|
179
|
+
height: extractHeight,
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
return defaultResizeOptions();
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
return defaultResizeOptions();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
106
189
|
// Defining the 'getOriginalImage' function
|
|
107
190
|
function getOriginalImage(url: string, domains: string[], headers: IncomingHttpHeaders, skipUrlValidity: boolean = false): Promise<Buffer> {
|
|
108
191
|
// Returning a new promise
|
|
@@ -113,8 +196,8 @@ function getOriginalImage(url: string, domains: string[], headers: IncomingHttpH
|
|
|
113
196
|
!skipUrlValidity &&
|
|
114
197
|
!checkUrlValidity(url, domains)
|
|
115
198
|
) {
|
|
116
|
-
console.error('imageHandler.getOriginalImage.error invalid
|
|
117
|
-
return reject({ status: 401, message: 'invalid
|
|
199
|
+
// console.error('imageHandler.getOriginalImage.error invalid origin');
|
|
200
|
+
return reject({ status: 401, message: 'invalid origin' });
|
|
118
201
|
}
|
|
119
202
|
// Fetching the image from the URL
|
|
120
203
|
fetch(url, {
|
|
@@ -187,9 +270,13 @@ function getCachedImage(pathname: string): Promise<Buffer> {
|
|
|
187
270
|
// Defining the 'urlToFileName' function
|
|
188
271
|
function urlToFileName(url: string): string {
|
|
189
272
|
// Replacing useless characters with UNDERSCORE
|
|
190
|
-
|
|
273
|
+
const segments = url.split('?');
|
|
274
|
+
const path = segments[0];
|
|
275
|
+
const query = segments[1];
|
|
276
|
+
let uniqueName = path.replace('://', '_').replace(/\.|\/|\?|=/g, '_');
|
|
191
277
|
// Replacing last UNDERSCORE with a DOT
|
|
192
278
|
uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf('_'))
|
|
279
|
+
+ (query ? '_' + query.replace(/\.|\/|\?|=/g, '_') : '')
|
|
193
280
|
+ '.' + uniqueName.substring(uniqueName.lastIndexOf('_') + 1, uniqueName.length);
|
|
194
281
|
// Returning the unique name
|
|
195
282
|
return uniqueName;
|
|
@@ -200,7 +287,7 @@ function isVercel(): boolean {
|
|
|
200
287
|
}
|
|
201
288
|
|
|
202
289
|
// Defining the 'urlToCachedName' function
|
|
203
|
-
function urlToCachedName(url: string, width: number, height: number | undefined, quality: number): string {
|
|
290
|
+
function urlToCachedName(url: string, width: number, height: number | undefined, focalX: number | undefined, focalY: number | undefined, quality: number): string {
|
|
204
291
|
// Converting the URL to a file name
|
|
205
292
|
const fileName = urlToFileName(url);
|
|
206
293
|
// Parsing the file name
|
|
@@ -215,7 +302,7 @@ function urlToCachedName(url: string, width: number, height: number | undefined,
|
|
|
215
302
|
fs.mkdirSync(cacheDir);
|
|
216
303
|
}
|
|
217
304
|
// Generating the cached name
|
|
218
|
-
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_q_${quality}${parsedName.ext}`);
|
|
305
|
+
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_x_${focalX || 50}_y_${focalY || 50}_q_${quality}${parsedName.ext}`);
|
|
219
306
|
// Returning the cached name
|
|
220
307
|
return cachedName;
|
|
221
308
|
}
|