@@ -36,15 +36,19 @@ const AVIF = 'image/avif'
36
36
const WEBP = 'image/webp'
37
37
const PNG = 'image/png'
38
38
const JPEG = 'image/jpeg'
39
+ const JXL = 'image/jxl'
40
+ const JP2 = 'image/jp2'
41
+ const HEIC = 'image/heic'
39
42
const GIF = 'image/gif'
40
43
const SVG = 'image/svg+xml'
41
44
const ICO = 'image/x-icon'
42
45
const ICNS = 'image/x-icns'
43
46
const TIFF = 'image/tiff'
44
47
const BMP = 'image/bmp'
48
+ const PDF = 'application/pdf'
45
49
const CACHE_VERSION = 4
46
50
const ANIMATABLE_TYPES = [ WEBP , PNG , GIF ]
47
- const BYPASS_TYPES = [ SVG , ICO , ICNS , BMP ]
51
+ const BYPASS_TYPES = [ SVG , ICO , ICNS , BMP , JXL , HEIC ]
48
52
const BLUR_IMG_SIZE = 8 // should match `next-image-loader`
49
53
const BLUR_QUALITY = 70 // should match `next-image-loader`
50
54
@@ -152,7 +156,9 @@ async function writeToCacheDir(
152
156
* it matches the "magic number" of known file signatures.
153
157
* https://en.wikipedia.org/wiki/List_of_file_signatures
154
158
*/
155
- export function detectContentType ( buffer : Buffer ) {
159
+ export async function detectContentType (
160
+ buffer : Buffer
161
+ ) : Promise < string | null > {
156
162
if ( [ 0xff , 0xd8 , 0xff ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
157
163
return JPEG
158
164
}
@@ -198,7 +204,77 @@ export function detectContentType(buffer: Buffer) {
198
204
if ( [ 0x42 , 0x4d ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
199
205
return BMP
200
206
}
201
- return null
207
+ if ( [ 0xff , 0x0a ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
208
+ return JXL
209
+ }
210
+ if (
211
+ [
212
+ 0x00 , 0x00 , 0x00 , 0x0c , 0x4a , 0x58 , 0x4c , 0x20 , 0x0d , 0x0a , 0x87 , 0x0a ,
213
+ ] . every ( ( b , i ) => buffer [ i ] === b )
214
+ ) {
215
+ return JXL
216
+ }
217
+ if (
218
+ [ 0 , 0 , 0 , 0 , 0x66 , 0x74 , 0x79 , 0x70 , 0x68 , 0x65 , 0x69 , 0x63 ] . every (
219
+ ( b , i ) => ! b || buffer [ i ] === b
220
+ )
221
+ ) {
222
+ return HEIC
223
+ }
224
+ if ( [ 0x25 , 0x50 , 0x44 , 0x46 , 0x2d ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
225
+ return PDF
226
+ }
227
+ if (
228
+ [
229
+ 0x00 , 0x00 , 0x00 , 0x0c , 0x6a , 0x50 , 0x20 , 0x20 , 0x0d , 0x0a , 0x87 , 0x0a ,
230
+ ] . every ( ( b , i ) => buffer [ i ] === b )
231
+ ) {
232
+ return JP2
233
+ }
234
+
235
+ const sharp = getSharp ( null )
236
+ const meta = await sharp ( buffer )
237
+ . metadata ( )
238
+ . catch ( ( _ ) => null )
239
+ switch ( meta ?. format ) {
240
+ case 'avif' :
241
+ return AVIF
242
+ case 'webp' :
243
+ return WEBP
244
+ case 'png' :
245
+ return PNG
246
+ case 'jpeg' :
247
+ case 'jpg' :
248
+ return JPEG
249
+ case 'gif' :
250
+ return GIF
251
+ case 'svg' :
252
+ return SVG
253
+ case 'jxl' :
254
+ return JXL
255
+ case 'jp2' :
256
+ return JP2
257
+ case 'tiff' :
258
+ case 'tif' :
259
+ return TIFF
260
+ case 'pdf' :
261
+ return PDF
262
+ case 'dcraw' :
263
+ case 'dz' :
264
+ case 'exr' :
265
+ case 'fits' :
266
+ case 'heif' :
267
+ case 'input' :
268
+ case 'magick' :
269
+ case 'openslide' :
270
+ case 'ppm' :
271
+ case 'rad' :
272
+ case 'raw' :
273
+ case 'v' :
274
+ case undefined :
275
+ default :
276
+ return null
277
+ }
202
278
}
203
279
204
280
export class ImageOptimizerCache {
@@ -702,58 +778,58 @@ export async function imageOptimizer(
702
778
getMaxAge ( imageUpstream . cacheControl )
703
779
)
704
780
705
- const upstreamType =
706
- detectContentType ( upstreamBuffer ) ||
707
- imageUpstream . contentType ?. toLowerCase ( ) . trim ( )
708
-
709
- if ( upstreamType ) {
710
- if (
711
- upstreamType . startsWith ( 'image/svg' ) &&
712
- ! nextConfig . images . dangerouslyAllowSVG
713
- ) {
714
- if ( ! opts . silent ) {
715
- Log . error (
716
- `The requested resource "${ href } " has type "${ upstreamType } " but dangerouslyAllowSVG is disabled`
717
- )
718
- }
719
- throw new ImageError (
720
- 400 ,
721
- '"url" parameter is valid but image type is not allowed'
781
+ const upstreamType = await detectContentType ( upstreamBuffer )
782
+
783
+ if (
784
+ ! upstreamType ||
785
+ ! upstreamType . startsWith ( 'image/' ) ||
786
+ upstreamType . includes ( ',' )
787
+ ) {
788
+ if ( ! opts . silent ) {
789
+ Log . error (
790
+ "The requested resource isn't a valid image for" ,
791
+ href ,
792
+ 'received' ,
793
+ upstreamType
722
794
)
723
795
}
724
- if ( ANIMATABLE_TYPES . includes ( upstreamType ) && isAnimated ( upstreamBuffer ) ) {
725
- if ( ! opts . silent ) {
726
- Log . warnOnce (
727
- `The requested resource "${ href } " is an animated image so it will not be optimized. Consider adding the "unoptimized" property to the <Image>.`
728
- )
729
- }
730
- return {
731
- buffer : upstreamBuffer ,
732
- contentType : upstreamType ,
733
- maxAge,
734
- etag : upstreamEtag ,
735
- upstreamEtag,
736
- }
796
+ throw new ImageError ( 400 , "The requested resource isn't a valid image." )
797
+ }
798
+ if (
799
+ upstreamType . startsWith ( 'image/svg' ) &&
800
+ ! nextConfig . images . dangerouslyAllowSVG
801
+ ) {
802
+ if ( ! opts . silent ) {
803
+ Log . error (
804
+ `The requested resource "${ href } " has type "${ upstreamType } " but dangerouslyAllowSVG is disabled. Consider adding the "unoptimized" property to the <Image>.`
805
+ )
737
806
}
738
- if ( BYPASS_TYPES . includes ( upstreamType ) ) {
739
- return {
740
- buffer : upstreamBuffer ,
741
- contentType : upstreamType ,
742
- maxAge,
743
- etag : upstreamEtag ,
744
- upstreamEtag,
745
- }
807
+ throw new ImageError (
808
+ 400 ,
809
+ '"url" parameter is valid but image type is not allowed'
810
+ )
811
+ }
812
+ if ( ANIMATABLE_TYPES . includes ( upstreamType ) && isAnimated ( upstreamBuffer ) ) {
813
+ if ( ! opts . silent ) {
814
+ Log . warnOnce (
815
+ `The requested resource "${ href } " is an animated image so it will not be optimized. Consider adding the "unoptimized" property to the <Image>.`
816
+ )
746
817
}
747
- if ( ! upstreamType . startsWith ( 'image/' ) || upstreamType . includes ( ',' ) ) {
748
- if ( ! opts . silent ) {
749
- Log . error (
750
- "The requested resource isn't a valid image for" ,
751
- href ,
752
- 'received' ,
753
- upstreamType
754
- )
755
- }
756
- throw new ImageError ( 400 , "The requested resource isn't a valid image." )
818
+ return {
819
+ buffer : upstreamBuffer ,
820
+ contentType : upstreamType ,
821
+ maxAge,
822
+ etag : upstreamEtag ,
823
+ upstreamEtag,
824
+ }
825
+ }
826
+ if ( BYPASS_TYPES . includes ( upstreamType ) ) {
827
+ return {
828
+ buffer : upstreamBuffer ,
829
+ contentType : upstreamType ,
830
+ maxAge,
831
+ etag : upstreamEtag ,
832
+ upstreamEtag,
757
833
}
758
834
}
759
835
@@ -762,7 +838,6 @@ export async function imageOptimizer(
762
838
if ( mimeType ) {
763
839
contentType = mimeType
764
840
} else if (
765
- upstreamType ?. startsWith ( 'image/' ) &&
766
841
getExtension ( upstreamType ) &&
767
842
upstreamType !== WEBP &&
768
843
upstreamType !== AVIF
0 commit comments