@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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @websolutespa/bom-mixer-image
2
2
 
3
+ ## 1.8.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Modified: imageHandler
8
+
9
+ ## 1.8.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Modified: imageHandler
14
+
3
15
  ## 1.8.0
4
16
 
5
17
  ### 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 !== "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 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.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
- 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",
@@ -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
- 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 !== "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 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.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
- 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",
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@websolutespa/bom-mixer-image",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "Mixer Image module of the BOM Repository",
5
5
  "keywords": [
6
6
  "bom",
@@ -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,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 originalImage = await getOriginalImage(url, domains, request.headers);
99
+ const originalImageBuffer = await getOriginalImage(url, domains, request.headers);
67
100
  // Checking if the original image exists
68
- if (originalImage) {
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 = sharp(originalImage)
71
- .resize({
72
- width,
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
- resizedImage.toFile(cachedName)
113
+ compressedImage.toFile(cachedName)
84
114
  .then(() => console.log('imageHandler.sharp.cached', cachedName))
85
- .catch((error: any) => console.error('error caching image', cachedName, 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 resizedImage.toBuffer();
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 url');
117
- return reject({ status: 401, message: 'invalid url' });
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
  }