@@ -10,67 +10,54 @@ exports.handler = async (event, context, callback) => {
1010 const request = event . Records [ 0 ] . cf . request ;
1111 const response = event . Records [ 0 ] . cf . response ;
1212
13- if (
14- '200' !== response . status ||
15- ! request . origin ||
16- ! request . origin . s3 ||
17- ! request . origin . s3 . domainName
18- ) {
13+ if ( shouldSkipProcessing ( request , response ) ) {
1914 return callback ( null , response ) ;
2015 }
2116
22- const match = request . origin . s3 . domainName . match ( / ( [ ^ . ] * ) \. s 3 ( \. [ ^ . ] * ) ? \. a m a z o n a w s \. c o m / i ) ;
17+ const bucketDetails = extractBucketDetails ( request ) ;
2318
24- if ( ! match || ! match [ 1 ] || 'string' !== typeof match [ 1 ] ) {
19+ if ( ! bucketDetails ) {
2520 return callback ( null , response ) ;
2621 }
2722
2823 const allowedContentTypes = [ 'image/gif' , 'image/jpeg' , 'image/png' ] ;
29- const bucket = match [ 1 ] ;
24+ const bucket = bucketDetails ;
3025 const key = decodeURIComponent ( request . uri . substring ( 1 ) ) ;
26+ const params = new URLSearchParams ( request . querystring ) ;
27+ const formatSetting = params . get ( 'format' ) ;
3128
32- const getObjectCommand = new GetObjectCommand ( { Bucket : bucket , Key : key } ) ;
33- const objectResponse = await s3Client . send ( getObjectCommand ) ;
29+ const objectResponse = await fetchOriginalImage ( bucket , key ) ;
3430
35- if (
36- ! objectResponse . ContentType ||
37- ! allowedContentTypes . includes ( objectResponse . ContentType ) ||
38- ( 'image/gif' === objectResponse . ContentType &&
39- animated ( await streamToBuffer ( objectResponse . Body ) ) )
40- ) {
31+ if ( shouldSkipImageProcessing ( objectResponse , allowedContentTypes ) ) {
32+ return callback ( null , response ) ;
33+ }
34+
35+ const isAnimatedGif =
36+ 'image/gif' === objectResponse . ContentType &&
37+ animated ( await streamToBuffer ( objectResponse . Body ) ) ;
38+
39+ if ( isAnimatedGif ) {
4140 return callback ( null , response ) ;
4241 }
4342
44- let contentType = null ;
4543 const objectBody = await streamToBuffer ( objectResponse . Body ) ;
4644 const image = sharp ( objectBody ) ;
47- const params = new URLSearchParams ( request . querystring ) ;
45+ const preserveOriginalFormat = formatSetting === 'original' ;
4846
49- if ( 'image/gif' === objectResponse . ContentType ) {
50- image . png ( ) ;
51- contentType = [ { key : 'Content-Type' , value : 'image/png' } ] ;
52- }
47+ let contentType = null ;
5348
54- if ( request . headers [ 'accept' ] && request . headers [ 'accept' ] [ 0 ] . value . match ( 'image/webp' ) ) {
55- image . webp ( {
56- quality : Math . round ( Math . min ( Math . max ( parseInt ( params . get ( 'quality' ) , 10 ) || 82 , 0 ) , 100 ) ) ,
57- } ) ;
58- contentType = [ { key : 'Content-Type' , value : 'image/webp' } ] ;
49+ if ( ! preserveOriginalFormat ) {
50+ contentType = await processImageFormat ( image , objectResponse , request , params , formatSetting ) ;
5951 }
6052
6153 if ( params . has ( 'width' ) || params . has ( 'height' ) ) {
62- image . resize ( {
63- width : parseInt ( params . get ( 'width' ) , 10 ) || null ,
64- height : parseInt ( params . get ( 'height' ) , 10 ) || null ,
65- fit : params . has ( 'cropped' ) ? sharp . fit . cover : sharp . fit . inside ,
66- withoutEnlargement : true ,
67- } ) ;
54+ applyResize ( image , params ) ;
6855 }
6956
7057 const buffer = await image . toBuffer ( ) ;
7158 const responseBody = buffer . toString ( 'base64' ) ;
7259
73- if ( 1330000 < Buffer . byteLength ( responseBody ) ) {
60+ if ( isResponseTooLarge ( responseBody ) ) {
7461 return callback ( null , response ) ;
7562 }
7663
@@ -87,7 +74,90 @@ exports.handler = async (event, context, callback) => {
8774 }
8875} ;
8976
90- // Helper function to convert a stream to a buffer
77+ function shouldSkipProcessing ( request , response ) {
78+ return (
79+ '200' !== response . status ||
80+ ! request . origin ||
81+ ! request . origin . s3 ||
82+ ! request . origin . s3 . domainName
83+ ) ;
84+ }
85+
86+ function extractBucketDetails ( request ) {
87+ const match = request . origin . s3 . domainName . match ( / ( [ ^ . ] * ) \. s 3 ( \. [ ^ . ] * ) ? \. a m a z o n a w s \. c o m / i) ;
88+
89+ if ( ! match || ! match [ 1 ] || 'string' !== typeof match [ 1 ] ) {
90+ return null ;
91+ }
92+
93+ return match [ 1 ] ;
94+ }
95+
96+ function fetchOriginalImage ( bucket , key ) {
97+ const getObjectCommand = new GetObjectCommand ( { Bucket : bucket , Key : key } ) ;
98+ return s3Client . send ( getObjectCommand ) ;
99+ }
100+
101+ function shouldSkipImageProcessing ( objectResponse , allowedContentTypes ) {
102+ return ! objectResponse . ContentType || ! allowedContentTypes . includes ( objectResponse . ContentType ) ;
103+ }
104+
105+ function processImageFormat ( image , objectResponse , request , params , formatSetting ) {
106+ let contentType = null ;
107+
108+ if ( formatSetting && formatSetting !== 'auto' ) {
109+ return applyExplicitFormat ( image , formatSetting ) ;
110+ }
111+
112+ if ( 'image/gif' === objectResponse . ContentType ) {
113+ image . png ( ) ;
114+ contentType = [ { key : 'Content-Type' , value : 'image/png' } ] ;
115+ }
116+
117+ if ( request . headers [ 'accept' ] && request . headers [ 'accept' ] [ 0 ] . value . match ( 'image/webp' ) ) {
118+ const quality = calculateQuality ( params ) ;
119+ image . webp ( { quality } ) ;
120+ contentType = [ { key : 'Content-Type' , value : 'image/webp' } ] ;
121+ }
122+
123+ return contentType ;
124+ }
125+
126+ function applyExplicitFormat ( image , formatSetting ) {
127+ switch ( formatSetting . toLowerCase ( ) ) {
128+ case 'webp' :
129+ image . webp ( ) ;
130+ return [ { key : 'Content-Type' , value : 'image/webp' } ] ;
131+ case 'png' :
132+ image . png ( ) ;
133+ return [ { key : 'Content-Type' , value : 'image/png' } ] ;
134+ case 'jpeg' :
135+ case 'jpg' :
136+ image . jpeg ( ) ;
137+ return [ { key : 'Content-Type' , value : 'image/jpeg' } ] ;
138+ default :
139+ return null ;
140+ }
141+ }
142+
143+ function calculateQuality ( params ) {
144+ const rawQuality = parseInt ( params . get ( 'quality' ) , 10 ) || 82 ;
145+ return Math . round ( Math . min ( Math . max ( rawQuality , 0 ) , 100 ) ) ;
146+ }
147+
148+ function applyResize ( image , params ) {
149+ image . resize ( {
150+ width : parseInt ( params . get ( 'width' ) , 10 ) || null ,
151+ height : parseInt ( params . get ( 'height' ) , 10 ) || null ,
152+ fit : params . has ( 'cropped' ) ? sharp . fit . cover : sharp . fit . inside ,
153+ withoutEnlargement : true ,
154+ } ) ;
155+ }
156+
157+ function isResponseTooLarge ( responseBody ) {
158+ return 1330000 < Buffer . byteLength ( responseBody ) ;
159+ }
160+
91161async function streamToBuffer ( stream ) {
92162 const chunks = [ ] ;
93163
0 commit comments