Skip to content
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

Image processing #241

Open
Rich-Harris opened this issue Dec 8, 2020 · 69 comments
Open

Image processing #241

Rich-Harris opened this issue Dec 8, 2020 · 69 comments
Labels
feature request New feature or request
Milestone

Comments

@Rich-Harris
Copy link
Member

Rich-Harris commented Dec 8, 2020

Updated Aug 2023 by @benmccann - this thread has gotten quite long, so I've summarized some key points here. We have some docs on the site (https://kit.svelte.dev/docs/assets) that share some helpful basics. This issue discusses a potential future implementation within SvelteKit

Static optimization with an Image component

Vite's build pipeline will handle assets that you import: https://vitejs.dev/guide/assets.html

You can use vite-imagetools to transform those images as part of Vite's pipeline. E.g. the most common transform might be to generate avif and webp versions of an image. You can use the defaultDirectives option to set a project-wide default or you can add query parameters to handle images individually. You can also import a directory of images using Vite's import.meta.glob with its query option.

You could use something like bluwy/svelte-preprocess-import-assets to let users simply write img tags that get converted to import statements for Vite to process.

A discussion of how to set this up is included at #241 (comment) and further demonstrated in #241 (comment).

Static optimization powered by a preprocessor

A problem with using an Image component in Svelte is that it requires the usage of :global to style it and it's difficult to handle events on it. It's possible some of these issues could be addressed in Svelte itself (e.g. there was a community proposal regarding event forwarding sveltejs/rfcs#60), but at the current time there are no concrete plans around this.

One solution to this would be to use a preprocessor to convert:

<img alt="delete" src="$lib/icons/trashcan.png" />

Into something like:

<script>
  import __image1 from '$lib/icons/trashcan.png';
  import __image1_avif from '$lib/icons/trashcan.png?format=avif';
  import __image1_webp from '$lib/icons/trashcan.png?format=webp';
</script>

<picture>
  <source type="image/avif" src={__image1_avif} />
  <source type="image/webp" src={__image1_webp} />
  <img alt="delete" src={__image1} width="24" height="24" />
</picture>

This actually scales very well since sveltejs/svelte#8948.

However, this approach doesn't work as well for something like the SvelteKit showcase or import.meta.glob because it requires the presence of an img tag.

Dynamic optimization

You could also implement a function to alter a URL and output the CDN's URL (e.g. #10323). Including this manually may become cumbersome, but as with the static optimization case, you could use something like bluwy/svelte-preprocess-import-assets to let users simply write img tags that get converted to use this function. The unpic library is an example of this approach. This approach works really well when users have a CDN available and some hosts like Vercel have CDNs as included offerings. For users that want to deploy to somewhere like GitHub pages the static approach might work better and so it may make sense to offer dynamic optimzation alongside one of the static optimization approaches.

@benmccann

This comment was marked as outdated.

@Rich-Harris

This comment was marked as outdated.

@benmccann

This comment was marked as outdated.

@Rich-Harris
Copy link
Member Author

Maybe — need to take a look and see if there's anything missing from this: https://twitter.com/rchrdnsh/status/1336386125552779264

@pngwn
Copy link
Member

pngwn commented Dec 8, 2020

That description isn't very helpful, does the plugin transform the sourcecode as well as optimising the image? If it only does the image optimisation, it would be pretty limiting in terms of lazy-loading, generating srcsets, etc. Likewise some way to set the priority of the image (by adding preload tags to the head) would also be good. next/image does all of this.

Image components aren't always the best solution but they offer a lot of flexibility depending on your needs.

@pngwn
Copy link
Member

pngwn commented Dec 8, 2020

I think the most comprehensive implementations will need to be framework specific, although I haven't thought about it a great deal.

@Rich-Harris Rich-Harris added this to the 1.0 milestone Mar 5, 2021
@antony
Copy link
Member

antony commented Mar 20, 2021

With regards to this, I think a rollup/vite plugin for https://www.npmjs.com/package/sharp would likely be able to fulfil these requirements.

@rchrdnsh
Copy link

rchrdnsh commented Mar 23, 2021

Hi all, thank you for considering this feature...

Just found this new (2 months old, it seems) plugin for Vite...starting to mess around with it with no success yet...seems potentially promising, however :-)

