Skip to content

Commit

Permalink
Basic Thumbhash support for Solid component
Browse files Browse the repository at this point in the history
  • Loading branch information
simonihmig committed Dec 29, 2024
1 parent be8fad7 commit 0429d13
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 2 deletions.
34 changes: 33 additions & 1 deletion packages/solid/src/responsive-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getDestinationWidthBySize,
env,
isLqipBlurhash,
isLqipThumbhash,
} from '@responsive-image/core';
import {
Component,
Expand Down Expand Up @@ -189,6 +190,36 @@ export const ResponsiveImage: Component<ResponsiveImageProps> = (props) => {
};
}

const thumbhashMeta = isLqipThumbhash(args.src.lqip)
? args.src.lqip
: undefined;
let thumbhashStyles: (() => JSX.CSSProperties | undefined) | undefined =
undefined;

if (!isServer && thumbhashMeta) {
const [blurhashLib] = createResource(() => {
return import('@responsive-image/core/thumbhash/decode');
});

thumbhashStyles = () => {
if (isLoaded()) {
return undefined;
}

const { hash } = thumbhashMeta;
const uri = blurhashLib()?.decode2url(hash);

if (!uri) {
return undefined;
}

return {
'background-image': `url("${uri}")`,
'background-size': 'cover',
};
};
}

