Skip to content

Commit c3b0f4e

Browse files
authored
Update index.mjs
1 parent 959628c commit c3b0f4e

File tree

1 file changed

+80
-56
lines changed

1 file changed

+80
-56
lines changed

lambda/image_optimization/index.mjs

Lines changed: 80 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
21
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
32
// SPDX-License-Identifier: MIT-0
43

54
import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
65
import Sharp from 'sharp';
76

8-
const s3Client = new S3Client();
7+
const s3Client = new S3Client({ region: process.env.s3BucketRegion });
98
const S3_ORIGINAL_IMAGE_BUCKET = process.env.originalImageBucketName;
109
const S3_TRANSFORMED_IMAGE_BUCKET = process.env.transformedImageBucketName;
1110
const TRANSFORMED_IMAGE_CACHE_TTL = process.env.transformedImageCacheTTL;
@@ -14,94 +13,118 @@ const MAX_IMAGE_SIZE = parseInt(process.env.maxImageSize);
1413
export const handler = async (event) => {
1514
// Validate if this is a GET request
1615
if (!event.requestContext || !event.requestContext.http || !(event.requestContext.http.method === 'GET')) return sendError(400, 'Only GET method is supported', event);
17-
// An example of expected path is /images/rio/1.jpeg/format=auto,width=100 or /images/rio/1.jpeg/original where /images/rio/1.jpeg is the path of the original image
18-
var imagePathArray = event.requestContext.http.path.split('/');
19-
// get the requested image operations
20-
var operationsPrefix = imagePathArray.pop();
21-
// get the original image path images/rio/1.jpg
22-
imagePathArray.shift();
23-
var originalImagePath = imagePathArray.join('/');
16+
17+
// Extracting the path and query parameters
18+
const path = event.requestContext.http.path;
19+
const queryStringParameters = event.queryStringParameters || {};
20+
21+
// The image path from the URL
22+
const imagePath = path.startsWith('/') ? path.substring(1) : path;
23+
24+
// Check if the requested file should be processed or ignored
25+
const fileExtension = path.split('.').pop().toLowerCase();
26+
const supportedExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'avif', 'svg'];
27+
28+
if (!supportedExtensions.includes(fileExtension)) {
29+
// If the file is not an image, return a 302 redirect to the original URL
30+
return {
31+
statusCode: 302,
32+
headers: {
33+
'Location': path,
34+
'Cache-Control': 'no-cache'
35+
}
36+
};
37+
}
38+
39+
// Extracting operations from query parameters
40+
const width = queryStringParameters.width ? parseInt(queryStringParameters.width) : null;
41+
const height = queryStringParameters.height ? parseInt(queryStringParameters.height) : null;
42+
const format = queryStringParameters['image-type'] || null;
43+
const quality = queryStringParameters.quality ? parseInt(queryStringParameters.quality) : null;
2444