https://github.com/JonasKruckenberg/vite-imagetools

@alteredorange
Copy link

Nextjs handles this pretty well, and since it's open source, you can see how they do it. Docs here and here, with the main source code here. Handles different sources, sizes, etc. It's not perfect, but it's more robust than most options.

@benmccann benmccann added vite and removed bundling labels Mar 24, 2021
@yamiteru
Copy link

Hi all, thank you for considering this feature...

Just found this new (2 months old, it seems) plugin for Vite...starting to mess around with it with no success yet...seems potentially promising, however :-)

https://github.com/JonasKruckenberg/vite-imagetools

I'm in a close contact with the author and he's rewriting the whole library into core library and different bundler adapters including vite.

You can easily just do this to generate 6 different images together with their width and height info:

import {width, height, src} from "./image.jpg?format=webp;jpg&width=480;1024;1920&metadata"

@JonasKruckenberg
Copy link

JonasKruckenberg commented Mar 25, 2021

Hey guys! I'm a little late to the party, but whatever 👋🏻

With regards to this, I think a rollup/vite plugin for https://www.npmjs.com/package/sharp would likely be able to fulfil these requirements.

vite-imagetools (or the core library more specifically) is basically just a wrapper around sharp, that let's you "execute" sharps image function in the import statement.

The basic idea though is to provide a "common api" that lets you specify how you want your image to be transformed and all the rest (wether the transformation is done by sharp or libvips directly, what buildtools you use, etc.) are completely irrelevant to the developer as all information is encoded in the url and not some config file.

It's not super reliable in vite yet(because vite is sometimes a bit quirky with static assets)
and the docs are horrible,
but I'm working on improvements and other buildtool integrations so if you have any questions, bug, feature requests just let me know 👍🏻

@cryptodeal
Copy link

cryptodeal commented Mar 26, 2021

@JonasKruckenberg really appreciate your work on the vite-imagetools library. I was able to get vite-imagetools integrated with the current release of sveltekit and the node adapter!

  1. Install vite-imagetools@next as a devDependency
  2. In your svelte.config.cjs, ensure you have imagetools() set as a vite plugin:
const imagetools = require('vite-imagetools')

vite: {
  plugins: [imagetools({force: true})]
}
  1. wherever you want to import the transformed image (example shown below is in src/routes/$layout.svelte) :
<script>
   import Logo1 from '../../static/title.png?webp&meta'
</script>

<img src={Logo1.src} alt="alt description">

Edit: With srcset:

<script>
   import Logo1 from '../../static/title.png?w=300;400;500&format=webp&srcset'
</script>
<img srcset={Logo1} type="image/webp" alt="testattribute"/>

@babichjacob
Copy link
Member

babichjacob commented Mar 27, 2021

   import Logo1 from '../../static/title.png?w=300;400;500&format=webp&srcset'

Is it a better idea to put your images like this in the src folder instead of importing from static which requires weird .. directory traversal?

@cryptodeal
Copy link

Is it a better idea to put your images like this in the src folder instead of importing from static which requires weird .. directory traversal?

Definitely; I will likely end up moving them to src/static and updating svelte.config.cjs to reflect the updated location of the static assets.

@rchrdnsh
Copy link

hmmmm...

might it be worth considering adding some sort of official Svelte/Kit support for some sort of assets folder in the src folder for images that will be processed by plugins such as these in the future?

@babichjacob
Copy link
Member

hmmmm...

might it be worth considering adding some sort of official Svelte/Kit support for some sort of assets folder in the src folder for images that will be processed by plugins such as these in the future?

What more is SvelteKit supposed to do than is already possible per the comment above?

@rchrdnsh
Copy link

rchrdnsh commented Mar 28, 2021

