2021-12-20 14:01:35 +01:00
import { mediaType } from 'next/dist/compiled/@hapi/accept'
2020-10-16 13:10:01 +02:00
import { createHash } from 'crypto'
2022-02-09 00:46:59 +01:00
import { promises } from 'fs'
2021-12-20 14:01:35 +01:00
import { getOrientation , Orientation } from 'next/dist/compiled/get-orientation'
import imageSizeOf from 'next/dist/compiled/image-size'
2021-02-18 11:23:24 +01:00
import { IncomingMessage , ServerResponse } from 'http'
2020-10-17 21:22:10 +02:00
import isAnimated from 'next/dist/compiled/is-animated'
2021-11-02 00:23:24 +01:00
import contentDisposition from 'next/dist/compiled/content-disposition'
2021-02-18 11:23:24 +01:00
import { join } from 'path'
import nodeUrl , { UrlWithParsedQuery } from 'url'
2022-02-09 00:46:59 +01:00
import { NextConfigComplete } from './config-shared'
2022-08-30 00:19:39 +02:00
import {
processBuffer ,
decodeBuffer ,
Operation ,
getMetadata ,
} from './lib/squoosh/main'
2021-02-18 11:23:24 +01:00
import { sendEtagResponse } from './send-payload'
import { getContentType , getExtension } from './serve-static'
2021-12-21 16:13:45 +01:00
import chalk from 'next/dist/compiled/chalk'
2022-01-14 22:01:35 +01:00
import { NextUrlWithParsedQuery } from './request-meta'
2022-02-09 00:46:59 +01:00
import { IncrementalCacheEntry , IncrementalCacheValue } from './response-cache'
2022-03-17 18:06:44 +01:00
import { mockRequest } from './lib/mock-request'
2022-05-05 04:19:16 +02:00
import { hasMatch } from '../shared/lib/match-remote-pattern'
2022-08-30 00:19:39 +02:00
import { getImageBlurSvg } from '../shared/lib/image-blur-svg'
2020-10-16 13:10:01 +02:00
2022-02-02 22:27:56 +01:00
type XCacheHeader = 'MISS' | 'HIT' | 'STALE'
2021-10-12 01:17:47 +02:00
const AVIF = 'image/avif'
2020-10-16 13:10:01 +02:00
const WEBP = 'image/webp'
const PNG = 'image/png'
const JPEG = 'image/jpeg'
2020-10-17 21:22:10 +02:00
const GIF = 'image/gif'
2020-10-24 03:26:52 +02:00
const SVG = 'image/svg+xml'
2021-06-30 23:26:20 +02:00
const CACHE_VERSION = 3
2020-10-17 21:22:10 +02:00
const ANIMATABLE_TYPES = [ WEBP , PNG , GIF ]
2020-10-24 03:26:52 +02:00
const VECTOR_TYPES = [ SVG ]
2021-07-10 22:27:14 +02:00
const BLUR_IMG_SIZE = 8 // should match `next-image-loader`
2022-08-30 00:19:39 +02:00
const BLUR_QUALITY = 70 // should match `next-image-loader`
2021-04-13 00:38:51 +02:00
2021-07-23 01:11:17 +02:00
let sharp :
| ( (
input? : string | Buffer ,
options? : import ( 'sharp' ) . SharpOptions
) = > import ( 'sharp' ) . Sharp )
| undefined
try {
sharp = require ( process . env . NEXT_SHARP_PATH || 'sharp' )
} catch ( e ) {
// Sharp not present on the server, Squoosh fallback will be used
}
2021-10-12 01:17:47 +02:00
let showSharpMissingWarning = process . env . NODE_ENV === 'production'
2021-07-23 01:11:17 +02:00
2022-02-09 00:46:59 +01:00
export interface ImageParamsResult {
href : string
isAbsolute : boolean
isStatic : boolean
width : number
quality : number
mimeType : string
sizes : number [ ]
minimumCacheTTL : number
}
2020-10-25 05:54:22 +01:00
2022-08-15 16:29:51 +02:00
function getSupportedMimeType ( options : string [ ] , accept = '' ) : string {
const mimeType = mediaType ( accept , options )
return accept . includes ( mimeType ) ? mimeType : ''
}
export function getHash ( items : ( string | number | Buffer ) [ ] ) {
const hash = createHash ( 'sha256' )
for ( let item of items ) {
if ( typeof item === 'number' ) hash . update ( String ( item ) )
else {
hash . update ( item )
}
}
// See https://en.wikipedia.org/wiki/Base64#Filenames
return hash . digest ( 'base64' ) . replace ( /\//g , '-' )
}
async function writeToCacheDir (
dir : string ,
extension : string ,
maxAge : number ,
expireAt : number ,
buffer : Buffer ,
etag : string
) {
const filename = join ( dir , ` ${ maxAge } . ${ expireAt } . ${ etag } . ${ extension } ` )
// Added in: v14.14.0 https://nodejs.org/api/fs.html#fspromisesrmpath-options
// attempt cleaning up existing stale cache
if ( ( promises as any ) . rm ) {
await ( promises as any )
. rm ( dir , { force : true , recursive : true } )
. catch ( ( ) = > { } )
} else {
await promises . rmdir ( dir , { recursive : true } ) . catch ( ( ) = > { } )
}
await promises . mkdir ( dir , { recursive : true } )
await promises . writeFile ( filename , buffer )
}
/ * *
* Inspects the first few bytes of a buffer to determine if
* it matches the "magic number" of known file signatures .
* https : //en.wikipedia.org/wiki/List_of_file_signatures
* /
export function detectContentType ( buffer : Buffer ) {
if ( [ 0xff , 0xd8 , 0xff ] . every ( ( b , i ) = > buffer [ i ] === b ) ) {
return JPEG
}
if (
[ 0x89 , 0x50 , 0x4e , 0x47 , 0x0d , 0x0a , 0x1a , 0x0a ] . every (
( b , i ) = > buffer [ i ] === b
)
) {
return PNG
}
if ( [ 0x47 , 0x49 , 0x46 , 0x38 ] . every ( ( b , i ) = > buffer [ i ] === b ) ) {
return GIF
}
if (
[ 0x52 , 0x49 , 0x46 , 0x46 , 0 , 0 , 0 , 0 , 0x57 , 0x45 , 0x42 , 0x50 ] . every (
( b , i ) = > ! b || buffer [ i ] === b
)
) {
return WEBP
}
if ( [ 0x3c , 0x3f , 0x78 , 0x6d , 0x6c ] . every ( ( b , i ) = > buffer [ i ] === b ) ) {
return SVG
}
if (
[ 0 , 0 , 0 , 0 , 0x66 , 0x74 , 0x79 , 0x70 , 0x61 , 0x76 , 0x69 , 0x66 ] . every (
( b , i ) = > ! b || buffer [ i ] === b
)
) {
return AVIF
}
return null
}
2022-02-09 00:46:59 +01:00
export class ImageOptimizerCache {
private cacheDir : string
private nextConfig : NextConfigComplete
static validateParams (
req : IncomingMessage ,
query : UrlWithParsedQuery [ 'query' ] ,
nextConfig : NextConfigComplete ,
isDev : boolean
) : ImageParamsResult | { errorMessage : string } {
const imageData = nextConfig . images
const {
deviceSizes = [ ] ,
imageSizes = [ ] ,
domains = [ ] ,
minimumCacheTTL = 60 ,
formats = [ 'image/webp' ] ,
} = imageData
2022-09-01 00:44:17 +02:00
const remotePatterns = nextConfig . images ? . remotePatterns || [ ]
2022-02-09 00:46:59 +01:00
const { url , w , q } = query
let href : string
if ( ! url ) {
return { errorMessage : '"url" parameter is required' }
} else if ( Array . isArray ( url ) ) {
return { errorMessage : '"url" parameter cannot be an array' }
}
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
let isAbsolute : boolean
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
if ( url . startsWith ( '/' ) ) {
href = url
isAbsolute = false
} else {
let hrefParsed : URL
2020-10-19 19:03:35 +02:00
2022-02-09 00:46:59 +01:00
try {
hrefParsed = new URL ( url )
href = hrefParsed . toString ( )
isAbsolute = true
} catch ( _error ) {
return { errorMessage : '"url" parameter is invalid' }
}
2020-10-19 19:03:35 +02:00
2022-02-09 00:46:59 +01:00
if ( ! [ 'http:' , 'https:' ] . includes ( hrefParsed . protocol ) ) {
return { errorMessage : '"url" parameter is invalid' }
}
2022-05-05 04:19:16 +02:00
if ( ! hasMatch ( domains , remotePatterns , hrefParsed ) ) {
2022-02-09 00:46:59 +01:00
return { errorMessage : '"url" parameter is not allowed' }
}
2020-10-16 13:10:01 +02:00
}
2022-02-09 00:46:59 +01:00
if ( ! w ) {
return { errorMessage : '"w" parameter (width) is required' }
} else if ( Array . isArray ( w ) ) {
return { errorMessage : '"w" parameter (width) cannot be an array' }
2020-10-19 19:03:35 +02:00
}
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
if ( ! q ) {
return { errorMessage : '"q" parameter (quality) is required' }
} else if ( Array . isArray ( q ) ) {
return { errorMessage : '"q" parameter (quality) cannot be an array' }
2020-10-19 19:03:35 +02:00
}
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
const width = parseInt ( w , 10 )
2020-10-16 13:10:01 +02:00
2022-07-06 16:09:29 +02:00
if ( width <= 0 || isNaN ( width ) ) {
2022-02-09 00:46:59 +01:00
return {
errorMessage : '"w" parameter (width) must be a number greater than 0' ,
}
}
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
const sizes = [ . . . ( deviceSizes || [ ] ) , . . . ( imageSizes || [ ] ) ]
2021-06-04 10:06:00 +02:00
2022-02-09 00:46:59 +01:00
if ( isDev ) {
sizes . push ( BLUR_IMG_SIZE )
}
2020-10-16 13:10:01 +02:00
2022-08-30 00:19:39 +02:00
const isValidSize =
sizes . includes ( width ) || ( isDev && width <= BLUR_IMG_SIZE )
if ( ! isValidSize ) {
2022-02-09 00:46:59 +01:00
return {
errorMessage : ` "w" parameter (width) of ${ width } is not allowed ` ,
}
}
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
const quality = parseInt ( q )
2021-01-04 19:09:07 +01:00
2022-02-09 00:46:59 +01:00
if ( isNaN ( quality ) || quality < 1 || quality > 100 ) {
return {
errorMessage :
'"q" parameter (quality) must be a number between 1 and 100' ,
}
}
2021-07-10 22:27:14 +02:00
2022-02-09 00:46:59 +01:00
const mimeType = getSupportedMimeType ( formats || [ ] , req . headers [ 'accept' ] )
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
const isStatic = url . startsWith (
` ${ nextConfig . basePath || '' } /_next/static/media `
)
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
return {
href ,
sizes ,
isAbsolute ,
isStatic ,
width ,
quality ,
mimeType ,
minimumCacheTTL ,
}
2020-10-16 13:10:01 +02:00
}
2022-02-09 00:46:59 +01:00
static getCacheKey ( {
href ,
width ,
quality ,
mimeType ,
} : {
href : string
width : number
quality : number
mimeType : string
} ) : string {
return getHash ( [ CACHE_VERSION , href , width , quality , mimeType ] )
}
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
constructor ( {
distDir ,
nextConfig ,
} : {
distDir : string
nextConfig : NextConfigComplete
} ) {
this . cacheDir = join ( distDir , 'cache' , 'images' )
this . nextConfig = nextConfig
2021-04-13 00:38:51 +02:00
}
2022-02-09 00:46:59 +01:00
async get ( cacheKey : string ) : Promise < IncrementalCacheEntry | null > {
try {
const cacheDir = join ( this . cacheDir , cacheKey )
const files = await promises . readdir ( cacheDir )
const now = Date . now ( )
for ( const file of files ) {
const [ maxAgeSt , expireAtSt , etag , extension ] = file . split ( '.' )
const buffer = await promises . readFile ( join ( cacheDir , file ) )
2021-06-30 23:26:20 +02:00
const expireAt = Number ( expireAtSt )
2022-02-09 00:46:59 +01:00
const maxAge = Number ( maxAgeSt )
return {
value : {
kind : 'IMAGE' ,
etag ,
buffer ,
extension ,
} ,
revalidateAfter :
Math . max ( maxAge , this . nextConfig . images . minimumCacheTTL ) * 1000 +
Date . now ( ) ,
curRevalidate : maxAge ,
isStale : now > expireAt ,
2020-11-10 05:40:26 +01:00
}
2020-10-16 13:10:01 +02:00
}
2022-02-09 00:46:59 +01:00
} catch ( _ ) {
// failed to read from cache dir, treat as cache miss
}
return null
}
async set (
cacheKey : string ,
value : IncrementalCacheValue | null ,
revalidate? : number | false
) {
if ( value ? . kind !== 'IMAGE' ) {
throw new Error ( 'invariant attempted to set non-image to image-cache' )
2020-10-16 13:10:01 +02:00
}
2022-02-09 00:46:59 +01:00
if ( typeof revalidate !== 'number' ) {
throw new Error ( 'invariant revalidate must be a number for image-cache' )
}
const expireAt =
Math . max ( revalidate , this . nextConfig . images . minimumCacheTTL ) * 1000 +
Date . now ( )
2022-02-11 03:28:55 +01:00
try {
await writeToCacheDir (
join ( this . cacheDir , cacheKey ) ,
value . extension ,
revalidate ,
expireAt ,
value . buffer ,
value . etag
)
} catch ( err ) {
console . error ( ` Failed to write image to cache ${ cacheKey } ` , err )
}
2022-02-09 00:46:59 +01:00
}
}
export class ImageError extends Error {
statusCode : number
2020-10-19 19:03:35 +02:00
2022-02-09 00:46:59 +01:00
constructor ( statusCode : number , message : string ) {
super ( message )
2022-02-23 02:29:53 +01:00
// ensure an error status is used > 400
if ( statusCode >= 400 ) {
this . statusCode = statusCode
} else {
this . statusCode = 500
}
2022-02-09 00:46:59 +01:00
}
}
2020-10-19 19:03:35 +02:00
2022-08-15 16:29:51 +02:00
function parseCacheControl ( str : string | null ) : Map < string , string > {
const map = new Map < string , string > ( )
if ( ! str ) {
return map
}
for ( let directive of str . split ( ',' ) ) {
let [ key , value ] = directive . trim ( ) . split ( '=' )
key = key . toLowerCase ( )
if ( value ) {
value = value . toLowerCase ( )
}
map . set ( key , value )
}
return map
}
export function getMaxAge ( str : string | null ) : number {
const map = parseCacheControl ( str )
if ( map ) {
let age = map . get ( 's-maxage' ) || map . get ( 'max-age' ) || ''
if ( age . startsWith ( '"' ) && age . endsWith ( '"' ) ) {
age = age . slice ( 1 , - 1 )
}
const n = parseInt ( age , 10 )
if ( ! isNaN ( n ) ) {
return n
}
}
return 0
}
2022-02-09 00:46:59 +01:00
export async function imageOptimizer (
_req : IncomingMessage ,
_res : ServerResponse ,
paramsResult : ImageParamsResult ,
nextConfig : NextConfigComplete ,
2022-08-30 00:19:39 +02:00
isDev : boolean | undefined ,
2022-02-09 00:46:59 +01:00
handleRequest : (
newReq : IncomingMessage ,
newRes : ServerResponse ,
newParsedUrl? : NextUrlWithParsedQuery
) = > Promise < void >
) : Promise < { buffer : Buffer ; contentType : string ; maxAge : number } > {
let upstreamBuffer : Buffer
let upstreamType : string | null
let maxAge : number
const { isAbsolute , href , width , mimeType , quality } = paramsResult
if ( isAbsolute ) {
const upstreamRes = await fetch ( href )
if ( ! upstreamRes . ok ) {
console . error (
'upstream image response failed for' ,
href ,
upstreamRes . status
)
throw new ImageError (
upstreamRes . status ,
'"url" parameter is valid but upstream response is invalid'
)
}
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
upstreamBuffer = Buffer . from ( await upstreamRes . arrayBuffer ( ) )
upstreamType =
detectContentType ( upstreamBuffer ) ||
upstreamRes . headers . get ( 'Content-Type' )
maxAge = getMaxAge ( upstreamRes . headers . get ( 'Cache-Control' ) )
} else {
try {
2022-03-17 18:06:44 +01:00
const {
resBuffers ,
req : mockReq ,
res : mockRes ,
streamPromise : isStreamFinished ,
} = mockRequest ( href , _req . headers , _req . method || 'GET' , _req . connection )
2022-02-09 00:46:59 +01:00
await handleRequest ( mockReq , mockRes , nodeUrl . parse ( href , true ) )
await isStreamFinished
if ( ! mockRes . statusCode ) {
console . error ( 'image response failed for' , href , mockRes . statusCode )
throw new ImageError (
mockRes . statusCode ,
'"url" parameter is valid but internal response is invalid'
2021-06-30 23:26:20 +02:00
)
2021-04-13 00:38:51 +02:00
}
2022-02-09 00:46:59 +01:00
upstreamBuffer = Buffer . concat ( resBuffers )
upstreamType =
detectContentType ( upstreamBuffer ) || mockRes . getHeader ( 'Content-Type' )
maxAge = getMaxAge ( mockRes . getHeader ( 'Cache-Control' ) )
} catch ( err ) {
console . error ( 'upstream image response failed for' , href , err )
throw new ImageError (
500 ,
'"url" parameter is valid but upstream response is invalid'
)
2021-03-24 18:59:00 +01:00
}
2022-02-09 00:46:59 +01:00
}
2020-10-17 21:22:10 +02:00
2022-02-16 20:28:22 +01:00
if ( upstreamType === SVG && ! nextConfig . images . dangerouslyAllowSVG ) {
console . error (
` The requested resource " ${ href } " has type " ${ upstreamType } " but dangerouslyAllowSVG is disabled `
)
throw new ImageError (
400 ,
'"url" parameter is valid but image type is not allowed'
)
}
2022-02-09 00:46:59 +01:00
if ( upstreamType ) {
const vector = VECTOR_TYPES . includes ( upstreamType )
const animate =
ANIMATABLE_TYPES . includes ( upstreamType ) && isAnimated ( upstreamBuffer )
2020-10-24 03:26:52 +02:00
2022-02-09 00:46:59 +01:00
if ( vector || animate ) {
return { buffer : upstreamBuffer , contentType : upstreamType , maxAge }
2021-04-13 00:38:51 +02:00
}
2022-02-09 00:46:59 +01:00
if ( ! upstreamType . startsWith ( 'image/' ) ) {
console . error (
"The requested resource isn't a valid image for" ,
href ,
'received' ,
upstreamType
)
throw new ImageError ( 400 , "The requested resource isn't a valid image." )
}
}
2021-03-18 16:51:36 +01:00
2022-02-09 00:46:59 +01:00
let contentType : string
2021-07-23 01:11:17 +02:00
2022-02-09 00:46:59 +01:00
if ( mimeType ) {
contentType = mimeType
2022-03-10 15:02:13 +01:00
} else if (
upstreamType ? . startsWith ( 'image/' ) &&
getExtension ( upstreamType ) &&
upstreamType !== WEBP &&
upstreamType !== AVIF
) {
2022-02-09 00:46:59 +01:00
contentType = upstreamType
} else {
contentType = JPEG
}
try {
let optimizedBuffer : Buffer | undefined
if ( sharp ) {
// Begin sharp transformation logic
const transformer = sharp ( upstreamBuffer )
2021-07-23 01:11:17 +02:00
2022-02-09 00:46:59 +01:00
transformer . rotate ( )
2021-07-23 01:11:17 +02:00
2022-02-09 00:46:59 +01:00
const { width : metaWidth } = await transformer . metadata ( )
2021-03-18 16:51:36 +01:00
2022-02-09 00:46:59 +01:00
if ( metaWidth && metaWidth > width ) {
transformer . resize ( width )
}
if ( contentType === AVIF ) {
if ( transformer . avif ) {
const avifQuality = quality - 15
transformer . avif ( {
quality : Math.max ( avifQuality , 0 ) ,
chromaSubsampling : '4:2:0' , // same as webp
} )
} else {
2021-07-23 01:11:17 +02:00
console . warn (
chalk . yellow . bold ( 'Warning: ' ) +
2022-02-09 00:46:59 +01:00
` Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version. \ n ` +
'Read more: https://nextjs.org/docs/messages/sharp-version-avif'
2021-07-23 01:11:17 +02:00
)
2022-02-09 00:46:59 +01:00
transformer . webp ( { quality } )
2021-07-23 01:11:17 +02:00
}
2022-02-09 00:46:59 +01:00
} else if ( contentType === WEBP ) {
transformer . webp ( { quality } )
} else if ( contentType === PNG ) {
transformer . png ( { quality } )
} else if ( contentType === JPEG ) {
transformer . jpeg ( { quality } )
}
2020-10-22 22:39:24 +02:00
2022-02-09 00:46:59 +01:00
optimizedBuffer = await transformer . toBuffer ( )
// End sharp transformation logic
} else {
2022-06-24 21:58:35 +02:00
if ( showSharpMissingWarning && nextConfig . output === 'standalone' ) {
2022-02-09 00:46:59 +01:00
// TODO: should we ensure squoosh also works even though we don't
// recommend it be used in production and this is a production feature
console . error (
` Error: 'sharp' is required to be installed in standalone mode for the image optimization to function correctly `
)
throw new ImageError ( 500 , 'internal server error' )
}
// Show sharp warning in production once
if ( showSharpMissingWarning ) {
console . warn (
chalk . yellow . bold ( 'Warning: ' ) +
` For production Image Optimization with Next.js, the optional 'sharp' package is strongly recommended. Run 'yarn add sharp', and Next.js will use it automatically for Image Optimization. \ n ` +
'Read more: https://nextjs.org/docs/messages/sharp-missing-in-production'
)
showSharpMissingWarning = false
}
2021-04-13 00:38:51 +02:00
2022-02-09 00:46:59 +01:00
// Begin Squoosh transformation logic
const orientation = await getOrientation ( upstreamBuffer )
2021-07-23 01:11:17 +02:00
2022-02-09 00:46:59 +01:00
const operations : Operation [ ] = [ ]
2021-07-23 01:11:17 +02:00
2022-02-09 00:46:59 +01:00
if ( orientation === Orientation . RIGHT_TOP ) {
operations . push ( { type : 'rotate' , numRotations : 1 } )
} else if ( orientation === Orientation . BOTTOM_RIGHT ) {
operations . push ( { type : 'rotate' , numRotations : 2 } )
} else if ( orientation === Orientation . LEFT_BOTTOM ) {
operations . push ( { type : 'rotate' , numRotations : 3 } )
} else {
// TODO: support more orientations
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// const _: never = orientation
}
2021-07-23 01:11:17 +02:00
2022-02-09 00:46:59 +01:00
operations . push ( { type : 'resize' , width } )
2020-10-22 22:39:24 +02:00
2022-02-09 00:46:59 +01:00
if ( contentType === AVIF ) {
optimizedBuffer = await processBuffer (
upstreamBuffer ,
operations ,
'avif' ,
quality
2021-06-30 23:26:20 +02:00
)
2022-02-09 00:46:59 +01:00
} else if ( contentType === WEBP ) {
optimizedBuffer = await processBuffer (
upstreamBuffer ,
operations ,
'webp' ,
quality
)
} else if ( contentType === PNG ) {
optimizedBuffer = await processBuffer (
upstreamBuffer ,
operations ,
'png' ,
quality
)
} else if ( contentType === JPEG ) {
optimizedBuffer = await processBuffer (
upstreamBuffer ,
operations ,
'jpeg' ,
quality
2021-06-30 23:26:20 +02:00
)
2021-04-13 00:38:51 +02:00
}
2020-10-16 13:10:01 +02:00
2022-02-09 00:46:59 +01:00
// End Squoosh transformation logic
}
if ( optimizedBuffer ) {
2022-08-30 00:19:39 +02:00
if ( isDev && width <= BLUR_IMG_SIZE && quality === BLUR_QUALITY ) {
// During `next dev`, we don't want to generate blur placeholders with webpack
// because it can delay starting the dev server. Instead, `next-image-loader.js`
// will inline a special url to lazily generate the blur placeholder at request time.
const meta = await getMetadata ( optimizedBuffer )
const opts = {
blurWidth : meta.width ,
blurHeight : meta.height ,
blurDataURL : ` data: ${ contentType } ;base64, ${ optimizedBuffer . toString (
'base64'
) } ` ,
}
optimizedBuffer = Buffer . from ( unescape ( getImageBlurSvg ( opts ) ) )
contentType = 'image/svg+xml'
}
2022-02-09 00:46:59 +01:00
return {
buffer : optimizedBuffer ,
contentType ,
maxAge : Math.max ( maxAge , nextConfig . images . minimumCacheTTL ) ,
}
} else {
throw new ImageError ( 500 , 'Unable to optimize buffer' )
}
} catch ( error ) {
2022-03-01 17:38:51 +01:00
if ( upstreamBuffer && upstreamType ) {
// If we fail to optimize, fallback to the original image
return {
buffer : upstreamBuffer ,
contentType : upstreamType ,
maxAge : nextConfig.images.minimumCacheTTL ,
}
} else {
throw new ImageError (
2022-08-05 04:36:21 +02:00
400 ,
2022-03-01 17:38:51 +01:00
'Unable to optimize image and unable to fallback to upstream image'
)
2022-02-09 00:46:59 +01:00
}
2021-04-13 00:38:51 +02:00
}
2020-10-16 13:10:01 +02:00
}
2021-07-28 01:22:48 +02:00
function getFileNameWithExtension (
url : string ,
contentType : string | null
) : string | void {
const [ urlWithoutQueryParams ] = url . split ( '?' )
const fileNameWithExtension = urlWithoutQueryParams . split ( '/' ) . pop ( )
if ( ! contentType || ! fileNameWithExtension ) {
return
}
const [ fileName ] = fileNameWithExtension . split ( '.' )
const extension = getExtension ( contentType )
return ` ${ fileName } . ${ extension } `
}
2022-02-16 20:28:22 +01:00
function setResponseHeaders (
2020-11-10 05:40:26 +01:00
req : IncomingMessage ,
res : ServerResponse ,
2021-07-28 01:22:48 +02:00
url : string ,
2021-06-30 23:26:20 +02:00
etag : string ,
2020-11-10 05:40:26 +01:00
contentType : string | null ,
2021-06-30 23:26:20 +02:00
isStatic : boolean ,
2022-02-16 20:28:22 +01:00
xCache : XCacheHeader ,
2022-06-14 00:13:55 +02:00
contentSecurityPolicy : string ,
maxAge : number ,
isDev : boolean
2020-11-10 05:40:26 +01:00
) {
2021-07-01 21:59:16 +02:00
res . setHeader ( 'Vary' , 'Accept' )
2021-06-04 10:06:00 +02:00
res . setHeader (
'Cache-Control' ,
isStatic
2021-06-09 00:05:02 +02:00
? 'public, max-age=315360000, immutable'
2022-06-14 00:13:55 +02:00
: ` public, max-age= ${ isDev ? 0 : maxAge } , must-revalidate `
2021-06-04 10:06:00 +02:00
)
2020-11-10 05:40:26 +01:00
if ( sendEtagResponse ( req , res , etag ) ) {
2021-06-30 23:26:20 +02:00
// already called res.end() so we're finished
return { finished : true }
2020-11-10 05:40:26 +01:00
}
if ( contentType ) {
res . setHeader ( 'Content-Type' , contentType )
}
2021-07-28 01:22:48 +02:00
const fileName = getFileNameWithExtension ( url , contentType )
if ( fileName ) {
2021-11-02 00:23:24 +01:00
res . setHeader (
'Content-Disposition' ,
contentDisposition ( fileName , { type : 'inline' } )
)
2021-07-28 01:22:48 +02:00
}
2022-02-16 20:28:22 +01:00
if ( contentSecurityPolicy ) {
res . setHeader ( 'Content-Security-Policy' , contentSecurityPolicy )
}
2022-01-27 22:33:23 +01:00
res . setHeader ( 'X-Nextjs-Cache' , xCache )
2021-08-30 18:51:47 +02:00
2021-06-30 23:26:20 +02:00
return { finished : false }
}
2022-02-09 00:46:59 +01:00
export function sendResponse (
2021-06-30 23:26:20 +02:00
req : IncomingMessage ,
res : ServerResponse ,
2021-07-28 01:22:48 +02:00
url : string ,
2022-02-09 00:46:59 +01:00
extension : string ,
2021-06-30 23:26:20 +02:00
buffer : Buffer ,
isStatic : boolean ,
2022-02-16 20:28:22 +01:00
xCache : XCacheHeader ,
2022-06-14 00:13:55 +02:00
contentSecurityPolicy : string ,
maxAge : number ,
isDev : boolean
2021-06-30 23:26:20 +02:00
) {
2022-02-09 00:46:59 +01:00
const contentType = getContentType ( extension )
2021-06-30 23:26:20 +02:00
const etag = getHash ( [ buffer ] )
const result = setResponseHeaders (
req ,
res ,
2021-07-28 01:22:48 +02:00
url ,
2021-06-30 23:26:20 +02:00
etag ,
contentType ,
isStatic ,
2022-02-16 20:28:22 +01:00
xCache ,
2022-06-14 00:13:55 +02:00
contentSecurityPolicy ,
maxAge ,
isDev
2021-06-30 23:26:20 +02:00
)
if ( ! result . finished ) {
2022-04-30 03:50:42 +02:00
res . setHeader ( 'Content-Length' , Buffer . byteLength ( buffer ) )
2021-06-30 23:26:20 +02:00
res . end ( buffer )
}
2020-11-10 05:40:26 +01:00
}
2021-07-23 01:11:17 +02:00
export async function resizeImage (
content : Buffer ,
2022-08-24 01:56:52 +02:00
width : number ,
height : number ,
2021-10-12 01:17:47 +02:00
// Should match VALID_BLUR_EXT
extension : 'avif' | 'webp' | 'png' | 'jpeg' ,
2021-07-23 01:11:17 +02:00
quality : number
) : Promise < Buffer > {
2022-08-05 23:28:17 +02:00
if ( isAnimated ( content ) ) {
return content
} else if ( sharp ) {
2021-07-23 01:11:17 +02:00
const transformer = sharp ( content )
2021-10-12 01:17:47 +02:00
if ( extension === 'avif' ) {
if ( transformer . avif ) {
transformer . avif ( { quality } )
} else {
console . warn (
chalk . yellow . bold ( 'Warning: ' ) +
` Your installed version of the 'sharp' package does not support AVIF images. Run 'yarn add sharp@latest' to upgrade to the latest version. \ n ` +
'Read more: https://nextjs.org/docs/messages/sharp-version-avif'
)
transformer . webp ( { quality } )
}
} else if ( extension === 'webp' ) {
2021-07-23 01:11:17 +02:00
transformer . webp ( { quality } )
} else if ( extension === 'png' ) {
transformer . png ( { quality } )
} else if ( extension === 'jpeg' ) {
transformer . jpeg ( { quality } )
}
2022-08-24 01:56:52 +02:00
transformer . resize ( width , height )
2021-07-23 01:11:17 +02:00
const buf = await transformer . toBuffer ( )
return buf
} else {
2022-08-24 01:56:52 +02:00
const resizeOperationOpts : Operation = { type : 'resize' , width , height }
2021-07-23 01:11:17 +02:00
const buf = await processBuffer (
content ,
[ resizeOperationOpts ] ,
extension ,
quality
)
return buf
}
}
2021-10-12 01:17:47 +02:00
export async function getImageSize (
buffer : Buffer ,
// Should match VALID_BLUR_EXT
extension : 'avif' | 'webp' | 'png' | 'jpeg'
) : Promise < {
width? : number
height? : number
} > {
// TODO: upgrade "image-size" package to support AVIF
// See https://github.com/image-size/image-size/issues/348
if ( extension === 'avif' ) {
if ( sharp ) {
const transformer = sharp ( buffer )
const { width , height } = await transformer . metadata ( )
return { width , height }
} else {
const { width , height } = await decodeBuffer ( buffer )
return { width , height }
}
}
const { width , height } = imageSizeOf ( buffer )
return { width , height }
}
2022-02-02 22:27:56 +01:00
export class Deferred < T > {
promise : Promise < T >
resolve ! : ( value : T ) = > void
reject ! : ( error? : Error ) = > void
constructor ( ) {
this . promise = new Promise ( ( resolve , reject ) = > {
this . resolve = resolve
this . reject = reject
} )
}
}