1
-
2
1
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
2
// SPDX-License-Identifier: MIT-0
4
3
5
4
import { GetObjectCommand , PutObjectCommand , S3Client } from "@aws-sdk/client-s3" ;
6
5
import Sharp from 'sharp' ;
7
6
8
- const s3Client = new S3Client ( ) ;
7
+ const s3Client = new S3Client ( { region : process . env . s3BucketRegion } ) ;
9
8
const S3_ORIGINAL_IMAGE_BUCKET = process . env . originalImageBucketName ;
10
9
const S3_TRANSFORMED_IMAGE_BUCKET = process . env . transformedImageBucketName ;
11
10
const TRANSFORMED_IMAGE_CACHE_TTL = process . env . transformedImageCacheTTL ;
@@ -14,94 +13,118 @@ const MAX_IMAGE_SIZE = parseInt(process.env.maxImageSize);
14
13
export const handler = async ( event ) => {
15
14
// Validate if this is a GET request
16
15
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 ;
24
44
25
45
var startTime = performance . now ( ) ;
26
- // Downloading original image
27
46
let originalImageBody ;
28
47
let contentType ;
48
+
29
49
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 } ) ;
31
51
const getOriginalImageCommandOutput = await s3Client . send ( getOriginalImageCommand ) ;
32
- console . log ( `Got response from S3 for ${ originalImagePath } ` ) ;
52
+ console . log ( `Got response from S3 for ${ imagePath } ` ) ;
33
53
34
- originalImageBody = getOriginalImageCommandOutput . Body . transformToByteArray ( ) ;
54
+ originalImageBody = await getOriginalImageCommandOutput . Body . transformToByteArray ( ) ;
35
55
contentType = getOriginalImageCommandOutput . ContentType ;
36
56
} catch ( error ) {
37
57
return sendError ( 500 , 'Error downloading original image' , error ) ;
38
58
}
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 } ) ;
41
61
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
+
45
63
var timingLog = 'img-download;dur=' + parseInt ( performance . now ( ) - startTime ) ;
46
64
startTime = performance . now ( ) ;
65
+
47
66
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
+ }
54
75
if ( imageMetadata . orientation ) transformedImage = transformedImage . rotate ( ) ;
55
- // check if formatting is requested
56
- if ( operationsJSON [ ' format' ] ) {
76
+
77
+ if ( format ) {
57
78
var isLossy = false ;
58
- switch ( operationsJSON [ ' format' ] ) {
79
+ switch ( format ) {
59
80
case 'jpeg' : contentType = 'image/jpeg' ; isLossy = true ; break ;
60
81
case 'gif' : contentType = 'image/gif' ; break ;
61
82
case 'webp' : contentType = 'image/webp' ; isLossy = true ; break ;
62
83
case 'png' : contentType = 'image/png' ; break ;
63
84
case 'avif' : contentType = 'image/avif' ; isLossy = true ; break ;
64
85
default : contentType = 'image/jpeg' ; isLossy = true ;
65
86
}
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
+ }
71
92
} else {
72
- /// If not format is precised, Sharp converts svg to png by default https://github.com/aws-samples/image-optimization/issues/48
73
93
if ( contentType === 'image/svg+xml' ) contentType = 'image/png' ;
74
94
}
95
+
75
96
transformedImage = await transformedImage . toBuffer ( ) ;
76
97
} catch ( error ) {
77
- return sendError ( 500 , 'error transforming image' , error ) ;
98
+ return sendError ( 500 , 'Error transforming image' , error ) ;
78
99
}
100
+
79
101
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)
82
102
const imageTooBig = Buffer . byteLength ( transformedImage ) > MAX_IMAGE_SIZE ;
83
-
84
- // upload transformed image back to S3 if required in the architecture
103
+
85
104
if ( S3_TRANSFORMED_IMAGE_BUCKET ) {
86
105
startTime = performance . now ( ) ;
87
106
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
+ } ;
88
113
const putImageCommand = new PutObjectCommand ( {
89
114
Body : transformedImage ,
90
115
Bucket : S3_TRANSFORMED_IMAGE_BUCKET ,
91
- Key : originalImagePath + '/' + operationsPrefix ,
116
+ Key : imagePath ,
92
117
ContentType : contentType ,
93
- Metadata : {
94
- 'cache-control' : TRANSFORMED_IMAGE_CACHE_TTL ,
95
- } ,
96
- } )
118
+ Metadata : metadata
119
+ } ) ;
97
120
await s3Client . send ( putImageCommand ) ;
98
121
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
+
100
123
if ( imageTooBig ) {
101
124
return {
102
125
statusCode : 302 ,
103
126
headers : {
104
- 'Location' : '/' + originalImagePath + '?' + operationsPrefix . replace ( / , / g , "&" ) ,
127
+ 'Location' : '/' + imagePath + '?' + new URLSearchParams ( queryStringParameters ) . toString ( ) ,
105
128
'Cache-Control' : 'private,no-store' ,
106
129
'Server-Timing' : timingLog
107
130
}
@@ -112,19 +135,20 @@ export const handler = async (event) => {
112
135
}
113
136
}
114
137
115
- // Return error if the image is too big and a redirection to the generated image was not possible, else return transformed image
116
138
if ( imageTooBig ) {
117
139
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
+ }
128
152
} ;
129
153
130
154
function sendError ( statusCode , body , error ) {
0 commit comments