well, i don't have to make a lib folder myself, as it's already there, with a nice and ergonomic alias as well, which would be super awesome to have for an images folder...but maybe it's easy to make as alias, as I simply don't know how...but to be more specific, making image processing part of the default configuration out of the box, as it's such an important consideration for performant sites and apps...or maybe even a svelte-add for image processing in the future...

@cryptodeal
Copy link

cryptodeal commented Mar 28, 2021

Example integration of vite-imagetools and sveltekit with $static alias for src/static below:

  1. Install vite-imagetools@next as a devDependency
  2. Your svelte.config.cjs should be modified to include the following:
const node = require('@sveltejs/adapter-node');
const pkg = require('./package.json');
const imagetools = require('vite-imagetools')
const path = require('path')

/** @type {import('@sveltejs/kit').Config} */
module.exports = {
  kit: {
    files: {
      assets: 'src/static',
    },
    vite: {
      resolve: {
        alias: {
          $static: path.resolve('src/static')
        }
      },
      plugins: [imagetools({force: true})]
    }
  },
};
  1. Example importing png and converting to webp ($static alias to src/static for convenience)
<script>
   import Logo1 from '$static/title.png?webp&meta'
</script>
<img src={Logo1.src} alt="alt description">
  1. Example importing png in 3 sizes, convert to webp and render as srcset. ($static alias to src/static for convenience)
<script>
   import Logo1 from '$static/title.png?w=300;400;500&format=webp&srcset'
</script>
<img srcset={Logo1} type="image/webp" alt="testattribute"/>

@rchrdnsh
Copy link

very nice :-) We should probably move this kind of stuff to discord, but thank you!

@pzerelles
Copy link
Contributor

pzerelles commented Mar 30, 2021

There is so much boilerplate to write, even more if you need backward compatibility with WebP in a picture tag. But since svelte is a compiler, can't we just have an image transformation attribute right on the img tag that can create whatever we want - including lazy loading or fade-in transitions and more?

@tony-sull
Copy link

Well I went down the rabbit hole of trying to use vite-imagetools with SvelteKit yesterday. I have a site using Netlify CMS with local JSON files and wanted to dynamically load and resize images at build time along the lines of

<script lang="ts" context="module">
    import page from "$data/pages/home.json";
    
    export async function load() {
        const { default: hero } = await import(`../static/${page.hero}.jpg?height=550&format=webp`)
        
        return {
            props: {
                hero
            }
        }
    }
</script>

It looks like it may be a limitation related to dynamic-import-vars, but when digging through that project I couldn't tell if the query parameters are expected to break. Based on the examples in vite-imagetools I would have assumed query params alone wouldn't break production builds, but who knows.

I made sure to follow the workarounds for known limitations of the rollup plugin, like having the import starts with ./ or ../ and the file extension is included for the glob serach. It works fine in dev and actually works in production without the query params, I was hoping the dynamic-import-vars logic would have been able to strip off the query params and find the original image file.

Repro

I made a barebones repro of this at https://github.com/tonyfsullivan/sveltekit-imagetools-repro. Not sure if my use case here is just an outlier, it works flawlessly when the image file names are basic strings rather than dynamic variables.

Does anyone know if this should have worked for me, or where the bug/feature request would land? At the moment I have no idea if this would be related to vite, vite-imagetools, @rollup/plugin-dynamic-import-vars, or if it's specific to SvelteKit somehow.

@JonasKruckenberg
Copy link

I don't know enough about svelte-kit to really debug this, but I can confirm that it is not an issue with vite-imagetools as the plugin never gets told about the image in build mode. Seems to be related to vite-imagetools#34 so a problem with with the resolution of the import glob pattern

@pzerelles
Copy link
Contributor

I wrote a little preprocessor to make using vite-imagetools even easier. Modifiers can be directly appended to the img's src or srcset prop and the imports will be added by the preprocessor.

const svelte = require("svelte/compiler")

/**
 * @typedef Options
 * @property {{ [tag: string]: string[] }} tags
 */

