@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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # @websolutespa/bom-mixer-image
2
2
 
3
+ ## 1.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Modified: imageHandler
8
+
9
+ ## 1.8.0
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [f8a1d97]
14
+ - @websolutespa/bom-core@1.8.0
15
+
3
16
  ## 1.0.9
4
17
 
5
18
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- declare function imageHandler({ domains }: {
1
+ declare function imageHandler({ domains, sizes, quality, }: {
2
2
  domains: string[];
3
+ sizes: number[];
4
+ quality: number;
3
5
  }): any;
4
6
 
5
7
  export { imageHandler };
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
- function imageHandler({ domains = [] }) {
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 { url, w, h, q } = request.query;
77
- if (typeof url !== "string") {
78
- return response.status(400).send("missing url parameter");
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 > 2048) {
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 quality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : 80));
89
- const cachedName = urlToCachedName(url, width, height, quality);
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 originalImage = await getOriginalImage(url, domains, request.headers);
101
- if (originalImage) {
102
- const resizedImage = (0, import_sharp.default)(originalImage).resize({
103
- width,
104
- height: height ? height : void 0,
105
- fit: import_sharp.default.fit.cover,
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
- resizedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("error caching image", cachedName, error));
113
- const data = await resizedImage.toBuffer();
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
- console.error("imageHandler.getOriginalImage.error invalid url");
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
- let uniqueName = url.replace("://", "_").replace(".", "_").replace("/", "_");
174
- uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf("_")) + "." + uniqueName.substring(uniqueName.lastIndexOf("_") + 1, uniqueName.length);
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
- function imageHandler({ domains = [] }) {
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 { url, w, h, q } = request.query;
41
- if (typeof url !== "string") {
42
- return response.status(400).send("missing url parameter");
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 > 2048) {
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 quality = Math.max(30, Math.min(100, typeof q === "string" ? parseInt(q) : 80));
53
- const cachedName = urlToCachedName(url, width, height, quality);
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 originalImage = await getOriginalImage(url, domains, request.headers);
65
- if (originalImage) {
66
- const resizedImage = sharp(originalImage).resize({
67
- width,
68
- height: height ? height : void 0,
69
- fit: sharp.fit.cover,
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
- resizedImage.toFile(cachedName).then(() => console.log("imageHandler.sharp.cached", cachedName)).catch((error) => console.error("error caching image", cachedName, error));
77
- const data = await resizedImage.toBuffer();
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
- console.error("imageHandler.getOriginalImage.error invalid url");
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
- let uniqueName = url.replace("://", "_").replace(".", "_").replace("/", "_");
138
- uniqueName = uniqueName.substring(0, uniqueName.lastIndexOf("_")) + "." + uniqueName.substring(uniqueName.lastIndexOf("_") + 1, uniqueName.length);
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.0.9",
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({ domains = [] }: { domains: string[] }) {
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 { url, w, h, q } = request.query;
24
- // Checking if 'url' is not a string
25
- if (typeof url !== 'string') {
26
- // Logging an error message and returning a 400 status code with a message
27
- // console.error('imageHandler', 'missing url parameter');
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 'q' to an integer, ensuring it's between 30 and 100, and assigning it to 'quality'
44
- const quality = Math.max(30, Math.min(100, (typeof q === 'string' ? parseInt(q) : 80)));
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, quality);
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 originalImage = await getOriginalImage(url, domains, request.headers);
95
+ const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
67
96
  // Checking if the original image exists
68
- if (originalImage) {
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 = sharp(originalImage)
71
- .resize({
72
- width,
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
- resizedImage.toFile(cachedName)
109
+ compressedImage.toFile(cachedName)
84
110
  .then(() => console.log('imageHandler.sharp.cached', cachedName))
85
- .catch((error: any) => console.error('error caching image', cachedName, 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 resizedImage.toBuffer();
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 url');
117
- return reject({ status: 401, message: 'invalid url' });
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
- let uniqueName = url.replace('://', '_').replace('.', '_').replace('/', '_');
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
  }