@@ -37,12 +37,20 @@ const AVIF = 'image/avif'
37
37
const WEBP = 'image/webp'
38
38
const PNG = 'image/png'
39
39
const JPEG = 'image/jpeg'
40
+ const JXL = 'image/jxl'
41
+ const JP2 = 'image/jp2'
42
+ const HEIC = 'image/heic'
40
43
const GIF = 'image/gif'
41
44
const SVG = 'image/svg+xml'
42
45
const ICO = 'image/x-icon'
46
+ const ICNS = 'image/x-icns'
47
+ const TIFF = 'image/tiff'
48
+ const BMP = 'image/bmp'
49
+ const PDF = 'application/pdf'
43
50
const CACHE_VERSION = 3
44
51
const ANIMATABLE_TYPES = [ WEBP , PNG , GIF ]
45
52
const VECTOR_TYPES = [ SVG ]
53
+ const BYPASS_TYPES = [ SVG , ICO , ICNS , BMP , JXL , HEIC ]
46
54
const BLUR_IMG_SIZE = 8 // should match `next-image-loader`
47
55
const BLUR_QUALITY = 70 // should match `next-image-loader`
48
56
@@ -118,7 +126,9 @@ async function writeToCacheDir(
118
126
* it matches the "magic number" of known file signatures.
119
127
* https://en.wikipedia.org/wiki/List_of_file_signatures
120
128
*/
121
- export function detectContentType ( buffer : Buffer ) {
129
+ export async function detectContentType (
130
+ buffer : Buffer
131
+ ) : Promise < string | null > {
122
132
if ( [ 0xff , 0xd8 , 0xff ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
123
133
return JPEG
124
134
}
@@ -152,6 +162,89 @@ export function detectContentType(buffer: Buffer) {
152
162
if ( [ 0x00 , 0x00 , 0x01 , 0x00 ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
153
163
return ICO
154
164
}
165
+ if ( [ 0x69 , 0x63 , 0x6e , 0x73 ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
166
+ return ICNS
167
+ }
168
+ if ( [ 0x49 , 0x49 , 0x2a , 0x00 ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
169
+ return TIFF
170
+ }
171
+ if ( [ 0x42 , 0x4d ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
172
+ return BMP
173
+ }
174
+ if ( [ 0xff , 0x0a ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
175
+ return JXL
176
+ }
177
+ if (
178
+ [
179
+ 0x00 , 0x00 , 0x00 , 0x0c , 0x4a , 0x58 , 0x4c , 0x20 , 0x0d , 0x0a , 0x87 , 0x0a ,
180
+ ] . every ( ( b , i ) => buffer [ i ] === b )
181
+ ) {
182
+ return JXL
183
+ }
184
+ if (
185
+ [ 0 , 0 , 0 , 0 , 0x66 , 0x74 , 0x79 , 0x70 , 0x68 , 0x65 , 0x69 , 0x63 ] . every (
186
+ ( b , i ) => ! b || buffer [ i ] === b
187
+ )
188
+ ) {
189
+ return HEIC
190
+ }
191
+ if ( [ 0x25 , 0x50 , 0x44 , 0x46 , 0x2d ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
192
+ return PDF
193
+ }
194
+ if (
195
+ [
196
+ 0x00 , 0x00 , 0x00 , 0x0c , 0x6a , 0x50 , 0x20 , 0x20 , 0x0d , 0x0a , 0x87 , 0x0a ,
197
+ ] . every ( ( b , i ) => buffer [ i ] === b )
198
+ ) {
199
+ return JP2
200
+ }
201
+
202
+ // Fallback to sharp if available
203
+ if ( sharp ) {
204
+ const meta = await sharp ( buffer )
205
+ . metadata ( )
206
+ . catch ( ( _ ) => null )
207
+ switch ( meta ?. format ) {
208
+ case 'avif' :
209
+ return AVIF
210
+ case 'webp' :
211
+ return WEBP
212
+ case 'png' :
213
+ return PNG
214
+ case 'jpeg' :
215
+ case 'jpg' :
216
+ return JPEG
217
+ case 'gif' :
218
+ return GIF
219
+ case 'svg' :
220
+ return SVG
221
+ case 'jxl' :
222
+ return JXL
223
+ case 'jp2' :
224
+ return JP2
225
+ case 'tiff' :
226
+ case 'tif' :
227
+ return TIFF
228
+ case 'pdf' :
229
+ return PDF
230
+ case 'dcraw' :
231
+ case 'dz' :
232
+ case 'exr' :
233
+ case 'fits' :
234
+ case 'heif' :
235
+ case 'input' :
236
+ case 'magick' :
237
+ case 'openslide' :
238
+ case 'ppm' :
239
+ case 'rad' :
240
+ case 'raw' :
241
+ case 'v' :
242
+ case undefined :
243
+ default :
244
+ return null
245
+ }
246
+ }
247
+
155
248
return null
156
249
}
157
250
@@ -639,54 +732,52 @@ export async function imageOptimizer(
639
732
) : Promise < { buffer : Buffer ; contentType : string ; maxAge : number } > {
640
733
const { href, quality, width, mimeType } = paramsResult
641
734
const upstreamBuffer = imageUpstream . buffer
642
- const maxAge = getMaxAge ( imageUpstream . cacheControl )
643
- const upstreamType =
644
- detectContentType ( upstreamBuffer ) ||
645
- imageUpstream . contentType ?. toLowerCase ( ) . trim ( )
646
-
647
- if ( upstreamType ) {
648
- if (
649
- upstreamType . startsWith ( 'image/svg' ) &&
650
- ! nextConfig . images . dangerouslyAllowSVG
651
- ) {
652
- Log . error (
653
- `The requested resource "${ href } " has type "${ upstreamType } " but dangerouslyAllowSVG is disabled`
654
- )
655
- throw new ImageError (
656
- 400 ,
657
- '"url" parameter is valid but image type is not allowed'
658
- )
659
- }
735
+ const maxAge = Math . max (
736
+ nextConfig . images . minimumCacheTTL ,
737
+ getMaxAge ( imageUpstream . cacheControl )
738
+ )
739
+ const upstreamType = await detectContentType ( upstreamBuffer )
660
740
661
- if ( ANIMATABLE_TYPES . includes ( upstreamType ) && isAnimated ( upstreamBuffer ) ) {
662
- Log . warnOnce (
663
- `The requested resource "${ href } " is an animated image so it will not be optimized. Consider adding the "unoptimized" property to the <Image>.`
664
- )
665
- return { buffer : upstreamBuffer , contentType : upstreamType , maxAge }
666
- }
667
- if ( VECTOR_TYPES . includes ( upstreamType ) ) {
668
- // We don't warn here because we already know that "dangerouslyAllowSVG"
669
- // was enabled above, therefore the user explicitly opted in.
670
- // If we add more VECTOR_TYPES besides SVG, perhaps we could warn for those.
671
- return { buffer : upstreamBuffer , contentType : upstreamType , maxAge }
672
- }
673
- if ( ! upstreamType . startsWith ( 'image/' ) || upstreamType . includes ( ',' ) ) {
674
- Log . error (
675
- "The requested resource isn't a valid image for" ,
676
- href ,
677
- 'received' ,
678
- upstreamType
679
- )
680
- throw new ImageError ( 400 , "The requested resource isn't a valid image." )
681
- }
741
+ if (
742
+ ! upstreamType ||
743
+ ! upstreamType . startsWith ( 'image/' ) ||
744
+ upstreamType . includes ( ',' )
745
+ ) {
746
+ Log . error (
747
+ "The requested resource isn't a valid image for" ,
748
+ href ,
749
+ 'received' ,
750
+ upstreamType
751
+ )
752
+ throw new ImageError ( 400 , "The requested resource isn't a valid image." )
753
+ }
754
+ if (
755
+ upstreamType . startsWith ( 'image/svg' ) &&
756
+ ! nextConfig . images . dangerouslyAllowSVG
757
+ ) {
758
+ Log . error (
759
+ `The requested resource "${ href } " has type "${ upstreamType } " but dangerouslyAllowSVG is disabled`
760
+ )
761
+ throw new ImageError (
762
+ 400 ,
763
+ '"url" parameter is valid but image type is not allowed'
764
+ )
765
+ }
766
+ if ( ANIMATABLE_TYPES . includes ( upstreamType ) && isAnimated ( upstreamBuffer ) ) {
767
+ Log . warnOnce (
768
+ `The requested resource "${ href } " is an animated image so it will not be optimized. Consider adding the "unoptimized" property to the <Image>.`
769
+ )
770
+ return { buffer : upstreamBuffer , contentType : upstreamType , maxAge }
771
+ }
772
+ if ( BYPASS_TYPES . includes ( upstreamType ) ) {
773
+ return { buffer : upstreamBuffer , contentType : upstreamType , maxAge }
682
774
}
683
775
684
776
let contentType : string
685
777
686
778
if ( mimeType ) {
687
779
contentType = mimeType
688
780
} else if (
689
- upstreamType ?. startsWith ( 'image/' ) &&
690
781
getExtension ( upstreamType ) &&
691
782
upstreamType !== WEBP &&
692
783
upstreamType !== AVIF
0 commit comments