@websolutespa/bom-mixer-image 1.8.0 → 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,11 @@
1
1
  # @websolutespa/bom-mixer-image
2
2
 
3
+ ## 1.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Modified: imageHandler
8
+
3
9
  ## 1.8.0
4
10
 
5
11
  ### 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.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 || 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.1",
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,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.attention,
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, {
@@ -204,7 +287,7 @@ function isVercel(): boolean {
204
287
  }
205
288
 
206
289
  // Defining the 'urlToCachedName' function
207
- 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 {
208
291
  // Converting the URL to a file name
209
292
  const fileName = urlToFileName(url);
210
293
  // Parsing the file name
@@ -219,7 +302,7 @@ function urlToCachedName(url: string, width: number, height: number | undefined,
219
302
  fs.mkdirSync(cacheDir);
220
303
  }
221
304
  // Generating the cached name
222
- 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}`);
223
306
  // Returning the cached name
224
307
  return cachedName;
225
308
  }