Skip to content

Commit a85f441

Browse files
authored
feat(next/image): add support for images.qualities in next.config (#74500)
Backports PR #74257 to 14.x
1 parent adfe537 commit a85f441

File tree

24 files changed

+395
-9
lines changed

24 files changed

+395
-9
lines changed

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

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

248248
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`.
249249

250+
If the [`qualities`](#qualities) configuration is defined in `next.config.js`, the `quality` prop must match one of the values defined in the configuration.
251+
252+
> **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.
253+
250254
### `priority`
251255

252256
```js
@@ -672,6 +676,20 @@ module.exports = {
672676
}
673677
```
674678

679+
### `qualities`
680+
681+
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`.
682+
683+
```js filename="next.config.js"
684+
module.exports = {
685+
images: {
686+
qualities: [25, 50, 75],
687+
},
688+
}
689+
```
690+
691+
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.
692+
675693
### `formats`
676694

677695
The default [Image Optimization API](#loader) will automatically detect the browser's supported image formats via the request's `Accept` header.
@@ -1050,6 +1068,7 @@ This `next/image` component uses browser native [lazy loading](https://caniuse.c
10501068

10511069
| Version | Changes |
10521070
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1071+
| `v14.2.23` | `qualities` configuration added. |
10531072
| `v14.2.15` | `decoding` prop added and `localPatterns` configuration added. |
10541073
| `v14.2.14` | `remotePatterns.search` prop added. |
10551074
| `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
@@ -108,13 +108,14 @@ function getImageConfig(
108108
'process.env.__NEXT_IMAGE_OPTS': {
109109
deviceSizes: config.images.deviceSizes,
110110
imageSizes: config.images.imageSizes,
111+
qualities: config.images.qualities,
111112
path: config.images.path,
112113
loader: config.images.loader,
113114
dangerouslyAllowSVG: config.images.dangerouslyAllowSVG,
114115
unoptimized: config?.images?.unoptimized,
115116
...(dev
116117
? {
117-
// pass domains in development to allow validating on the client
118+
// additional config in dev to allow validating on the client
118119
domains: config.images.domains,
119120
remotePatterns: config.images?.remotePatterns,
120121
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
@@ -374,7 +374,8 @@ export const Image = forwardRef<HTMLImageElement | null, ImageProps>(
374374
const c = configEnv || configContext || imageConfigDefault
375375
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
376376
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
377-
return { ...c, allSizes, deviceSizes }
377+
const qualities = c.qualities?.sort((a, b) => a - b)
378+
return { ...c, allSizes, deviceSizes, qualities }
378379
}, [configContext])
379380

380381
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
@@ -25,7 +25,7 @@ import { normalizePathTrailingSlash } from '../normalize-trailing-slash'
2525
function normalizeSrc(src: string): string {
2626
return src[0] === '/' ? src.slice(1) : src
2727
}
28-
28+
const DEFAULT_Q = 75
2929
const configEnv = process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete
3030
const loadedImageURLs = new Set<string>()
3131
const allImgs = new Map<
@@ -186,8 +186,22 @@ function defaultLoader({
186186
}
187187
}
188188
}
189+
190+
if (quality && config.qualities && !config.qualities.includes(quality)) {
191+
throw new Error(
192+
`Invalid quality prop (${quality}) on \`next/image\` does not match \`images.qualities\` configured in your \`next.config.js\`\n` +
193+
`See more info: https://nextjs.org/docs/messages/next-image-unconfigured-qualities`
194+
)
195+
}
189196
}
190197

198+
const q =
199+
quality ||
200+
config.qualities?.reduce((prev, cur) =>
201+
Math.abs(cur - DEFAULT_Q) < Math.abs(prev - DEFAULT_Q) ? cur : prev
202+
) ||
203+
DEFAULT_Q
204+
191205
if (src.endsWith('.svg') && !config.dangerouslyAllowSVG) {
192206
// Special case to make svg serve as-is to avoid proxying
193207
// through the built-in Image Optimization API.
@@ -196,7 +210,7 @@ function defaultLoader({
196210

197211
return `${normalizePathTrailingSlash(config.path)}?url=${encodeURIComponent(
198212
src
199-
)}&w=${width}&q=${quality || 75}`
213+
)}&w=${width}&q=${q}`
200214
}
201215

202216
const loaders = new Map<
@@ -637,7 +651,8 @@ export default function Image({
637651
const c = configEnv || configContext || imageConfigDefault
638652
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
639653
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
640-
return { ...c, allSizes, deviceSizes }
654+
const qualities = c.qualities?.sort((a, b) => a - b)
655+
return { ...c, allSizes, deviceSizes, qualities }
641656
}, [configContext])
642657

643658
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
@@ -505,6 +505,11 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
505505
loaderFile: z.string().optional(),
506506
minimumCacheTTL: z.number().int().gte(0).optional(),
507507
path: z.string().optional(),
508+
qualities: z
509+
.array(z.number().int().gte(1).lte(100))
510+
.min(1)
511+
.max(20)
512+
.optional(),
508513
})
509514
.optional(),
510515
logging: z

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export class ImageOptimizerCache {
175175
} = imageData
176176
const remotePatterns = nextConfig.images?.remotePatterns || []
177177
const localPatterns = nextConfig.images?.localPatterns
178+
const qualities = nextConfig.images?.qualities
178179
const { url, w, q } = query
179180
let href: string
180181

@@ -281,6 +282,18 @@ export class ImageOptimizerCache {
281282
}
282283
}
283284

285+
if (qualities) {
286+
if (isDev) {
287+
qualities.push(BLUR_QUALITY)
288+
}
289+
290+
if (!qualities.includes(quality)) {
291+
return {
292+
errorMessage: `"q" parameter (quality) of ${q} is not allowed`,
293+
}
294+
}
295+
}
296+
284297
const mimeType = getSupportedMimeType(formats || [], req.headers['accept'])
285298

286299
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
@@ -283,7 +283,8 @@ export function getImgProps(
283283
} else {
284284
const allSizes = [...c.deviceSizes, ...c.imageSizes].sort((a, b) => a - b)
285285
const deviceSizes = c.deviceSizes.sort((a, b) => a - b)
286-
config = { ...c, allSizes, deviceSizes }
286+
const qualities = c.qualities?.sort((a, b) => a - b)
287+
config = { ...c, allSizes, deviceSizes, qualities }
287288
}
288289

289290
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: 'inline',
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