2545
var startTime = performance.now();
26-
// Downloading original image
2746
let originalImageBody;
2847
let contentType;
48+
2949
try {
30-
const getOriginalImageCommand = new GetObjectCommand({ Bucket: S3_ORIGINAL_IMAGE_BUCKET, Key: originalImagePath });
50+
const getOriginalImageCommand = new GetObjectCommand({ Bucket: S3_ORIGINAL_IMAGE_BUCKET, Key: imagePath });
3151
const getOriginalImageCommandOutput = await s3Client.send(getOriginalImageCommand);
32-
console.log(`Got response from S3 for ${originalImagePath}`);
52+
console.log(`Got response from S3 for ${imagePath}`);
3353

34-
originalImageBody = getOriginalImageCommandOutput.Body.transformToByteArray();
54+
originalImageBody = await getOriginalImageCommandOutput.Body.transformToByteArray();
3555
contentType = getOriginalImageCommandOutput.ContentType;
3656
} catch (error) {
3757
return sendError(500, 'Error downloading original image', error);
3858
}
39-
let transformedImage = Sharp(await originalImageBody, { failOn: 'none', animated: true });
40-
// Get image orientation to rotate if needed
59+
60+
let transformedImage = Sharp(originalImageBody, { failOn: 'none', animated: true });
4161
const imageMetadata = await transformedImage.metadata();
42-
// execute the requested operations
43-
const operationsJSON = Object.fromEntries(operationsPrefix.split(',').map(operation => operation.split('=')));
44-
// variable holding the server timing header value
62+
4563
var timingLog = 'img-download;dur=' + parseInt(performance.now() - startTime);
4664
startTime = performance.now();
65+
4766
try {
48-
// check if resizing is requested
49-
var resizingOptions = {};
50-
if (operationsJSON['width']) resizingOptions.width = parseInt(operationsJSON['width']);
51-
if (operationsJSON['height']) resizingOptions.height = parseInt(operationsJSON['height']);
52-
if (resizingOptions) transformedImage = transformedImage.resize(resizingOptions);
53-
// check if rotation is needed
67+
if (width || height) {
68+
transformedImage = transformedImage.resize({
69+
width: width,
70+
height: height,
71+
fit: Sharp.fit.inside,
72+
withoutEnlargement: true
73+
});
74+
}
5475
if (imageMetadata.orientation) transformedImage = transformedImage.rotate();
55-
// check if formatting is requested
56-
if (operationsJSON['format']) {
76+
77+
if (format) {
5778
var isLossy = false;
58-
switch (operationsJSON['format']) {
79+
switch (format) {
5980
case 'jpeg': contentType = 'image/jpeg'; isLossy = true; break;
6081
case 'gif': contentType = 'image/gif'; break;
6182
case 'webp': contentType = 'image/webp'; isLossy = true; break;
6283
case 'png': contentType = 'image/png'; break;
6384
case 'avif': contentType = 'image/avif'; isLossy = true; break;
6485
default: contentType = 'image/jpeg'; isLossy = true;
6586
}
66-
if (operationsJSON['quality'] && isLossy) {
67-
transformedImage = transformedImage.toFormat(operationsJSON['format'], {
68-
quality: parseInt(operationsJSON['quality']),
69-
});
70-
} else transformedImage = transformedImage.toFormat(operationsJSON['format']);
87+
if (quality && isLossy) {
88+
transformedImage = transformedImage.toFormat(format, { quality: quality });
89+
} else {
90+
transformedImage = transformedImage.toFormat(format);
91+
}
7192
} else {
72-
/// If not format is precised, Sharp converts svg to png by default https://github.com/aws-samples/image-optimization/issues/48
7393
if (contentType === 'image/svg+xml') contentType = 'image/png';
7494
}
95+
7596
transformedImage = await transformedImage.toBuffer();
7697
} catch (error) {
77-
return sendError(500, 'error transforming image', error);
98+
return sendError(500, 'Error transforming image', error);
7899
}
100+
79101
timingLog = timingLog + ',img-transform;dur=' + parseInt(performance.now() - startTime);
80-
81-
// handle gracefully generated images bigger than a specified limit (e.g. Lambda output object limit)
82102
const imageTooBig = Buffer.byteLength(transformedImage) > MAX_IMAGE_SIZE;
83-
84-
// upload transformed image back to S3 if required in the architecture
103+
85104
if (S3_TRANSFORMED_IMAGE_BUCKET) {
86105
startTime = performance.now();
87106
try {
107+
const metadata = {
108+
'cache-control': TRANSFORMED_IMAGE_CACHE_TTL,
109+
'width': width ? width.toString() : 'empty',
110+
'height': height ? height.toString() : 'empty',
111+
'quality': quality ? quality.toString() : 'empty'
112+
};
88113
const putImageCommand = new PutObjectCommand({
89114
Body: transformedImage,
90115
Bucket: S3_TRANSFORMED_IMAGE_BUCKET,
91-
Key: originalImagePath + '/' + operationsPrefix,
116+
Key: imagePath,
92117
ContentType: contentType,
93-
Metadata: {
94-
'cache-control': TRANSFORMED_IMAGE_CACHE_TTL,
95-
},
96-
})
118+
Metadata: metadata
119+
});
97120
await s3Client.send(putImageCommand);
98121
timingLog = timingLog + ',img-upload;dur=' + parseInt(performance.now() - startTime);
99-
// If the generated image file is too big, send a redirection to the generated image on S3, instead of serving it synchronously from Lambda.
122+
100123
if (imageTooBig) {
101124
return {
102125
statusCode: 302,
103126
headers: {
104-
'Location': '/' + originalImagePath + '?' + operationsPrefix.replace(/,/g, "&"),
127+
'Location': '/' + imagePath + '?' + new URLSearchParams(queryStringParameters).toString(),
105128
'Cache-Control': 'private,no-store',
106129
'Server-Timing': timingLog
107130
}
@@ -112,19 +135,20 @@ export const handler = async (event) => {
112135
}
113136
}
114137

115-
// Return error if the image is too big and a redirection to the generated image was not possible, else return transformed image
116138
if (imageTooBig) {
117139
return sendError(403, 'Requested transformed image is too big', '');
118-
} else return {
119-
statusCode: 200,
120-
body: transformedImage.toString('base64'),
121-
isBase64Encoded: true,
122-
headers: {
123-
'Content-Type': contentType,
124-
'Cache-Control': TRANSFORMED_IMAGE_CACHE_TTL,
125-
'Server-Timing': timingLog
126-
}
127-
};
140+
} else {
141+
return {
142+
statusCode: 200,
143+
body: transformedImage.toString('base64'),
144+
isBase64Encoded: true,
145+
headers: {
146+
'Content-Type': contentType,
147+
'Cache-Control': TRANSFORMED_IMAGE_CACHE_TTL,
148+
'Server-Timing': timingLog
149+
}
150+
};
151+
}
128152
};
129153

130154
function sendError(statusCode, body, error) {

0 commit comments

Comments
 (0)