/**
 * @typedef {import('svelte/types/compiler/interfaces').TemplateNode} TemplateNode
 */

/**
 * @param {Options} [options]
 * @returns {import('svelte/types/compiler/preprocess/types').PreprocessorGroup}
 */
function imagePreprocess(options = {}) {
  const imports = {}
  const tags = options.tags || {
    img: ["src", "srcset", "data-src", "data-srcset"],
    source: ["src", "srcset", "data-src", "data-srcset"],
  }
  return {
    markup: ({ content, filename }) => {
      const preparedContent = content
        .replace(/<style[^>]*>[\w\W]+<\/style>/g, (match) => " ".repeat(match.length))
        .replace(/<script[^>]*>[\w\W]+<\/script>/g, (match) => " ".repeat(match.length))
      let ast
      try {
        ast = svelte.parse(preparedContent)
      } catch (e) {
        console.error(e, "Error parsing content")
        return
      }

      /** @type {TemplateNode[]} */
      const matches = []
      svelte.walk(ast, {
        enter: (node) => {
          if (!["Element", "Fragment", "InlineComponent"].includes(node.type)) {
            return
          }

          if (tags[node.name]) {
            matches.push({ node, attributes: tags[node.name] })
          }
        },
      })

      const dependencies = []
      const code = matches.reduce(
        /**
         * @param {{content: string, offset: number}} processed
         * @param {{node: TemplateNode, attributes: string[]} match
         * @param {number} index
         */
        (processed, match, index) => {
          const attributes = (match.node.attributes || []).filter(
            (a) => a.type === "Attribute" && match.attributes.includes(a.name)
          )
          if (
            attributes.length === 0 ||
            (match.node.attributes || []).find((a) => a.name === "rel" && a.value[0].data === "external")
          ) {
            return processed
          }

          let { content, offset } = processed

          for (const attribute of attributes) {
            if (attribute.value[0]?.type === "Text") {
              const value = attribute.value[0]
              if (value.data.startsWith("http")) continue

              const symbol = `__IMAGE_${index}__`
              const replacement = `{${symbol}}`

              if (!imports[filename]) imports[filename] = {}
              imports[filename][symbol] = value.data

              dependencies.push(value.data)

              content = content.substring(0, value.start + offset) + replacement + content.substring(value.end + offset)

              offset += replacement.length - value.data.length
            }
          }
          return { content, offset }
        },
        { content, offset: 0 }
      ).content

      return { code, dependencies }
    },
    script: ({ content, attributes, filename }) => {
      if (!attributes.context) {
        const localImports = Object.entries(imports[filename] || {})
        if (localImports.length > 0) {
          const dependencies = localImports.map(([symbol, path]) => path)
          const code =
            localImports.map(([symbol, path]) => `import ${symbol} from "${path}"`).join("\n") + "\n" + content
          return { code, dependencies }
        }
      }
    },
  }
}

module.exports = imagePreprocess

@Rich-Harris Rich-Harris modified the milestones: 1.0, post-1.0 May 1, 2021
@brev
Copy link
Contributor

brev commented Jan 20, 2023

@eur2 no, sorry, nothing automatic with markdown. If using mdsvex you could import the manifest at top, and call the svelte component from within, but that's not very markdown-like.

@benmccann benmccann added the feature request New feature or request label Jan 20, 2023
@eur2
Copy link

eur2 commented Jan 22, 2023

It seems that this new package will solve all the image optimization issues in an easy and efficient way:
https://github.com/divriots/jampack

@benmccann benmccann removed the vite label Jan 24, 2023
@jdgamble555
Copy link

Steve talks about some great options with the <picture> tag

https://youtu.be/-zzmfjIiC3M

J

@davej
Copy link

davej commented Feb 3, 2023

It seems that this new package will solve all the image optimization issues in an easy and efficient way: https://github.com/divriots/jampack

This doesn't work for SPAs though right? This requires that your site is static.

@djmtype
Copy link

djmtype commented Feb 20, 2023

