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

feat: support densities for devices with DevicePixelRatio > 1 #769

Merged
merged 18 commits into from
Jun 10, 2023

Conversation

Mihanik71
Copy link
Contributor

@Mihanik71 Mihanik71 commented Mar 14, 2023

Add densities param for issues #216

Add:

  • default value for densities in nuxt.config
  • add param for img and picture components

resolves #216
closes #803

@netlify
Copy link

netlify bot commented Mar 14, 2023

👷 Deploy request for nuxt-image pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit ee1ece6

@nuxt-studio
Copy link

nuxt-studio bot commented Mar 14, 2023

Live Preview ready!

Name Edit Preview Latest Commit
Image Edit on Studio ↗︎ View Live Preview ee1ece6

@simonkuran
Copy link

I could really use this feature. Would love to see this merged and released!

@simonkuran simonkuran mentioned this pull request Mar 31, 2023
# Conflicts:
#	src/types/image.ts
@Mihanik71
Copy link
Contributor Author

I could really use this feature. Would love to see this merged and released!

Thank you! Fixed the conflict so that you can easily merge

@Kolobok12309
Copy link
Contributor

Big thanks for this pr, i write something similar #803, but your solution more cleanest
Found some small bugs

  1. Not worked for sizes images, fixed by adding densities: props.densities here

    const options = computed(() => {
    return {
    provider: props.provider,
    preset: props.preset
    }
    })

  2. srcset entries may duplicate, for example in this case, 400

<nuxt-img
  src="/logos/nuxt.png"
  sizes="sm:200px lg:400px"
  densities="1x 2x 3x"
/>
/_ipx/w_200/logos/nuxt.png 200w, /_ipx/w_400/logos/nuxt.png 400w, /_ipx/w_400/logos/nuxt.png 400w, /_ipx/w_600/logos/nuxt.png 600w, /_ipx/w_800/logos/nuxt.png 800w, /_ipx/w_1200/logos/nuxt.png 1200w

Copy link
Member

@danielroe danielroe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be able to have a look at the failing tests?

I also think we can default to [1, 2] as default densities.

@danielroe danielroe marked this pull request as draft June 7, 2023 21:15
Copy link
Collaborator

@hartmut-co-uk hartmut-co-uk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All proposed code changes should fix the implementation, I also have adjusted tests, not sure how to propose changes via PR suggestions when the files have not been changed yet..

docs/content/5.configuration.md Outdated Show resolved Hide resolved
docs/content/5.configuration.md Outdated Show resolved Hide resolved
src/runtime/components/nuxt-img.ts Outdated Show resolved Hide resolved
src/runtime/utils/index.ts Outdated Show resolved Hide resolved
src/runtime/image.ts Outdated Show resolved Hide resolved
src/runtime/image.ts Outdated Show resolved Hide resolved
src/runtime/image.ts Outdated Show resolved Hide resolved
src/runtime/image.ts Outdated Show resolved Hide resolved

