Skip to content

Commit

Permalink
feat(image): DataURL placeholder support for <Image /> (vercel#53442)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
arturbien and styfle committed Aug 11, 2023
1 parent 4c14482 commit e0ca2ba
Show file tree
Hide file tree
Showing 16 changed files with 418 additions and 67 deletions.
46 changes: 24 additions & 22 deletions docs/02-app/02-api-reference/01-components/image.mdx

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions examples/image-component/pages/shimmer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ const Shimmer = () => (
<Image
alt="Mountains"
src="/mountains.jpg"
placeholder="blur"
blurDataURL={`data:image/svg+xml;base64,${toBase64(shimmer(700, 475))}`}
placeholder={`data:image/svg+xml;base64,${toBase64(shimmer(700, 475))}`}
width={700}
height={475}
style={{
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/client/image-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function handleLoading(
// - decode() completes
return
}
if (placeholder === 'blur') {
if (placeholder !== 'empty') {
setBlurComplete(true)
}
if (onLoadRef?.current) {
Expand Down Expand Up @@ -291,7 +291,7 @@ const ImageElement = forwardRef<HTMLImageElement | null, ImageElementProps>(
onError={(event) => {
// if the real image fails to load, this will ensure "alt" is visible
setShowAltText(true)
if (placeholder === 'blur') {
if (placeholder !== 'empty') {
// If the real image fails to load, this will still remove the placeholder.
setBlurComplete(true)
}
Expand Down
97 changes: 56 additions & 41 deletions packages/next/src/shared/lib/get-img-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export type ImageLoader = (p: ImageLoaderProps) => string
// built-in loaders, not for a custom loader() prop.
type ImageLoaderWithConfig = (p: ImageLoaderPropsWithConfig) => string

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

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

const allImgs = new Map<
string,
{ src: string; priority: boolean; placeholder: string }
{ src: string; priority: boolean; placeholder: PlaceholderValue }
>()
let perfObserver: PerformanceObserver | undefined

Expand Down Expand Up @@ -468,28 +468,35 @@ export function getImgProps(
`Image with src "${src}" has both "priority" and "loading='lazy'" properties. Only one should be used.`
)
}

if (placeholder === 'blur') {
if (
placeholder !== 'empty' &&
placeholder !== 'blur' &&
!placeholder.startsWith('data:image/')
) {
throw new Error(
`Image with src "${src}" has invalid "placeholder" property "${placeholder}".`
)
}
if (placeholder !== 'empty') {
if (widthInt && heightInt && widthInt * heightInt < 1600) {
warnOnce(
`Image with src "${src}" is smaller than 40x40. Consider removing the "placeholder='blur'" property to improve performance.`
`Image with src "${src}" is smaller than 40x40. Consider removing the "placeholder" property to improve performance.`
)
}
}
if (placeholder === 'blur' && !blurDataURL) {
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader

if (!blurDataURL) {
const VALID_BLUR_EXT = ['jpeg', 'png', 'webp', 'avif'] // should match next-image-loader

throw new Error(
`Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property.
Possible solutions:
- Add a "blurDataURL" property, the contents should be a small Data URL to represent the image
- Change the "src" property to a static import with one of the supported file types: ${VALID_BLUR_EXT.join(
','
)}
- Remove the "placeholder" property, effectively no blur effect
Read more: https://nextjs.org/docs/messages/placeholder-blur-data-url`
)
}
throw new Error(
`Image with src "${src}" has "placeholder='blur'" property but is missing the "blurDataURL" property.
Possible solutions:
- Add a "blurDataURL" property, the contents should be a small Data URL to represent the image
- Change the "src" property to a static import with one of the supported file types: ${VALID_BLUR_EXT.join(
','
)}
- Remove the "placeholder" property, effectively no blur effect
Read more: https://nextjs.org/docs/messages/placeholder-blur-data-url`
)
}
if ('ref' in rest) {
warnOnce(
Expand Down Expand Up @@ -544,7 +551,7 @@ export function getImgProps(
if (
lcpImage &&
!lcpImage.priority &&
lcpImage.placeholder !== 'blur' &&
lcpImage.placeholder === 'empty' &&
!lcpImage.src.startsWith('data:') &&
!lcpImage.src.startsWith('blob:')
) {
Expand Down Expand Up @@ -585,31 +592,39 @@ export function getImgProps(
style
)

const blurStyle =
placeholder === 'blur' && blurDataURL && !blurComplete
? {
backgroundSize: imgStyle.objectFit || 'cover',
backgroundPosition: imgStyle.objectPosition || '50% 50%',
backgroundRepeat: 'no-repeat',
backgroundImage: `url("data:image/svg+xml;charset=utf-8,${getImageBlurSvg(
{
widthInt,
heightInt,
blurWidth,
blurHeight,
blurDataURL,
objectFit: imgStyle.objectFit,
}
)}")`,
}
: {}
const backgroundImage =
!blurComplete && placeholder !== 'empty'
? placeholder === 'blur'
? `url("data:image/svg+xml;charset=utf-8,${getImageBlurSvg({
widthInt,
heightInt,
blurWidth,
blurHeight,
blurDataURL: blurDataURL || '', // assume not undefined
objectFit: imgStyle.objectFit,
})}")`
: `url("${placeholder}")` // assume `data:image/`
: null

let placeholderStyle = backgroundImage
? {
backgroundSize: imgStyle.objectFit || 'cover',
backgroundPosition: imgStyle.objectPosition || '50% 50%',
backgroundRepeat: 'no-repeat',
backgroundImage,
}
: {}

if (process.env.NODE_ENV === 'development') {
if (blurStyle.backgroundImage && blurDataURL?.startsWith('/')) {
if (
placeholderStyle.backgroundImage &&
placeholder === 'blur' &&
blurDataURL?.startsWith('/')
) {
// 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.
blurStyle.backgroundImage = `url("${blurDataURL}")`
placeholderStyle.backgroundImage = `url("${blurDataURL}")`
}
}

Expand Down Expand Up @@ -643,7 +658,7 @@ export function getImgProps(
height: heightInt,
decoding: 'async',
className,
style: { ...imgStyle, ...blurStyle },
style: { ...imgStyle, ...placeholderStyle },
sizes: imgAttributes.sizes,
srcSet: imgAttributes.srcSet,
src: imgAttributes.src,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Image from 'next/image'

const shimmer = `data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==`

export default function Page() {
return (
<div>
<p>Data URL Placeholder</p>

<Image
priority
id="data-url-placeholder-raw"
src="/test.ico"
width="400"
height="400"
placeholder={shimmer}
alt=""
/>

<div id="spacer" style={{ height: '1000vh' }} />

<Image
id="data-url-placeholder-with-lazy"
src="/test.bmp"
width="400"
height="400"
placeholder={shimmer}
alt=""
/>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Image from 'next/image'

// We don't use a static import intentionally
const shimmer = `data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==`

export default function Page() {
return (
<>
<p>Image with fill with Data URL placeholder</p>
<div style={{ position: 'relative', display: 'flex', minHeight: '30vh' }}>
<Image
fill
alt="alt"
src="/wide.png"
placeholder={shimmer}
id="data-url-placeholder-fit-cover"
style={{ objectFit: 'cover' }}
/>
</div>

<div style={{ position: 'relative', display: 'flex', minHeight: '30vh' }}>
<Image
fill
alt="alt"
src="/wide.png"
placeholder={shimmer}
id="data-url-placeholder-fit-contain"
style={{ objectFit: 'contain' }}
/>
</div>

<div style={{ position: 'relative', display: 'flex', minHeight: '30vh' }}>
<Image
fill
alt="alt"
src="/wide.png"
placeholder={shimmer}
id="data-url-placeholder-fit-fill"
style={{ objectFit: 'fill' }}
/>
</div>
</>
)
}
11 changes: 11 additions & 0 deletions test/integration/next-image-new/app-dir/app/static-img/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import TallImage from '../../components/TallImage'
const blurDataURL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNM/s/wBwAFjwJgf8HDLgAAAABJRU5ErkJggg=='

const shimmer = `data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==`

const Page = () => {
return (
<div>
Expand Down Expand Up @@ -97,6 +99,15 @@ const Page = () => {
/>
<br />
<Image id="static-unoptimized" src={testJPG} unoptimized />
<br />
<Image
id="data-url-placeholder"
src={testImg}
placeholder={shimmer}
width="200"
height="200"
alt=""
/>
</div>
)
}
Expand Down
66 changes: 66 additions & 0 deletions test/integration/next-image-new/app-dir/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1350,6 +1350,72 @@ function runTests(mode) {
})
}

it('should have data url placeholder when enabled', async () => {
const html = await renderViaHTTP(appPort, '/data-url-placeholder')
const $html = cheerio.load(html)

$html('noscript > img').attr('id', 'unused')

expect($html('#data-url-placeholder-raw')[0].attribs.style).toContain(
`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==")`
)

expect($html('#data-url-placeholder-with-lazy')[0].attribs.style).toContain(
`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==")`
)
})

it('should remove data url placeholder after image loads', async () => {
const browser = await webdriver(appPort, '/data-url-placeholder')
await check(
async () =>
await getComputedStyle(
browser,
'data-url-placeholder-raw',
'background-image'
),
'none'
)
expect(
await getComputedStyle(
browser,
'data-url-placeholder-with-lazy',
'background-image'
)
).toBe(
`url("data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImciPgogICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjMzMzIiBvZmZzZXQ9IjIwJSIgLz4KICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgb2Zmc2V0PSI1MCUiIC8+CiAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMzMzMiIG9mZnNldD0iNzAlIiAvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiMzMzMiIC8+CiAgPHJlY3QgaWQ9InIiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSJ1cmwoI2cpIiAvPgogIDxhbmltYXRlIHhsaW5rOmhyZWY9IiNyIiBhdHRyaWJ1dGVOYW1lPSJ4IiBmcm9tPSItMjAwIiB0bz0iMjAwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgIC8+Cjwvc3ZnPg==")`
)

await browser.eval('document.getElementById("spacer").remove()')

await check(
async () =>
await getComputedStyle(
browser,
'data-url-placeholder-with-lazy',
'background-image'
),
'none'
)
})

it('should render correct objectFit when data url placeholder and fill', async () => {
const html = await renderViaHTTP(appPort, '/fill-data-url-placeholder')
const $ = cheerio.load(html)

expect($('#data-url-placeholder-fit-cover')[0].attribs.style).toBe(
`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==")`
)

expect($('#data-url-placeholder-fit-contain')[0].attribs.style).toBe(
`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==")`
)

expect($('#data-url-placeholder-fit-fill')[0].attribs.style).toBe(
`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==")`
)
})

it('should have blurry placeholder when enabled', async () => {
const html = await renderViaHTTP(appPort, '/blurry-placeholder')
const $html = cheerio.load(html)
Expand Down
7 changes: 7 additions & 0 deletions test/integration/next-image-new/app-dir/test/static.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ const runTests = (isDev) => {
expect(img.attr('height')).toBe('233')
})

it('should add a data URL placeholder to an image', async () => {
const style = $('#data-url-placeholder').attr('style')
expect(style).toBe(
`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==")`
)
})

it('should add a blur placeholder a statically imported jpg', async () => {
const style = $('#basic-static').attr('style')
if (isDev) {
Expand Down
Loading

0 comments on commit e0ca2ba

Please sign in to comment.