Skip to content

Commit f2821b3

Browse files
committed
feat(next/image): add support for images.qualities in next.config (#74257)
This PR adds support for `images.qualities` configuration which is an allowlist of qualities that can be used with the quality prop on the Image component. - Depends on vercel/vercel#12792
1 parent 7b195a8 commit f2821b3

File tree

24 files changed

+395
-9
lines changed

24 files changed

+395
-9
lines changed

docs/01-app/03-api-reference/02-components/image.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ quality={75} // {number 1-100}
257257

258258
The quality of the optimized image, an integer between `1` and `100`, where `100` is the best quality and therefore largest file size. Defaults to `75`.
259259

260+
If the [`qualities`](#qualities) configuration is defined in `next.config.js`, the `quality` prop must match one of the values defined in the configuration.
261+
262+
> **Good to know**: If the original source image was already low quality, setting the quality prop too high could cause the resulting optimized image to be larger than the original source image.
263+
260264
### `priority`
261265

262266
```js
@@ -681,6 +685,20 @@ module.exports = {
681685
}
682686
```
683687

688+
### `qualities`
689+
690+
The default [Image Optimization API](#loader) will automatically allow all qualities from 1 to 100. If you wish to restrict the allowed qualities, you can add configuration to `next.config.js`.
691+
692+
```js filename="next.config.js"
693+
module.exports = {
694+
images: {
695+
qualities: [25, 50, 75],
696+
},
697+
}
698+
```
699+
700+
In this example above, only three qualities are allowed: 25, 50, and 75. If the [`quality`](#quality) prop does not match a value in this array, the image will fail with 400 Bad Request.
701+
684702
### `formats`
685703

686704
The default [Image Optimization API](#loader) will automatically detect the browser's supported image formats via the request's `Accept` header in order to determine the best output format.
@@ -1076,6 +1094,7 @@ This `next/image` component uses browser native [lazy loading](https://caniuse.c
10761094
| Version | Changes |
10771095
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10781096
| `v15.0.0` | `contentDispositionType` configuration default changed to `attachment`. |
1097+
| `v14.2.23` | `qualities` configuration added. |
10791098
| `v14.2.15` | `decoding` prop added and `localPatterns` configuration added. |
10801099
| `v14.2.14` | `remotePatterns.search` prop added. |
10811100
| `v14.2.0` | `overrideSrc` prop added. |

errors/invalid-images-config.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ module.exports = {
4141
localPatterns: [],
4242
// limit of 50 objects
4343
remotePatterns: [],
44+
// limit of 20 integers
45+
qualities: [25, 50, 75],
4446
// when true, every image will be unoptimized
4547
unoptimized: false,
4648
},
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
title: '`next/image` Un-configured qualities'
3+
---
4+
5+
## Why This Error Occurred
6+
7+
One of your pages that leverages the `next/image` component, passed a `quality` value that isn't defined in the `images.qualities` property in `next.config.js`.
8+
9+
## Possible Ways to Fix It
10+
11+
Add an entry to `images.qualities` array in `next.config.js` with the expected value. For example:
12+
13+
```js filename="next.config.js"
14+
module.exports = {
15+
images: {
16+
qualities: [25, 50, 75],
17+
},
18+
}
19+
```
20+
21+
## Useful Links
22+
23+
- [Image Optimization Documentation](/docs/pages/building-your-application/optimizing/images)
24+
- [Qualities Config Documentation](/docs/pages/api-reference/components/image#qualities)

packages/next/src/build/webpack/plugins/define-env-plugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,14 @@ function getImageConfig(
109109
'process.env.__NEXT_IMAGE_OPTS': {
110110
deviceSizes: config.images.deviceSizes,
111111
imageSizes: config.images.imageSizes,
112+
qualities: config.images.qualities,
112113
path: config.images.path,
113114
loader: config.images.loader,
114115
dangerouslyAllowSVG: config.images.dangerouslyAllowSVG,
115116
unoptimized: config?.images?.unoptimized,
116117
...(dev
117118
? {
118-
// pass domains in development to allow validating on the client
119+
// additional config in dev to allow validating on the client
119120
domains: config.images.domains,
120121
remotePatterns: config.images?.remotePatterns,
121122
localPatterns: config.images?.localPatterns,

packages/next/src/client/image-component.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,8 @@ export const Image = forwardRef<HTMLImageElement | null, ImageProps>(
371371
const c = configEnv || configContext || imageConfigDefault
372372
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
373373
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
374-
return { ...c, allSizes, deviceSizes }
374+
const qualities = c.qualities?.sort((a, b) => a - b)
375+
return { ...c, allSizes, deviceSizes, qualities }
375376
}, [configContext])
376377

377378
const { onLoad, onLoadingComplete } = props

packages/next/src/client/legacy/image.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function normalizeSrc(src: string): string {
2929
}
3030

3131
const supportsFloat = typeof ReactDOM.preload === 'function'
32-
32+
const DEFAULT_Q = 75
3333
const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
3434
const loadedImageURLs = new Set<string>()
3535
const allImgs = new Map<
@@ -190,8 +190,22 @@ function defaultLoader({
190190
}
191191
}
192192
}
193+
194+
if (quality && config.qualities && !config.qualities.includes(quality)) {
195+
throw new Error(
196+
`Invalid quality prop (${quality}) on \`next/image\` does not match \`images.qualities\` configured in your \`next.config.js\`\n` +
197+
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-qualities`
198+
)
199+
}
193200
}
194201

202+
const q =
203+
quality ||
204+
config.qualities?.reduce((prev, cur) =>
205+
Math.abs(cur - DEFAULT_Q) < Math.abs(prev - DEFAULT_Q) ? cur : prev
206+
) ||
207+
DEFAULT_Q
208+
195209
if (!config.dangerouslyAllowSVG && src.split('?', 1)[0].endsWith('.svg')) {
196210
// Special case to make svg serve as-is to avoid proxying
197211
// through the built-in Image Optimization API.
@@ -200,7 +214,7 @@ function defaultLoader({
200214

201215
return `${normalizePathTrailingSlash(config.path)}?url=${encodeURIComponent(
202216
src
203-
)}&w=${width}&q=${quality || 75}`
217+
)}&w=${width}&q=${q}`
204218
}
205219

206220
const loaders = new Map<
@@ -641,7 +655,8 @@ export default function Image({
641655
const c = configEnv || configContext || imageConfigDefault
642656
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
643657
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
644-
return { ...c, allSizes, deviceSizes }
658+
const qualities = c.qualities?.sort((a, b) => a - b)
659+
return { ...c, allSizes, deviceSizes, qualities }
645660
}, [configContext])
646661

647662
let rest: Partial<ImageProps> = all

packages/next/src/server/config-schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,11 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
539539
loaderFile: z.string().optional(),
540540
minimumCacheTTL: z.number().int().gte(0).optional(),
541541
path: z.string().optional(),
542+
qualities: z
543+
.array(z.number().int().gte(1).lte(100))
544+
.min(1)
545+
.max(20)
546+
.optional(),
542547
})
543548
.optional(),
544549
logging: z

packages/next/src/server/image-optimizer.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export class ImageOptimizerCache {
219219
} = imageData
220220
const remotePatterns = nextConfig.images?.remotePatterns || []
221221
const localPatterns = nextConfig.images?.localPatterns
222+
const qualities = nextConfig.images?.qualities
222223
const { url, w, q } = query
223224
let href: string
224225

@@ -334,6 +335,18 @@ export class ImageOptimizerCache {
334335
}
335336
}
336337

338+
if (qualities) {
339+
if (isDev) {
340+
qualities.push(BLUR_QUALITY)
341+
}
342+
343+
if (!qualities.includes(quality)) {
344+
return {
345+
errorMessage: `"q" parameter (quality) of ${q} is not allowed`,
346+
}
347+
}
348+
}
349+
337350
const mimeType = getSupportedMimeType(formats || [], req.headers['accept'])
338351

339352
const isStatic = url.startsWith(

packages/next/src/shared/lib/get-img-props.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,8 @@ export function getImgProps(
286286
} else {
287287
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
288288
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
289-
config = { ...c, allSizes, deviceSizes }
289+
const qualities = c.qualities?.sort((a, b) => a - b)
290+
config = { ...c, allSizes, deviceSizes, qualities }
290291
}
291292

292293
if (typeof defaultLoader === 'undefined') {

packages/next/src/shared/lib/image-config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ export type ImageConfigComplete = {
118118
/** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#localPatterns) */
119119
localPatterns: LocalPattern[] | undefined
120120

121+
/** @see [Qualities](https://nextjs.org/docs/api-reference/next/image#qualities) */
122+
qualities: number[] | undefined
123+
121124
/** @see [Unoptimized](https://nextjs.org/docs/api-reference/next/image#unoptimized) */
122125
unoptimized: boolean
123126
}
@@ -139,5 +142,6 @@ export const imageConfigDefault: ImageConfigComplete = {
139142
contentDispositionType: 'attachment',
140143
localPatterns: undefined, // default: allow all local images
141144
remotePatterns: [], // default: allow no remote images
145+
qualities: undefined, // default: allow all qualities
142146
unoptimized: false,
143147
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy