-
-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a low-quality-placeholder directive #86
Comments
I agree on blurring, that should be handled by the user to fit their desired aesthetic. I do think both the size and quality should be customizable to an extent (I like the idea of using an integer that scales the values accordingly), but I don't think there's really a need for fine grained controls given lqip would be, ultimately, for the sake of convenience. If a user needs the ability to highly customize the placeholder image, they should import the placeholder using the provided directives as that's going to give the most granular control in terms of the resulting image. E.g. I currently import placeholder images, which is passed into a lazy loading responsive image component (along w the srcset imported separately), by doing the following:
Edit: Great work on this library by the way; love using it in with SvelteKit. |
Adds a low-quality-placeholder directive as discussed in #86
Adds a low-quality-placeholder directive as discussed in #86
+1 I would love this feature. Great library btw it works great! |
Hey @cryptodeal do you have an example of that component? |
There is another style of placeholder which I really like "traced placeholder". It is included with the I created a custom directive using the import { imagetools } from "vite-imagetools";
import { setMetadata } from "imagetools-core";
import potrace from "potrace";
import { promisify } from "util";
import { optimize } from "svgo";
const trace = promisify(potrace.trace);
function svgPlaceholderTransform(config) {
if (!("svgPlaceholder" in config)) return;
return async function (image) {
const svg = await trace(await image.toBuffer(), {
// background: "#fff", // Default is transparent
color: "#002fa7",
threshold: 120,
});
const { data } = optimize(svg, {
multipass: true,
floatPrecision: 0,
datauri: "base64",
});
setMetadata(image, "svgPlaceholder", data);
return image;
};
}
const imagetoolsPlugin = imagetools({
extendTransforms: (builtins) => [svgPlaceholderTransform, ...builtins],
});
export { imagetoolsPlugin }; Some Svelte template <script lang="ts">
import srcsetWebp from "././photo-1531315630201-bb15abeb1653.jpeg?w=500;700;900;1200&webp&srcset";
import { svgPlaceholder } from "./photo-1531315630201-bb15abeb1653.jpeg?meta&svgPlaceholder";
</script>
<div>
<img src={svgPlaceholder} alt="Testing" />
<picture>
<source srcset={srcsetWebp} type="image/webp" />
<img alt="Thing" />
</picture>
</div>
<style>
img {
width: 300px;
}
div {
display: flex;
}
</style>
There is additional work to show and hide depending on the loading but you get the idea. Also I added this to my declare module "*?meta&svgPlaceholder" {
const svgPlaceholder: string;
export { svgPlaceholder };
export default { svgPlaceholder };
} |
Is this an official feature now? It looks like it was added as a directive by @JonasKruckenberg. Not mentioned in the Docs however. |
Would love to see |
This is what I've been using for now but would like some help integrating it into the imagetools. The result can be seen here(may require throttling)
import adapter from '@sveltejs/adapter-cloudflare';
import { vitePreprocess } from '@sveltejs/kit/vite';
import { importAssets } from 'svelte-preprocess-import-assets';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: [
importAssets({
sources: (defaultSources) => {
return [
...defaultSources,
{
tag: 'Image',
srcAttributes: ['src']
}
];
}
}),
vitePreprocess()
],
kit: {
adapter: adapter(),
}
};
export default config;
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
import Icons from 'unplugin-icons/vite';
import { imagetools } from 'vite-imagetools';
import {
setMetadata,
type OutputFormat,
type TransformFactory,
type Picture
} from 'imagetools-core';
import { createPlaceholder } from './placeholder';
const placeholderTransform: TransformFactory = (config) => {
return async function (image) {
if (!('lqip' in config)) return image;
/** @ts-ignore it's a string */
const href = await createPlaceholder(image.options.input.file);
setMetadata(image, 'lqip', href);
return image;
};
};
const pictureProxy = (a: OutputFormat): OutputFormat => {
return function (metadatas) {
const pictureFormat = a(metadatas);
return async function (imageConfig) {
// console.log(imageConfig);
const picture = pictureFormat(imageConfig) as Picture;
return { ...picture, lqip: imageConfig[0].lqip };
};
};
};
export default defineConfig({
plugins: [
imagetools({
extendOutputFormats: (builtins) => {
return { ...builtins, picture: pictureProxy(builtins.picture) };
},
extendTransforms: (builtins) => {
return [placeholderTransform, ...builtins];
},
defaultDirectives: (url) => {
if (url.searchParams.has('optimize')) {
/** @ts-ignore we can pass in booleans */
return new URLSearchParams({
w: '1920;1366;780;414',
format: 'avif;webp;jpg',
picture: true,
lqip: true
});
}
return new URLSearchParams();
}
}),
sveltekit()
],
});
// Copied from: https://github.com/xiphux/svimg
import sharp from 'sharp';
const PLACEHOLDER_WIDTH = 16;
/**
* @param {string} inputFile
* @param {{ width: number; height?: number; quality?: number }} options
*/
async function resizeImage(inputFile, options) {
if (!inputFile) {
throw new Error('Input file is required');
}
let sharpInstance = sharp(inputFile).toFormat('webp').blur(3);
sharpInstance = sharpInstance.resize(options.width, options.height);
return sharpInstance.toBuffer();
}
/**
* @param {string} inputFile
*/
export async function getImageMetadata(inputFile) {
if (!inputFile) {
throw new Error('Input file is required');
}
return sharp(inputFile).metadata();
}
// sharp only supports a very specific list of image formats,
// no point depending on a complete mime type database
/**
* @param {string | undefined} format
*/
export function getMimeType(format) {
switch (format) {
case 'jpeg':
case 'png':
case 'webp':
case 'avif':
case 'tiff':
case 'gif':
return `image/${format}`;
case 'svg':
return 'image/svg+xml';
}
return '';
}
/**
* @param {string} inputFile
*/
export async function createPlaceholder(inputFile) {
if (!inputFile) {
throw new Error('Input file is required');
}
const [{ format }, blurData] = await Promise.all([
getImageMetadata(inputFile),
resizeImage(inputFile, { width: PLACEHOLDER_WIDTH })
]);
const blur64 = blurData.toString('base64');
const mime = getMimeType(format);
const href = `data:${mime};base64,${blur64}`;
return href;
}
<script lang="ts">
import { onMount } from 'svelte';
import type { Picture } from 'vite-imagetools';
interface PictureWithLQIP extends Picture {
lqip: string;
}
export let src: string | PictureWithLQIP;
export let alt: string;
export let decoding: 'async' | 'sync' | 'auto' = 'auto';
export let style = '';
let className = '';
export { className as class };
export let dominantColor = '#F8F8F8';
export let loading: 'eager' | 'lazy' = 'lazy';
// fade-in the image after it has loaded
let image: HTMLImageElement;
let hidden: boolean | undefined = undefined;
onMount(() => {
if (image.complete) return;
image.onload = () => (hidden = false);
if (hidden === undefined) hidden = true;
});
</script>
<div
style="{typeof src !== 'string' && src.lqip
? `background-image: url(${src.lqip})`
: `background-color: ${dominantColor}`}; {style}"
class="img__placeholder {className}"
>
{#if typeof src === 'string'}
<img
bind:this={image}
{style}
class={className}
class:hidden
{src}
{alt}
{loading}
{decoding}
/>
{:else}
<picture>
{#each Object.entries(src.sources) as [format, images]}
<source
srcset={images.map((i) => `${i.src} ${i.w}w`).join(', ')}
type={'image/' + format}
/>
{/each}
<img
bind:this={image}
{style}
class={className}
class:hidden
src={src.fallback.src}
width={src.fallback.w}
height={src.fallback.h}
{alt}
{loading}
{decoding}
/>
</picture>
{/if}
</div>
<style>
.img__placeholder,
img {
width: 100%;
height: auto;
}
.img__placeholder {
height: min-content;
background-size: cover;
background-repeat: no-repeat;
overflow: hidden;
}
img {
display: block;
transition: opacity 0.25s ease-out;
object-fit: cover;
/* hide alt text while image is loading */
color: transparent;
}
.hidden {
opacity: 0;
}
</style> |
You can also use a gradient Or through canvas as in a Medium |
I needed this feature in a Vite plugin, and ran across this thread and saw it wasn’t supported (or is it? and undocumented?), so I made vite-plugin-lqip that handles LQIP and nothing else (can be used with vite-imagetools as this doesn’t optimize anything). I didn’t see lqip-modern mentioned in this thread, but was pretty impressed with the results it gave both in quality and tiny filesize, so that’s the approach I took. I’m still testing it / playing around with it, but open to feedback if anyone has any needed improvements. Might be easier for LQIP to be its own thing rather than putting that burden on vite-imagetools. But if @JonasKruckenberg wants to roll this into vite-imagetools I’d be more than happy to oblige 🙂 |
It seems to me that you might want to use different placeholders for different use cases. E.g. if the image is mostly a design element that's not displaying content then background color, gradient, or blur approaches could make sense. If the image is displaying necessary content like text then something like the traced placeholder might be best. I also think we might want to be pretty selective about encouraging the use of low quality placeholders as lazy loading can be annoying. |
My ideal solution for the problem being solved by placeholders would be an improved prioritization of image fetching by the browser potentially with user hints. I filed an issue for this with the WHATWG: whatwg/html#10056 |
This has been a requested feature and one that I'd argue is a wothwhile addition to the library.
I'm not quite sure on the specific syntax though and would like to get some feedback!
My proposal would be:
lqip
Where lqip invokes the following steps:
The question is wether these values should be customizeable on if so how? An integer that scales the values accordingly?
I don't want this directive to do too much since stuff like blurring can and should be added by the user to their liking.
Originally posted by @rchrdnsh in #69
The text was updated successfully, but these errors were encountered: