@websolutespa/bom-mixer-image 1.8.0 → 1.8.2
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 +12 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +85 -23
- package/dist/index.mjs +85 -23
- package/package.json +1 -1
- package/src/image/image.handler.ts +117 -30
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 !== "https://" && 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.attention,
|
|
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",
|
|
@@ -180,14 +242,14 @@ function urlToFileName(url) {
|
|
|
180
242
|
function isVercel() {
|
|
181
243
|
return process && process.env && String(process.env.VERCEL) === "1";
|
|
182
244
|
}
|
|
183
|
-
function urlToCachedName(url, width, height, quality) {
|
|
245
|
+
function urlToCachedName(url, width, height, focalX, focalY, quality) {
|
|
184
246
|
const fileName = urlToFileName(url);
|
|
185
247
|
const parsedName = path.parse(fileName);
|
|
186
248
|
const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
|
|
187
249
|
if (!fs.existsSync(cacheDir)) {
|
|
188
250
|
fs.mkdirSync(cacheDir);
|
|
189
251
|
}
|
|
190
|
-
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}`);
|
|
191
253
|
return cachedName;
|
|
192
254
|
}
|
|
193
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 !== "https://" && 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.attention,
|
|
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",
|
|
@@ -144,14 +206,14 @@ function urlToFileName(url) {
|
|
|
144
206
|
function isVercel() {
|
|
145
207
|
return process && process.env && String(process.env.VERCEL) === "1";
|
|
146
208
|
}
|
|
147
|
-
function urlToCachedName(url, width, height, quality) {
|
|
209
|
+
function urlToCachedName(url, width, height, focalX, focalY, quality) {
|
|
148
210
|
const fileName = urlToFileName(url);
|
|
149
211
|
const parsedName = path.parse(fileName);
|
|
150
212
|
const cacheDir = isVercel() ? path.join("/tmp", ".cache") : path.join(process.cwd(), ".cache");
|
|
151
213
|
if (!fs.existsSync(cacheDir)) {
|
|
152
214
|
fs.mkdirSync(cacheDir);
|
|
153
215
|
}
|
|
154
|
-
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}`);
|
|
155
217
|
return cachedName;
|
|
156
218
|
}
|
|
157
219
|
function checkUrlValidity(src, domains) {
|
package/package.json
CHANGED
|
@@ -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,42 @@ 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 (
|
|
88
|
+
referer &&
|
|
89
|
+
publicUrl !== 'https://' &&
|
|
90
|
+
referer.indexOf(publicUrl) !== 0
|
|
91
|
+
) {
|
|
92
|
+
return response.status(401).send('you are not authorized to perform this action');
|
|
93
|
+
}
|
|
61
94
|
// Temporarily fix Vercel local image issue
|
|
62
95
|
if (url.indexOf('http') === -1 && isVercel()) {
|
|
63
96
|
response.redirect(307, url);
|
|
64
97
|
}
|
|
65
98
|
// Reading the original image
|
|
66
|
-
const
|
|
99
|
+
const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
|
|
67
100
|
// Checking if the original image exists
|
|
68
|
-
if (
|
|
101
|
+
if (originalImageBuffer) {
|
|
102
|
+
// Original image
|
|
103
|
+
const originalImage = sharp(originalImageBuffer);
|
|
69
104
|
// 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.attention,
|
|
76
|
-
withoutEnlargement: true,
|
|
77
|
-
})
|
|
105
|
+
const resizedImage = await resizeImage(originalImage, width, height, focalX, focalY);
|
|
106
|
+
// Converting resized image to WebP format
|
|
107
|
+
const compressedImage = resizedImage
|
|
78
108
|
.webp({
|
|
79
|
-
quality,
|
|
109
|
+
quality: imageQuality,
|
|
80
110
|
// nearLossless: true,
|
|
81
111
|
});
|
|
82
112
|
// Saving the resized image to the cache
|
|
83
|
-
|
|
113
|
+
compressedImage.toFile(cachedName)
|
|
84
114
|
.then(() => console.log('imageHandler.sharp.cached', cachedName))
|
|
85
|
-
.catch((error: any) => console.error('error
|
|
115
|
+
.catch((error: any) => console.error('imageHandler.sharp.error', cachedName, error));
|
|
86
116
|
// Converting the resized image to a buffer
|
|
87
|
-
const data = await
|
|
117
|
+
const data = await compressedImage.toBuffer();
|
|
88
118
|
// Setting the 'Content-Type' header to 'image/webp'
|
|
89
119
|
response.setHeader('Content-Type', 'image/webp');
|
|
90
120
|
// Setting the 'Expires' header to the current time plus the maximum age of the image (in seconds)
|
|
@@ -103,6 +133,63 @@ export function imageHandler({ domains = [] }: { domains: string[] }) {
|
|
|
103
133
|
});
|
|
104
134
|
}
|
|
105
135
|
|
|
136
|
+
async function resizeImage(originalImage: sharp.Sharp, resizeWidth: number, resizeHeight: number | undefined, focalX: number, focalY: number): Promise<sharp.Sharp> {
|
|
137
|
+
const defaultResizeOptions = () => {
|
|
138
|
+
return originalImage.resize({
|
|
139
|
+
width: resizeWidth,
|
|
140
|
+
height: resizeHeight ? resizeHeight : undefined,
|
|
141
|
+
fit: sharp.fit.cover,
|
|
142
|
+
position: sharp.strategy.attention,
|
|
143
|
+
withoutEnlargement: true,
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
if (resizeHeight && (focalX !== 50 || focalY !== 50)) {
|
|
147
|
+
const { width: originalWidth, height: originalHeight } = await originalImage.metadata();
|
|
148
|
+
if (originalWidth && originalHeight) {
|
|
149
|
+
const resizeAspectRatio = resizeWidth / resizeHeight;
|
|
150
|
+
const originalAspectRatio = originalWidth / originalHeight;
|
|
151
|
+
const prioritizeHeight = resizeAspectRatio < originalAspectRatio;
|
|
152
|
+
const imageToResize = originalImage.clone();
|
|
153
|
+
// Scale the image up or down to fit the resize dimensions
|
|
154
|
+
const scaledImage = imageToResize.resize(prioritizeHeight ? {
|
|
155
|
+
height: resizeHeight,
|
|
156
|
+
fit: sharp.fit.cover,
|
|
157
|
+
} : {
|
|
158
|
+
width: resizeWidth,
|
|
159
|
+
fit: sharp.fit.cover,
|
|
160
|
+
});
|
|
161
|
+
const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true });
|
|
162
|
+
// extractWidth
|
|
163
|
+
const extractWidth = resizeWidth ?? scaledImageInfo.width;
|
|
164
|
+
const maxOffsetX = scaledImageInfo.width - extractWidth;
|
|
165
|
+
const leftFocalEdge = Math.round(
|
|
166
|
+
scaledImageInfo.width * (focalX / 100) - extractWidth / 2
|
|
167
|
+
);
|
|
168
|
+
// extractX
|
|
169
|
+
const extractX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX);
|
|
170
|
+
// extractHeight
|
|
171
|
+
const extractHeight = resizeHeight ?? scaledImageInfo.height;
|
|
172
|
+
const maxOffsetY = scaledImageInfo.height - extractHeight;
|
|
173
|
+
const topFocalEdge = Math.round(
|
|
174
|
+
scaledImageInfo.height * (focalY / 100) - extractHeight / 2
|
|
175
|
+
);
|
|
176
|
+
// extractY
|
|
177
|
+
const extractY = Math.min(Math.max(0, topFocalEdge), maxOffsetY);
|
|
178
|
+
// Extract the focal area from the scaled image
|
|
179
|
+
return scaledImage.extract({
|
|
180
|
+
left: extractX,
|
|
181
|
+
top: extractY,
|
|
182
|
+
width: extractWidth,
|
|
183
|
+
height: extractHeight,
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
return defaultResizeOptions();
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
return defaultResizeOptions();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
106
193
|
// Defining the 'getOriginalImage' function
|
|
107
194
|
function getOriginalImage(url: string, domains: string[], headers: IncomingHttpHeaders, skipUrlValidity: boolean = false): Promise<Buffer> {
|
|
108
195
|
// Returning a new promise
|
|
@@ -113,8 +200,8 @@ function getOriginalImage(url: string, domains: string[], headers: IncomingHttpH
|
|
|
113
200
|
!skipUrlValidity &&
|
|
114
201
|
!checkUrlValidity(url, domains)
|
|
115
202
|
) {
|
|
116
|
-
console.error('imageHandler.getOriginalImage.error invalid
|
|
117
|
-
return reject({ status: 401, message: 'invalid
|
|
203
|
+
// console.error('imageHandler.getOriginalImage.error invalid origin');
|
|
204
|
+
return reject({ status: 401, message: 'invalid origin' });
|
|
118
205
|
}
|
|
119
206
|
// Fetching the image from the URL
|
|
120
207
|
fetch(url, {
|
|
@@ -204,7 +291,7 @@ function isVercel(): boolean {
|
|
|
204
291
|
}
|
|
205
292
|
|
|
206
293
|
// Defining the 'urlToCachedName' function
|
|
207
|
-
function urlToCachedName(url: string, width: number, height: number | undefined, quality: number): string {
|
|
294
|
+
function urlToCachedName(url: string, width: number, height: number | undefined, focalX: number | undefined, focalY: number | undefined, quality: number): string {
|
|
208
295
|
// Converting the URL to a file name
|
|
209
296
|
const fileName = urlToFileName(url);
|
|
210
297
|
// Parsing the file name
|
|
@@ -219,7 +306,7 @@ function urlToCachedName(url: string, width: number, height: number | undefined,
|
|
|
219
306
|
fs.mkdirSync(cacheDir);
|
|
220
307
|
}
|
|
221
308
|
// Generating the cached name
|
|
222
|
-
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_q_${quality}${parsedName.ext}`);
|
|
309
|
+
const cachedName = path.join(cacheDir, `${parsedName.name}_w_${width}_h_${height || 0}_x_${focalX || 50}_y_${focalY || 50}_q_${quality}${parsedName.ext}`);
|
|
223
310
|
// Returning the cached name
|
|
224
311
|
return cachedName;
|
|
225
312
|
}
|