@websolutespa/bom-mixer-image 1.8.2 → 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 +8 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +89 -30
- package/dist/index.mjs +89 -30
- package/package.json +1 -1
- package/src/image/image.handler.ts +146 -50
package/CHANGELOG.md
CHANGED
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");
|
|
@@ -111,15 +106,15 @@ function imageHandler({
|
|
|
111
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");
|
|
@@ -75,15 +70,15 @@ function imageHandler({
|
|
|
75
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);
|
|
@@ -92,21 +84,21 @@ export function imageHandler({
|
|
|
92
84
|
return response.status(401).send('you are not authorized to perform this action');
|
|
93
85
|
}
|
|
94
86
|
// Temporarily fix Vercel local image issue
|
|
95
|
-
if (url.indexOf('http') === -1 && isVercel()) {
|
|
96
|
-
response.redirect(307, url);
|
|
87
|
+
if (imageOptions.url.indexOf('http') === -1 && isVercel()) {
|
|
88
|
+
response.redirect(307, imageOptions.url);
|
|
97
89
|
}
|
|
98
90
|
// Reading the original image
|
|
99
|
-
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
91
|
+
const originalImageBuffer = await getOriginalImage(imageOptions.url, domains, request.headers);
|
|
100
92
|
// Checking if the original image exists
|
|
101
93
|
if (originalImageBuffer) {
|
|
102
94
|
// Original image
|
|
103
95
|
const originalImage = sharp(originalImageBuffer);
|
|
104
96
|
// Resizing the original image and converting it to WebP format
|
|
105
|
-
const resizedImage = await resizeImage(originalImage,
|
|
97
|
+
const resizedImage = await resizeImage(originalImage, imageOptions);
|
|
106
98
|
// Converting resized image to WebP format
|
|
107
99
|
const compressedImage = resizedImage
|
|
108
100
|
.webp({
|
|
109
|
-
quality:
|
|
101
|
+
quality: imageOptions.q,
|
|
110
102
|
// nearLossless: true,
|
|
111
103
|
});
|
|
112
104
|
// Saving the resized image to the cache
|
|
@@ -124,16 +116,118 @@ export function imageHandler({
|
|
|
124
116
|
// Sending data in the response
|
|
125
117
|
return response.end(data);
|
|
126
118
|
}
|
|
127
|
-
} catch (error) {
|
|
119
|
+
} catch (error: any) {
|
|
128
120
|
// Logging an error message and throwing the error
|
|
129
121
|
console.error('imageHandler.error', error);
|
|
130
|
-
|
|
122
|
+
return response.status(error.status || 500).json({
|
|
123
|
+
errors: [{
|
|
124
|
+
message: error.message || error.name || error.statusText || 'An error occurred',
|
|
125
|
+
}],
|
|
126
|
+
});
|
|
131
127
|
}
|
|
132
128
|
},
|
|
133
129
|
});
|
|
134
130
|
}
|
|
135
131
|
|
|
136
|
-
|
|
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;
|
|
137
231
|
const defaultResizeOptions = () => {
|
|
138
232
|
return originalImage.resize({
|
|
139
233
|
width: resizeWidth,
|
|
@@ -143,7 +237,7 @@ async function resizeImage(originalImage: sharp.Sharp, resizeWidth: number, resi
|
|
|
143
237
|
withoutEnlargement: true,
|
|
144
238
|
});
|
|
145
239
|
};
|
|
146
|
-
if (resizeHeight && (focalX !==
|
|
240
|
+
if (resizeHeight && (focalX !== IMAGE_FOCAL_X || focalY !== IMAGE_FOCAL_Y)) {
|
|
147
241
|
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
148
242
|
if (originalWidth && originalHeight) {
|
|
149
243
|
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
@@ -291,9 +385,9 @@ function isVercel(): boolean {
|
|
|
291
385
|
}
|
|
292
386
|
|
|
293
387
|
// Defining the 'urlToCachedName' function
|
|
294
|
-
function urlToCachedName(
|
|
388
|
+
function urlToCachedName(options: IImageOptions): string {
|
|
295
389
|
// Converting the URL to a file name
|
|
296
|
-
const fileName = urlToFileName(url);
|
|
390
|
+
const fileName = urlToFileName(options.url);
|
|
297
391
|
// Parsing the file name
|
|
298
392
|
const parsedName = path.parse(fileName);
|
|
299
393
|
// Defining the cache directory
|
|
@@ -306,7 +400,9 @@ function urlToCachedName(url: string, width: number, height: number | undefined,
|
|
|
306
400
|
fs.mkdirSync(cacheDir);
|
|
307
401
|
}
|
|
308
402
|
// Generating the cached name
|
|
309
|
-
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}`);
|
|
310
406
|
// Returning the cached name
|
|
311
407
|
return cachedName;
|
|
312
408
|
}
|