/
next-image-loader.ts
124 lines (113 loc) · 3.85 KB
/
next-image-loader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import isAnimated from 'next/dist/compiled/is-animated'
import loaderUtils from 'next/dist/compiled/loader-utils3'
import { optimizeImage, getImageSize } from '../../../server/image-optimizer'
const BLUR_IMG_SIZE = 8
const BLUR_QUALITY = 70
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next/client/image.tsx
interface Options {
isServer: boolean
isDev: boolean
assetPrefix: string
basePath: string
}
function nextImageLoader(this: any, content: Buffer) {
const imageLoaderSpan = this.currentTraceSpan.traceChild('next-image-loader')
return imageLoaderSpan.traceAsyncFn(async () => {
const options: Options = this.getOptions()
const { isServer, isDev, assetPrefix, basePath } = options
const context = this.rootContext
const opts = { context, content }
const interpolatedName = loaderUtils.interpolateName(
this,
'/static/media/[name].[hash:8].[ext]',
opts
)
const outputPath = assetPrefix + '/_next' + interpolatedName
let extension = loaderUtils.interpolateName(this, '[ext]', opts)
if (extension === 'jpg') {
extension = 'jpeg'
}
const imageSizeSpan = imageLoaderSpan.traceChild('image-size-calculation')
const imageSize = await imageSizeSpan.traceAsyncFn(() =>
getImageSize(content, extension).catch((err) => err)
)
if (imageSize instanceof Error) {
const err = imageSize
err.name = 'InvalidImageFormatError'
throw err
}
let blurDataURL: string
let blurWidth: number
let blurHeight: number
if (VALID_BLUR_EXT.includes(extension)) {
// Shrink the image's largest dimension
if (imageSize.width >= imageSize.height) {
blurWidth = BLUR_IMG_SIZE
blurHeight = Math.max(
Math.round((imageSize.height / imageSize.width) * BLUR_IMG_SIZE),
1
)
} else {
blurWidth = Math.max(
Math.round((imageSize.width / imageSize.height) * BLUR_IMG_SIZE),
1
)
blurHeight = BLUR_IMG_SIZE
}
if (isDev) {
// During `next dev`, we don't want to generate blur placeholders with webpack
// because it can delay starting the dev server. Instead, we inline a
// special url to lazily generate the blur placeholder at request time.
const prefix = 'http://localhost'
const url = new URL(`${basePath || ''}/_next/image`, prefix)
url.searchParams.set('url', outputPath)
url.searchParams.set('w', String(blurWidth))
url.searchParams.set('q', String(BLUR_QUALITY))
blurDataURL = url.href.slice(prefix.length)
} else {
const resizeImageSpan = imageLoaderSpan.traceChild('image-resize')
const resizedImage = await resizeImageSpan.traceAsyncFn(() => {
if (isAnimated(content)) {
return content
}
return optimizeImage({
buffer: content,
width: blurWidth,
height: blurHeight,
contentType: `image/${extension}`,
quality: BLUR_QUALITY,
})
})
const blurDataURLSpan = imageLoaderSpan.traceChild(
'image-base64-tostring'
)
blurDataURL = blurDataURLSpan.traceFn(
() =>
`data:image/${extension};base64,${resizedImage.toString('base64')}`
)
}
}
const stringifiedData = imageLoaderSpan
.traceChild('image-data-stringify')
.traceFn(() =>
JSON.stringify({
src: outputPath,
height: imageSize.height,
width: imageSize.width,
blurDataURL,
blurWidth,
blurHeight,
})
)
if (isServer) {
this.emitFile(
`../${isDev ? '' : '../'}${interpolatedName}`,
content,
null
)
}
return `export default ${stringifiedData};`
})
}
export const raw = true
export default nextImageLoader