function getDensitySet (ctx: ImageCTX, input: string, opts: ImageSizesOptions): string|undefined {
const srcSet :{ density: string, src: string }[] = []
let densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities
// let densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities
let densities = opts.densities ? parseDensities(opts.densities) : (ctx.options.densities.length ? ctx.options.densities : [1, 2]) // TODO: pls help, default should come from `ctx.options.densities` ?!?

src/module.ts Outdated Show resolved Hide resolved
@hartmut-co-uk
Copy link
Collaborator

also to fix existing tests, I have made following changes:

test/e2e/generate.test.ts

import { fileURLToPath } from 'node:url'

import { describe, it, expect } from 'vitest'
import { setup, useTestContext } from '@nuxt/test-utils'
import { useNuxt } from '@nuxt/kit'
import { resolve } from 'pathe'
import { globby } from 'globby'

await setup({
  rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
  build: true,
  nuxtConfig: {
    image: {
      inject: false
    },
    hooks: {
      'modules:before' () {
        const nuxt = useNuxt()
        nuxt.options.nitro.prerender = { routes: ['/provider/ipx'] }
      }
    }
  }
})

describe('ipx provider', () => {
  it('generates static files', async () => {
    const ctx = useTestContext()
    const outputDir = resolve(ctx.nuxt!.options.nitro.output?.dir || '', 'public/_ipx')
    const files = await globby(outputDir)
    expect(files.sort().map(f => f.replace(outputDir, '/_ipx'))).toMatchInlineSnapshot(`
      [
        "/_ipx/_/images/nuxt.png",
        "/_ipx/s_300x300/images/colors.jpg",
        "/_ipx/s_300x300/images/everest.jpg",
        "/_ipx/s_300x300/images/tacos.svg",
        "/_ipx/s_300x300/unsplash/photo-1606112219348-204d7d8b94ee",
        "/_ipx/s_600x600/images/colors.jpg",
        "/_ipx/s_600x600/images/everest.jpg",
        "/_ipx/s_600x600/images/tacos.svg",
        "/_ipx/s_600x600/unsplash/photo-1606112219348-204d7d8b94ee",
      ]
    `)
  })
})

test/unit/image.test.ts

// @vitest-environment nuxt

import { beforeEach, describe, it, expect } from 'vitest'
import { ComponentMountingOptions, VueWrapper, mount } from '@vue/test-utils'

import { NuxtImg } from '#components'

describe('Renders simple image', () => {
  let wrapper: VueWrapper<any>
  const src = '/image.png'

  beforeEach(() => {
    wrapper = mountImage({
      width: 200,
      height: 200,
      sizes: '200,500:500,900:900',
      src
    })
  })

  it('Matches snapshot', () => {
    expect(wrapper.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1800x1800/image.png\\" width=\\"200\\" height=\\"200\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/s_500x500/image.png 500w, /_ipx/s_900x900/image.png 900w, /_ipx/s_1000x1000/image.png 1000w, /_ipx/s_1800x1800/image.png 1800w\\">"')
  })

  it('props.src is picked up by getImage()', () => {
    const domSrc = wrapper.element.getAttribute('src')
    expect(domSrc).toMatchInlineSnapshot('"/_ipx/s_1800x1800/image.png"')
  })

  it('props.src is reactive', async () => {
    const newSource = '/image.jpeg'
    wrapper.setProps({ src: newSource })
    await nextTick()

    const domSrc = wrapper.find('img').element.getAttribute('src')
    expect(domSrc).toMatchInlineSnapshot('"/_ipx/s_1800x1800/image.jpeg"')
  })

  it('sizes', () => {
    const sizes = wrapper.find('img').element.getAttribute('sizes')
    expect(sizes).toBe('(max-width: 500px) 500px, 900px')
  })

  it('encodes characters', () => {
    const img = mountImage({
      width: 200,
      height: 200,
      sizes: '200,500:500,900:900',
      src: '/汉字.png'
    })
    expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1800x1800/%E6%B1%89%E5%AD%97.png\\" width=\\"200\\" height=\\"200\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/s_500x500/%E6%B1%89%E5%AD%97.png 500w, /_ipx/s_900x900/%E6%B1%89%E5%AD%97.png 900w, /_ipx/s_1000x1000/%E6%B1%89%E5%AD%97.png 1000w, /_ipx/s_1800x1800/%E6%B1%89%E5%AD%97.png 1800w\\">"')
  })

  it('correctly sets crop', () => {
    const img = mountImage({
      src: '/image.png',
      width: 1000,
      height: 2000,
      sizes: 'xs:100vw sm:100vw md:300px lg:350px xl:350px 2xl:350px'
    })
    expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1280x2560/image.png\\" width=\\"1000\\" height=\\"2000\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 320px) 100vw, (max-width: 640px) 100vw, (max-width: 768px) 300px, (max-width: 1024px) 350px, (max-width: 1280px) 350px, 350px\\" srcset=\\"/_ipx/s_300x600/image.png 300w, /_ipx/s_320x640/image.png 320w, /_ipx/s_350x700/image.png 350w, /_ipx/s_600x1200/image.png 600w, /_ipx/s_640x1280/image.png 640w, /_ipx/s_700x1400/image.png 700w, /_ipx/s_1280x2560/image.png 1280w\\">"')
  })
})

const mountImage = (props: ComponentMountingOptions<typeof NuxtImg>['props']) => mount(NuxtImg, { props })

test/unit/picture.test.ts

// @vitest-environment nuxt

import { beforeEach, describe, it, expect } from 'vitest'
import { VueWrapper, mount } from '@vue/test-utils'
import { NuxtPicture } from '#components'

describe('Renders simple image', () => {
  let wrapper: VueWrapper<any>
  const src = '/image.png'

  const observer = {
    wasAdded: false,
    wasDestroyed: false
  }

  beforeEach(() => {
    window.IntersectionObserver = class IntersectionObserver {
      root: any
      rootMargin: any
      thresholds: any
      takeRecords: any

      observe (_target: Element) {
        observer.wasAdded = true
      }

      disconnect () {
        observer.wasDestroyed = true
      }

      unobserve () {
        observer.wasDestroyed = true
      }
    }
    wrapper = mount(NuxtPicture, {
      propsData: {
        loading: 'lazy',
        width: 200,
        height: 200,
        sizes: '200,500:500,900:900',
        src
      }
    })
  })

  it('Matches snapshot', () => {
    expect(wrapper.html()).toMatchInlineSnapshot(`
      "<picture>
        <source type=\\"image/webp\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_webp&s_500x500/image.png 500w, /_ipx/f_webp&s_900x900/image.png 900w, /_ipx/f_webp&s_1000x1000/image.png 1000w, /_ipx/f_webp&s_1800x1800/image.png 1800w\\"><img width=\\"200\\" height=\\"200\\" data-nuxt-pic=\\"\\" src=\\"/_ipx/f_png&s_1800x1800/image.png\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_png&s_500x500/image.png 500w, /_ipx/f_png&s_900x900/image.png 900w, /_ipx/f_png&s_1000x1000/image.png 1000w, /_ipx/f_png&s_1800x1800/image.png 1800w\\">
      </picture>"
    `)
  })

  it.todo('alt attribute is generated')

  it('props.src is picked up by getImage()', () => {
    [['source', 'srcset', '/_ipx/f_webp&s_500x500/image.png'], ['img', 'src']].forEach(([element, attribute, customSrc]) => {
      const domSrc = wrapper.find(element).element.getAttribute(attribute)
      expect(domSrc).toContain(customSrc || src)
    })
  })

  it('renders webp image source', () => {
    expect(wrapper.find('[type="image/webp"]').exists()).toBe(true)
  })

  it('props.src is reactive', async () => {
    const newSource = '/image.jpeg'
    wrapper.setProps({ src: newSource })

    await nextTick()

    ;[['source', 'srcset', '/_ipx/f_webp&s_500x500/image.jpeg'], ['img', 'src']].forEach(([element, attribute, src]) => {
      const domSrc = wrapper.find(element).element.getAttribute(attribute)
      expect(domSrc).toContain(src || newSource)
    })
  })

  it('sizes', () => {
    const sizes = wrapper.find('source').element.getAttribute('sizes')
    expect(sizes).toBe('(max-width: 500px) 500px, 900px')
  })

  it('renders src when svg is passed', () => {
    const wrapper = mount(NuxtPicture, {
      propsData: {
        src: '/image.svg'
      }
    })
    expect(wrapper.html()).toMatchInlineSnapshot('"<picture><img data-nuxt-pic=\\"\\" src=\\"/image.svg\\"></picture>"')
  })

  it('encodes characters', () => {
    const img = mount(NuxtPicture, {
      propsData: {
        loading: 'lazy',
        width: 200,
        height: 200,
        sizes: '200,500:500,900:900',
        src: '/汉字.png'
      }
    })
    expect(img.html()).toMatchInlineSnapshot(`
      "<picture>
        <source type=\\"image/webp\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_webp&s_500x500/%E6%B1%89%E5%AD%97.png 500w, /_ipx/f_webp&s_900x900/%E6%B1%89%E5%AD%97.png 900w, /_ipx/f_webp&s_1000x1000/%E6%B1%89%E5%AD%97.png 1000w, /_ipx/f_webp&s_1800x1800/%E6%B1%89%E5%AD%97.png 1800w\\"><img width=\\"200\\" height=\\"200\\" data-nuxt-pic=\\"\\" src=\\"/_ipx/f_png&s_1800x1800/%E6%B1%89%E5%AD%97.png\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_png&s_500x500/%E6%B1%89%E5%AD%97.png 500w, /_ipx/f_png&s_900x900/%E6%B1%89%E5%AD%97.png 900w, /_ipx/f_png&s_1000x1000/%E6%B1%89%E5%AD%97.png 1000w, /_ipx/f_png&s_1800x1800/%E6%B1%89%E5%AD%97.png 1800w\\">
      </picture>"
    `)
  })
})

@hartmut-co-uk hartmut-co-uk marked this pull request as ready for review June 9, 2023 22:22
@danielroe danielroe changed the title Add densities - addition params for device with DevicePixelRatio > 1 feat: support densities for devices with DevicePixelRatio > 1 Jun 9, 2023
- deep comparison of array elements using `JSON.stringify`
src/runtime/image.ts Outdated Show resolved Hide resolved
src/runtime/image.ts Outdated Show resolved Hide resolved
Copy link
Member

@danielroe danielroe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you all for amazing work on this. ❤️

Merging but please don't consider the API stabilised until next RC - @pi0 has yet to review and may have some helpful changes to make.

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

Successfully merging this pull request may close these issues.

Responsive image sizing
6 participants