@benmccann Would you mind sharing more of your Image component file, more particularly how you're adding different widths?

@rdela
Copy link

rdela commented Feb 20, 2023

Would you mind sharing more of your Image component file, more particularly how you're adding different widths?

@djmtype this PR may be of interest, planning to merge these changes from @SirNovi soon with a little documentation rdela/sveltekit-imagetools#1

(Thanks @benmccann and @eur2 for weighing in.)

@djmtype
Copy link

djmtype commented Feb 22, 2023

Thanks @rdela. Does that mean they'll be a drop-in Image/Picture component akin to Astro Imagetools rather than appending options as a query within the script tags?

@TomSmedley
Copy link

+1 would love to see this, mainly for automatic WebP. But image optimisations are always good to have and seems to be the biggest thing Page Speed Insights complains about.

@benmccann
Copy link
Member

For folks who want to use a CDN, there's @unpic/svelte. It supports Cloudflare and Vercel, so possibly could be a helpful solution for folks using those adapters.

@jasongitmail
Copy link

Great contributions above! But broken in the latest Vite Imagetools.

I fixed it up for current versions and added some improvements. Instructions for others:

Vite Imagetools Instructions

Tested with:

"@sveltejs/kit": "^1.5.0",
"vite": "^4.3.0",
"vite-imagetools": "~5.0.4",
  1. Install using: npm i -D vite-imagetools
  2. Update vite.config.js to add:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
+ import { imagetools } from 'vite-imagetools';

export default defineConfig({
  plugins: [
+  imagetools({
+    defaultDirectives: new URLSearchParams({
+      format: 'avif;webp',
+      as: 'picture'
+    })
+  }),

    sveltekit()
  ],
  test: {
    include: ['src/**/*.{test,spec}.{js,ts}']
  }
});
  1. Create this file at src/lib/image.svelte:
<script>
  /**
   * @typedef {Object} Picture
   * @property {Object} sources - The object containing different sources of image data.
   * @property {Object[]} sources.avif - The array of objects containing source and width for AVIF images.
   * @property {string} sources.avif[].src - The source of the AVIF image.
   * @property {number} sources.avif[].w - The width of the AVIF image.
   * @property {Object[]} sources.webp - The array of objects containing source and width for WebP images.
   * @property {string} sources.webp[].src - The source of the WebP image.
   * @property {number} sources.webp[].w - The width of the WebP image.
   * @property {Object} img - The object containing the default image source.
   * @property {string} img.src - The default image source.
   * @property {number} img.w - The width of the default image.
   * @property {number} img.h - The height of the default image.
   */

  /** REQUIRED */

  /** @type {Picture} */
  export let src;

  export let alt = '';

  /** OPTIONAL */

  /** @type {Boolean} */
  export let draggable = false;

  /** @type {'async' | 'sync' | 'auto'} */
  export let decoding = 'async';

  /** @type {'lazy' | 'eager'} */
  export let loading = 'lazy';

  let classes = '';
  export { classes as class };

  /** @type {number} */
  export let width;
</script>

<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
    src={src.img.src}
    {alt}
    class={classes}
    {loading}
    {decoding}
    {draggable}
    width={src.img.w}
    height={src.img.h}
  />
</picture>
  1. Add an image at lib/assets/images/example.jpg
  2. Use:
<script>
	import Image from '$lib/image.svelte';
	import example from '$lib/assets/images/example.jpg?w=400';
</script>

<pre>
  {JSON.stringify(example, null, 2)}
</pre>

<Image src={example} alt="Ocean Unsplash" />

Known limitations

  1. Although it "works", without support for pixel density declaration, images look soft. This image component doesn't support pixel density declaration and should, but vite-imagetools ?density= property appears broken currently.

@rchrdnsh

This comment was marked as outdated.

@benmccann

This comment was marked as outdated.

@rchrdnsh

This comment was marked as outdated.

dummdidumm added a commit that referenced this issue Jul 5, 2023
part of #241
closes #9787

