@@ -30,15 +30,18 @@ const optimizeImagesIntegration: AstroIntegration = {
3030 name : "optimize-images" ,
3131 hooks : {
3232 async "astro:build:done" ( { dir, routes } ) {
33- type Images = { original : string ; webp : string | string [ ] } [ ] ;
33+ type Resize = "" | "up" | "down" ;
34+ type Images = { original : string ; webp : string [ ] ; resize : Resize } [ ] ;
3435 type MatchGroups = {
35- resize ?: string ;
36+ resize : Resize ;
3637 preSrc : string ;
3738 src : string ;
3839 postSrc : string ;
3940 } ;
4041
41- const resizeSizes = [ 1 , 1.5 , 2 , 3 , 4 ] ;
42+ const resizeSuffixes = [ "a" , "b" , "c" , "d" , "e" ] ;
43+ const resizeUpMultipliers = [ 1 , 1.5 , 2 , 3 , 4 ] ;
44+ const resizeDownMultipliers = [ 0.25 , 0.375 , 0.5 , 0.75 , 1 ] ;
4245 const images : Images = [ ] ;
4346
4447 for ( const route of routes ) {
@@ -49,29 +52,33 @@ const optimizeImagesIntegration: AstroIntegration = {
4952 const htmlPath = nodeUrl . fileURLToPath ( route . distURL ) ;
5053 let html = await fs . readFile ( htmlPath , "utf-8" ) ;
5154 const matches = html . matchAll (
52- / < i m g o p t i m i z e - i m a g e (?< resize > r e s i z e = " t r u e " ) ? (?< preSrc > .+ ) s r c = " (?< src > [ ^ " ] + ) " (?< postSrc > .+ ) > / g
55+ / < i m g o p t i m i z e - i m a g e r e s i z e = " (?< resize > [ a - z - ] * ) " (?< preSrc > .+ ) s r c = " (?< src > [ ^ " ] + ) " (?< postSrc > .+ ) > / g
5356 ) ;
5457
5558 for ( const match of matches ) {
5659 const { resize, preSrc, src, postSrc } = match . groups as MatchGroups ;
57- const baseName = src . slice ( 0 , src . lastIndexOf ( "." ) ) ;
60+ const extensionlessPath = src . slice ( 0 , src . lastIndexOf ( "." ) ) ;
5861 let webp ;
5962
60- if ( resize ) {
61- webp = resizeSizes . map ( ( size ) => `${ baseName } - ${ size } .webp` ) ;
63+ if ( resize === "" ) {
64+ webp = [ `${ extensionlessPath } .webp` ] ;
6265 } else {
63- webp = `${ baseName } .webp` ;
66+ webp = resizeSuffixes . map (
67+ ( suffix ) => `${ extensionlessPath } ${ suffix } .webp`
68+ ) ;
6469 }
6570
66- images . push ( { original : src , webp } ) ;
71+ images . push ( { original : src , webp, resize } ) ;
6772
6873 html = html . replace (
6974 match [ 0 ] ,
7075 `
7176 <source srcset="${
72- typeof webp === "string"
73- ? webp
74- : webp . map ( ( fileName , i ) => `${ fileName } ${ resizeSizes [ i ] } x` )
77+ webp . length === 1
78+ ? webp [ 0 ]
79+ : webp . map (
80+ ( filePath , i ) => `${ filePath } ${ resizeUpMultipliers [ i ] } x`
81+ )
7582 } " type="image/webp">
7683 <img ${ preSrc } src="${ src } " ${ postSrc } >
7784 `
@@ -86,27 +93,34 @@ const optimizeImagesIntegration: AstroIntegration = {
8693 name : "astro-optimize-images" ,
8794 create : true
8895 } ) as string ;
89- for ( let { original, webp } of images ) {
96+ const sharpOptions = { limitInputPixels : false } ;
97+ const imageOptions = {
98+ png : { compressionLevel : 9 , quality : 80 } ,
99+ webp : { effort : 6 }
100+ } ;
101+ for ( let { original, webp, resize } of images ) {
90102 original = path . join ( distDir , original ) ;
103+ webp = webp . map ( ( filePath ) => path . join ( distDir , filePath ) ) ;
91104
92- if ( typeof webp === "string" ) {
93- webp = [ path . join ( distDir , webp ) ] ;
94- } else {
95- webp = webp . map ( ( fileName ) => path . join ( distDir , fileName ) ) ;
96- }
97-
98- const format = path . extname ( original ) . slice ( 1 ) ;
105+ const format = path . extname ( original ) . slice ( 1 ) as "gif" | "png" ;
99106 const image = sharp ( original , {
100- limitInputPixels : false ,
107+ ... sharpOptions ,
101108 animated : format === "gif"
102109 } ) ;
103- const { width, height } = await image . metadata ( ) ;
104110
111+ const { width, height } = await image . metadata ( ) ;
112+ const multiply =
113+ webp . length > 1 &&
114+ typeof width === "number" &&
115+ typeof height === "number" ;
116+ const multipliers =
117+ resize === "up" ? resizeUpMultipliers : resizeDownMultipliers ;
105118 for ( let i = 0 ; i < webp . length ; i ++ ) {
106119 const cachedWebp = path . join (
107120 cacheDir ,
108121 path . basename ( webp [ i ] as string )
109122 ) ;
123+
110124 try {
111125 await fs . access ( cachedWebp ) ;
112126
@@ -116,24 +130,36 @@ const optimizeImagesIntegration: AstroIntegration = {
116130 } catch {
117131 const clone = image . clone ( ) ;
118132
119- if ( typeof width === "number" && typeof height === "number" ) {
133+ if ( multiply ) {
120134 clone . resize (
121- width * ( resizeSizes [ i ] as number ) ,
122- height * ( resizeSizes [ i ] as number )
135+ width * ( multipliers [ i ] as number ) ,
136+ height * ( multipliers [ i ] as number )
123137 ) ;
124138 }
125139
126- await clone
127- . webp ( {
128- effort : 6 ,
129- // For some reason, using lossless compression mode on large
130- // GIFs increase their WebP size.
131- lossless :
132- format === "gif" &&
133- // Less than 1MB
134- ( await fs . stat ( original ) ) . size / 1000 < 1000
135- } )
136- . toFile ( webp [ i ] as string ) ;
140+ // Compressing images in their original format before converting
141+ // them to WebP reduces the WebP file size. GIFs are compressed
142+ // using Gifsicle instead of Sharp, so that step is skipped for GIFs
143+ // for now.
144+ if ( format !== "gif" ) {
145+ await sharp (
146+ await clone . toFormat ( format , imageOptions [ format ] ) . toBuffer ( ) ,
147+ sharpOptions
148+ )
149+ . webp ( imageOptions . webp )
150+ . toFile ( webp [ i ] as string ) ;
151+ } else {
152+ await clone
153+ . webp ( {
154+ ...imageOptions . webp ,
155+ // For some reason, using lossless compression mode on large
156+ // GIFs increase their WebP size but reduces the size of
157+ // smaller GIFs, so it is turned on for GIFs smaller than 1MB.
158+ lossless : ( await fs . stat ( original ) ) . size / 1000 < 1000
159+ } )
160+ . toFile ( webp [ i ] as string ) ;
161+ }
162+
137163 await fs . copyFile ( webp [ i ] as string , cachedWebp ) ;
138164 }
139165 }
@@ -161,9 +187,7 @@ const optimizeImagesIntegration: AstroIntegration = {
161187 case "png" : {
162188 await fs . writeFile (
163189 original ,
164- await image
165- . png ( { compressionLevel : 9 , palette : true } )
166- . toBuffer ( ) ,
190+ await image . png ( imageOptions . png ) . toBuffer ( ) ,
167191 "binary"
168192 ) ;
169193
0 commit comments