return (
<picture>
{sourcesSorted.map((s) => (
Expand All @@ -205,7 +236,8 @@ export const ResponsiveImage: Component<ResponsiveImageProps> = (props) => {
data-ri-bh={blurhashMeta?.hash}
data-ri-bh-w={blurhashMeta?.width}
data-ri-bh-h={blurhashMeta?.height}
style={blurhashStyles?.()}
data-ri-th={thumbhashMeta?.hash}
style={blurhashStyles?.() ?? thumbhashStyles?.()}
on:load={() => setLoaded(true)}
/>
</picture>
Expand Down
38 changes: 38 additions & 0 deletions packages/solid/tests/client.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -486,4 +486,42 @@ describe('ResponsiveImage', () => {
'after image is loaded the background PNG is removed',
).to.equal('none');
});

test('it sets LQIP from thumbhash as background', async () => {
const { onload, loaded } = imageLoaded();
const imageData: ImageData = {
imageTypes: ['jpeg', 'webp'],
// to replicate the loading timing, we need to load a real existing image
imageUrlFor: () => `/test-assets/test-image.jpg?${cacheBreaker()}`,
aspectRatio: 1.5,
lqip: {
type: 'thumbhash',
hash: 'jJcFFYI1fIWHe4dweXlYeUaAmWj3',
},
};

const { container } = render(() => <ResponsiveImage src={imageData} />);
onload(container);

const imgEl = container.querySelector('img')!;
expect(imgEl).toBeDefined();
expect(imgEl?.complete).toBe(false);

await waitFor(
() =>
expect(imgEl.style.backgroundImage, 'it has a background PNG').to.match(
/data:image\/png/,
),
{ timeout: 5000 },
);
expect(imgEl).toHaveStyle({ backgroundSize: 'cover' });
expect(imgEl.style.backgroundImage).toMatchInlineSnapshot(`"url("data:image/png;base64,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")"`);

await loaded;

expect(
window.getComputedStyle(imgEl!).backgroundImage,
'after image is loaded the background PNG is removed',
).to.equal('none');
});
});
33 changes: 32 additions & 1 deletion packages/solid/tests/server.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,45 @@ describe('ResponsiveImage', () => {
imageUrlFor(width, type = 'jpeg') {
return `/provider/w${width}/image.${type}`;
},
aspectRatio: 1.5,
};

it('renders a responsive image', () => {
const string = renderToString(() => (
<ResponsiveImage src={defaultImageData} />
));
expect(string).toMatchInlineSnapshot(
`"<picture><source srcset="/provider/w640/image.avif 640w, /provider/w750/image.avif 750w, /provider/w828/image.avif 828w, /provider/w1080/image.avif 1080w, /provider/w1200/image.avif 1200w, /provider/w1920/image.avif 1920w, /provider/w2048/image.avif 2048w, /provider/w3840/image.avif 3840w" type="image/avif"><source srcset="/provider/w640/image.webp 640w, /provider/w750/image.webp 750w, /provider/w828/image.webp 828w, /provider/w1080/image.webp 1080w, /provider/w1200/image.webp 1200w, /provider/w1920/image.webp 1920w, /provider/w2048/image.webp 2048w, /provider/w3840/image.webp 3840w" type="image/webp"><source srcset="/provider/w640/image.jpeg 640w, /provider/w750/image.jpeg 750w, /provider/w828/image.jpeg 828w, /provider/w1080/image.jpeg 1080w, /provider/w1200/image.jpeg 1200w, /provider/w1920/image.jpeg 1920w, /provider/w2048/image.jpeg 2048w, /provider/w3840/image.jpeg 3840w" type="image/jpeg"><img width="320" class="ri-responsive " loading="lazy" decoding="async" src="/provider/w320/image.jpeg" style=""/></picture>"`,
`"<picture><source srcset="/provider/w640/image.avif 640w, /provider/w750/image.avif 750w, /provider/w828/image.avif 828w, /provider/w1080/image.avif 1080w, /provider/w1200/image.avif 1200w, /provider/w1920/image.avif 1920w, /provider/w2048/image.avif 2048w, /provider/w3840/image.avif 3840w" type="image/avif"><source srcset="/provider/w640/image.webp 640w, /provider/w750/image.webp 750w, /provider/w828/image.webp 828w, /provider/w1080/image.webp 1080w, /provider/w1200/image.webp 1200w, /provider/w1920/image.webp 1920w, /provider/w2048/image.webp 2048w, /provider/w3840/image.webp 3840w" type="image/webp"><source srcset="/provider/w640/image.jpeg 640w, /provider/w750/image.jpeg 750w, /provider/w828/image.jpeg 828w, /provider/w1080/image.jpeg 1080w, /provider/w1200/image.jpeg 1200w, /provider/w1920/image.jpeg 1920w, /provider/w2048/image.jpeg 2048w, /provider/w3840/image.jpeg 3840w" type="image/jpeg"><img width="320" height="213" class="ri-responsive " loading="lazy" decoding="async" src="/provider/w320/image.jpeg" style=""/></picture>"`,
);
});

it('renders blurhash LQIP', () => {
const imageData: ImageData = {
...defaultImageData,
lqip: {
type: 'blurhash',
hash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj',
width: 4,
height: 3,
},
};
const string = renderToString(() => <ResponsiveImage src={imageData} />);
expect(string).toMatchInlineSnapshot(
`"<picture><source srcset="/provider/w640/image.avif 640w, /provider/w750/image.avif 750w, /provider/w828/image.avif 828w, /provider/w1080/image.avif 1080w, /provider/w1200/image.avif 1200w, /provider/w1920/image.avif 1920w, /provider/w2048/image.avif 2048w, /provider/w3840/image.avif 3840w" type="image/avif"><source srcset="/provider/w640/image.webp 640w, /provider/w750/image.webp 750w, /provider/w828/image.webp 828w, /provider/w1080/image.webp 1080w, /provider/w1200/image.webp 1200w, /provider/w1920/image.webp 1920w, /provider/w2048/image.webp 2048w, /provider/w3840/image.webp 3840w" type="image/webp"><source srcset="/provider/w640/image.jpeg 640w, /provider/w750/image.jpeg 750w, /provider/w828/image.jpeg 828w, /provider/w1080/image.jpeg 1080w, /provider/w1200/image.jpeg 1200w, /provider/w1920/image.jpeg 1920w, /provider/w2048/image.jpeg 2048w, /provider/w3840/image.jpeg 3840w" type="image/jpeg"><img width="320" height="213" class="ri-responsive ri-lqip-blurhash " loading="lazy" decoding="async" src="/provider/w320/image.jpeg" data-ri-bh="LEHV6nWB2yk8pyo0adR*.7kCMdnj" data-ri-bh-w="4" data-ri-bh-h="3" style=""/></picture>"`,
);
});

it('renders thumbhash LQIP', () => {
const imageData: ImageData = {
...defaultImageData,
lqip: {
type: 'thumbhash',
hash: 'jJcFFYI1fIWHe4dweXlYeUaAmWj3',
},
};
const string = renderToString(() => <ResponsiveImage src={imageData} />);
expect(string).toMatchInlineSnapshot(
`"<picture><source srcset="/provider/w640/image.avif 640w, /provider/w750/image.avif 750w, /provider/w828/image.avif 828w, /provider/w1080/image.avif 1080w, /provider/w1200/image.avif 1200w, /provider/w1920/image.avif 1920w, /provider/w2048/image.avif 2048w, /provider/w3840/image.avif 3840w" type="image/avif"><source srcset="/provider/w640/image.webp 640w, /provider/w750/image.webp 750w, /provider/w828/image.webp 828w, /provider/w1080/image.webp 1080w, /provider/w1200/image.webp 1200w, /provider/w1920/image.webp 1920w, /provider/w2048/image.webp 2048w, /provider/w3840/image.webp 3840w" type="image/webp"><source srcset="/provider/w640/image.jpeg 640w, /provider/w750/image.jpeg 750w, /provider/w828/image.jpeg 828w, /provider/w1080/image.jpeg 1080w, /provider/w1200/image.jpeg 1200w, /provider/w1920/image.jpeg 1920w, /provider/w2048/image.jpeg 2048w, /provider/w3840/image.jpeg 3840w" type="image/jpeg"><img width="320" height="213" class="ri-responsive ri-lqip-thumbhash " loading="lazy" decoding="async" src="/provider/w320/image.jpeg" data-ri-th="jJcFFYI1fIWHe4dweXlYeUaAmWj3" style=""/></picture>"`,
);
});
});

0 comments on commit 0429d13

Please sign in to comment.