This adds image optimization through a new $app/images import. It's deliberately low level: The only export is getImage which you pass an image src and it returns an object containing src and srcset (possibly more?) values which you spread on an img tag.

In order to use this you need to define a path to a loader in kit.config.images. The loader takes the original img src and a width and returns a URL pointing to the optimized image. You can also modify the number of sizes and trusted domains.
@benmccann
Copy link
Member

  1. Although it "works", without support for pixel density declaration, images look soft. This image component doesn't support pixel density declaration and should, but vite-imagetools ?density= property appears broken currently.

Thanks for testing it out and sharing the feedback. There was never such a directive in vite-imagetools, but I agree there should be. I just added a new directive (with a different name) and will use it in #10788

@benmccann
Copy link
Member

@sveltejs/enhanced-img is now available to help optimized local images contained within your project. Read more at https://kit.svelte.dev/docs/images

I'll leave this issue open for a little while longer as we consider support for image CDNs (#10323)

@rdela
Copy link

rdela commented Nov 30, 2023

@benmccann @sveltejs/enhanced-img seems to address @djmtype’s question, any reason I shouldn’t update rdela/sveltekit-imagetools to use that? Or do you want to PR some changes? Is there a complete example anywhere other than the docs currently?

@benmccann
Copy link
Member

I think rdela/sveltekit-imagetools could probably be retired now and if there's any need for additional documentation we should try to update the official docs

@rdela
Copy link

rdela commented Dec 5, 2023

Does anyone else think having a complete example in a repo anywhere is helpful?

UPDATE 2023-12-08: RETIRED

@FractalHQ
Copy link
Member

How might this work with divs using images via backgorund-image: url()?

@leoj3n
Copy link
Contributor

leoj3n commented Feb 29, 2024

How might this work with divs using images via backgorund-image: url()?

Probably your best bet is to avoid using CSS background images if you can, and instead make a child element that is absolutely positioned and the same size as the parent, but have the "replaced content" (the actual "image" contained in the <img> tag) be object-fit: cover... There are a number of methods to get the child element to match the parent width/height when using position: absolute, something like inset: 0; width: 100%; height: 100% and CSS like that.

For a fixed background image covering the whole page I have this at the beginning of +layout.svelte:

<script>
	import Srcset from '$lib/components/image/Srcset.svelte';
</script>

<Srcset
	alt=""
	width="2400"
	height="1500"
	lazy={false}
	quality={80}
	draggable="false"
	fetchpriority="high"
	class="unselectable"
	src="/images/background/vaneer.jpg"
	sizes="max(100vw, calc(100vh/(1500/2400)))"
	style="position: fixed; width: 100vw; height: 100vh; object-fit: cover; object-position: top center"
/>

<!--
Note: you may want to change sizes to something more like sizes="max(100vw, 100vh)" as most portrait
screens will have a DPR around 2.0 which means they will request this specified "size" times 2... and
100vh * 2 happens to be a pretty good middle ground for most mobile screens... so taking the aspect
ratio into consideration here is not so helpful as 1.0 DPR desktop devices are usually landscape... and
100vw would be a rather small image on mobile but filling 100% height would be too much when * 2...
Just want to illustrate the point that there is often more to the decision than just setting 100vw and you
will see that in Lighthouse scores saying potential savings of some KiB... For instance you may want to
take into consideration your website margins at certain breakpoints that reduce the image rendered size
to less than 100vw. [Perhaps calc(50vh/(1500/2400)) is even better; anticipating the doubling on mobile]
-->

Where Srcset pumps out an <img> tag that has srcset entries at various widths pointing to a CDN.

However, this won't work if you need a repeating background image. The best you can do then is to use image-set. You will probably want to use preload in addition if you decide to use image-set.

I suppose it depends on your design requirements, but I see a lot of nice results using just CSS gradients or even canvas animations. I think the general consensus is avoid CSS backgrounds if you can, as they currently aren't very flexible and when the CSS object model is being built is not an optimal time to begin downloading images.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request
Projects
None yet
Development

No branches or pull requests