Skip to content

Commit e0ca2ba

Browse files
arturbienstyfle
andauthored
feat(image): DataURL placeholder support for <Image /> (vercel#53442)
Adds support for base64-encoded `placeholder`. Enables using placeholders without the "blur" effect. Fixes vercel#47639 - [x] Add support for DataURL placeholder - [x] Add tests - [x] Update docs Co-authored-by: Steven <229881+styfle@users.noreply.github.com>
1 parent 4c14482 commit e0ca2ba

File tree

16 files changed

+418
-67
lines changed

16 files changed

+418
-67
lines changed

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

Lines changed: 24 additions & 22 deletions
Large diffs are not rendered by default.

examples/image-component/pages/shimmer.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ const Shimmer = () => (
2727
<Image
2828
alt="Mountains"
2929
src="/mountains.jpg"
30-
placeholder="blur"
31-
blurDataURL={`data:image/svg+xml;base64,${toBase64(shimmer(700, 475))}`}
30+
placeholder={`data:image/svg+xml;base64,${toBase64(shimmer(700, 475))}`}
3231
width={700}
3332
height={475}
3433
style={{

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ function handleLoading(
8080
// - decode() completes
8181
return
8282
}
83-
if (placeholder === 'blur') {
83+
if (placeholder !== 'empty') {
8484
setBlurComplete(true)
8585
}
8686
if (onLoadRef?.current) {
@@ -291,7 +291,7 @@ const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
291291
onError={(event) => {
292292
// if the real image fails to load, this will ensure "alt" is visible
293293
setShowAltText(true)
294-
if (placeholder === 'blur') {
294+
if (placeholder !== 'empty') {
295295
// If the real image fails to load, this will still remove the placeholder.
296296
setBlurComplete(true)
297297
}

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

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export type ImageLoader = (p: ImageLoaderProps) => string
8686
// built-in loaders, not for a custom loader() prop.
8787
type ImageLoaderWithConfig = (p: ImageLoaderPropsWithConfig) => string
8888

89-
export type PlaceholderValue = 'blur' | 'empty'
89+
export type PlaceholderValue = 'blur' | 'empty' | `data:image/${string}`
9090
export type OnLoad = React.ReactEventHandler<HTMLImageElement> | undefined
9191
export type OnLoadingComplete = (img: HTMLImageElement) => void
9292

@@ -112,7 +112,7 @@ function isStaticImport(src: string | StaticImport): src is StaticImport {
112112

113113
const allImgs = new Map<
114114
string,
115-
{ src: string; priority: boolean; placeholder: string }
115+
{ src: string; priority: boolean; placeholder: PlaceholderValue }
116116
>()
117117
let perfObserver: PerformanceObserver | undefined
118118

@@ -468,28 +468,35 @@ export function getImgProps(
468468
`Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.`
469469
)
470470
}
471-
472-
if (placeholder === 'blur') {
471+
if (
472+
placeholder !== 'empty' &&
473+
placeholder !== 'blur' &&
474+
!placeholder.startsWith('data:image/')
475+
) {
476+
throw new Error(
477+
`Image with src "${src}" has invalid "placeholder" property "${placeholder}".`
478+
)
479+
}
480+
if (placeholder !== 'empty') {
473481
if (widthInt && heightInt && widthInt * heightInt < 1600) {
474482
warnOnce(
475-
`Image with src "${src}" is smaller than 40x40. Consider removing the "placeholder='blur'" property to improve performance.`
483+
`Image with src "${src}" is smaller than 40x40. Consider removing the "placeholder" property to improve performance.`
476484
)
477485
}
486+
}
487+
if (placeholder === 'blur' && !blurDataURL) {
488+
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader
478489

479-
if (!blurDataURL) {
480-
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader
481-
482-
throw new Error(
483-
`Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property.
484-
Possible solutions:
485-
- Add a "blurDataURL" property, the contents should be a small Data URL to represent the image
486-
- Change the "src" property to a static import with one of the supported file types: ${VALID_BLUR_EXT.join(
487-
','
488-
)}
489-
- Remove the "placeholder" property, effectively no blur effect
490-
Read more: https://nextjs.org/docs/messages/placeholder-blur-data-url`
491-
)
492-
}
490+
throw new Error(
491+
`Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property.
492+
Possible solutions:
493+
- Add a "blurDataURL" property, the contents should be a small Data URL to represent the image
494+
- Change the "src" property to a static import with one of the supported file types: ${VALID_BLUR_EXT.join(
495+
','
496+
)}
497+
- Remove the "placeholder" property, effectively no blur effect
498+
Read more: https://nextjs.org/docs/messages/placeholder-blur-data-url`
499+
)
493500
}
494501
if ('ref' in rest) {
495502
warnOnce(
@@ -544,7 +551,7 @@ export function getImgProps(
544551
if (
545552
lcpImage &&
546553
!lcpImage.priority &&
547-
lcpImage.placeholder !== 'blur' &&
554+
lcpImage.placeholder === 'empty' &&
548555
!lcpImage.src.startsWith('data:') &&
549556
!lcpImage.src.startsWith('blob:')
550557
) {
@@ -585,31 +592,39 @@ export function getImgProps(
585592
style
586593
)
587594

588-
const blurStyle =
589-
placeholder === 'blur' && blurDataURL && !blurComplete
590-
? {
591-
backgroundSize: imgStyle.objectFit || 'cover',
592-
backgroundPosition: imgStyle.objectPosition || '50% 50%',
593-
backgroundRepeat: 'no-repeat',
594-
backgroundImage: `url("data:image/svg+xml;charset=utf-8,${getImageBlurSvg(
595-
{
596-
widthInt,
597-
heightInt,
598-
blurWidth,
599-
blurHeight,
600-
blurDataURL,
601-
objectFit: imgStyle.objectFit,
602-
}
603-
)}")`,
604-
}
605-
: {}
595+
const backgroundImage =
596+
!blurComplete && placeholder !== 'empty'
597+
? placeholder === 'blur'
598+
? `url("data:image/svg+xml;charset=utf-8,${getImageBlurSvg({
599+
widthInt,
600+
heightInt,
601+
blurWidth,
602+
blurHeight,
603+
blurDataURL: blurDataURL || '', // assume not undefined
604+
objectFit: imgStyle.objectFit,
605+
})}")`
606+
: `url("${placeholder}")` // assume `data:image/`
607+
: null
608+
609+
let placeholderStyle = backgroundImage
610+
? {
611+
backgroundSize: imgStyle.objectFit || 'cover',
612+
backgroundPosition: imgStyle.objectPosition || '50% 50%',
613+
backgroundRepeat: 'no-repeat',
614+
backgroundImage,
615+
}
616+
: {}
606617

607618
if (process.env.NODE_ENV === 'development') {
608-
if (blurStyle.backgroundImage && blurDataURL?.startsWith('/')) {
619+
if (
620+
placeholderStyle.backgroundImage &&
621+
placeholder === 'blur' &&
622+
blurDataURL?.startsWith('/')
623+
) {
609624
// During `next dev`, we don't want to generate blur placeholders with webpack
610625
// because it can delay starting the dev server. Instead, `next-image-loader.js`
611626
// will inline a special url to lazily generate the blur placeholder at request time.
612-
blurStyle.backgroundImage = `url("${blurDataURL}")`
627+
placeholderStyle.backgroundImage = `url("${blurDataURL}")`
613628
}
614629
}
615630

@@ -643,7 +658,7 @@ export function getImgProps(
643658
height: heightInt,
644659
decoding: 'async',
645660
className,
646-
style: { ...imgStyle, ...blurStyle },
661+
style: { ...imgStyle, ...placeholderStyle },
647662
sizes: imgAttributes.sizes,
648663
srcSet: imgAttributes.srcSet,
649664
src: imgAttributes.src,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Image from 'next/image'
2+
3+
const shimmer = `data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==`
4+
5+
export default function Page() {
6+
return (
7+
<div>
8+
<p>Data URL Placeholder</p>
9+
10+
<Image
11+
priority
12+
id="data-url-placeholder-raw"
13+
src="/test.ico"
14+
width="400"
15+
height="400"
16+
placeholder={shimmer}
17+
alt=""
18+
/>
19+
20+
<div id="spacer" style={{ height: '1000vh' }} />
21+
22+
<Image
23+
id="data-url-placeholder-with-lazy"
24+
src="/test.bmp"
25+
width="400"
26+
height="400"
27+
placeholder={shimmer}
28+
alt=""
29+
/>
30+
</div>
31+
)
32+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Image from 'next/image'
2+
3+
// We don't use a static import intentionally
4+
const shimmer = `data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==`
5+
6+
export default function Page() {
7+
return (
8+
<>
9+
<p>Image with fill with Data URL placeholder</p>
10+
<div style={{ position: 'relative', display: 'flex', minHeight: '30vh' }}>
11+
<Image
12+
fill
13+
alt="alt"
14+
src="/wide.png"
15+
placeholder={shimmer}
16+
id="data-url-placeholder-fit-cover"
17+
style={{ objectFit: 'cover' }}
18+
/>
19+
</div>
20+
21+
<div style={{ position: 'relative', display: 'flex', minHeight: '30vh' }}>
22+
<Image
23+
fill
24+
alt="alt"
25+
src="/wide.png"
26+
placeholder={shimmer}
27+
id="data-url-placeholder-fit-contain"
28+
style={{ objectFit: 'contain' }}
29+
/>
30+
</div>
31+
32+
<div style={{ position: 'relative', display: 'flex', minHeight: '30vh' }}>
33+
<Image
34+
fill
35+
alt="alt"
36+
src="/wide.png"
37+
placeholder={shimmer}
38+
id="data-url-placeholder-fit-fill"
39+
style={{ objectFit: 'fill' }}
40+
/>
41+
</div>
42+
</>
43+
)
44+
}

test/integration/next-image-new/app-dir/app/static-img/page.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import TallImage from '../../components/TallImage'
1919
const blurDataURL =
2020
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNM/s/wBwAFjwJgf8HDLgAAAABJRU5ErkJggg=='
2121

22+
const shimmer = `data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==`
23+
2224
const Page = () => {
2325
return (
2426
<div>
@@ -97,6 +99,15 @@ const Page = () => {
9799
/>
98100
<br />
99101
<Image id="static-unoptimized" src={testJPG} unoptimized />
102+
<br />
103+
<Image
104+
id="data-url-placeholder"
105+
src={testImg}
106+
placeholder={shimmer}
107+
width="200"
108+
height="200"
109+
alt=""
110+
/>
100111
</div>
101112
)
102113
}

test/integration/next-image-new/app-dir/test/index.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,72 @@ function runTests(mode) {
13501350
})
13511351
}
13521352

1353+
it('should have data url placeholder when enabled', async () => {
1354+
const html = await renderViaHTTP(appPort, '/data-url-placeholder')
1355+
const $html = cheerio.load(html)
1356+
1357+
$html('noscript > img').attr('id', 'unused')
1358+
1359+
expect($html('#data-url-placeholder-raw')[0].attribs.style).toContain(
1360+
`color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==")`
1361+
)
1362+
1363+
expect($html('#data-url-placeholder-with-lazy')[0].attribs.style).toContain(
1364+
`color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==")`
1365+
)
1366+
})
1367+
1368+
it('should remove data url placeholder after image loads', async () => {
1369+
const browser = await webdriver(appPort, '/data-url-placeholder')
1370+
await check(
1371+
async () =>
1372+
await getComputedStyle(
1373+
browser,
1374+
'data-url-placeholder-raw',
1375+
'background-image'
1376+
),
1377+
'none'
1378+
)
1379+
expect(
1380+
await getComputedStyle(
1381+
browser,
1382+
'data-url-placeholder-with-lazy',
1383+
'background-image'
1384+
)
1385+
).toBe(
1386+
`url("data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==")`
1387+
)
1388+
1389+
await browser.eval('document.getElementById("spacer").remove()')
1390+
1391+
await check(
1392+
async () =>
1393+
await getComputedStyle(
1394+
browser,
1395+
'data-url-placeholder-with-lazy',
1396+
'background-image'
1397+
),
1398+
'none'
1399+
)
1400+
})
1401+
1402+
it('should render correct objectFit when data url placeholder and fill', async () => {
1403+
const html = await renderViaHTTP(appPort, '/fill-data-url-placeholder')
1404+
const $ = cheerio.load(html)
1405+
1406+
expect($('#data-url-placeholder-fit-cover')[0].attribs.style).toBe(
1407+
`position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:cover;color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==")`
1408+
)
1409+
1410+
expect($('#data-url-placeholder-fit-contain')[0].attribs.style).toBe(
1411+
`position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:contain;color:transparent;background-size:contain;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==")`
1412+
)
1413+
1414+
expect($('#data-url-placeholder-fit-fill')[0].attribs.style).toBe(
1415+
`position:absolute;height:100%;width:100%;left:0;top:0;right:0;bottom:0;object-fit:fill;color:transparent;background-size:fill;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==")`
1416+
)
1417+
})
1418+
13531419
it('should have blurry placeholder when enabled', async () => {
13541420
const html = await renderViaHTTP(appPort, '/blurry-placeholder')
13551421
const $html = cheerio.load(html)

test/integration/next-image-new/app-dir/test/static.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ const runTests = (isDev) => {
114114
expect(img.attr('height')).toBe('233')
115115
})
116116

117+
it('should add a data URL placeholder to an image', async () => {
118+
const style = $('#data-url-placeholder').attr('style')
119+
expect(style).toBe(
120+
`color:transparent;background-size:cover;background-position:50% 50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==")`
121+
)
122+
})
123+
117124
it('should add a blur placeholder a statically imported jpg', async () => {
118125
const style = $('#basic-static').attr('style')
119126
if (isDev) {

0 commit comments

Comments
 (0)