Skip to content

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

Merged
danielroe merged 18 commits into
nuxt:mainfrom
Mihanik71:densities
Jun 10, 2023
Merged

feat: support densities for devices with DevicePixelRatio > 1#769
danielroe merged 18 commits into
nuxt:mainfrom
Mihanik71:densities

Conversation

@Mihanik71

@Mihanik71 Mihanik71 commented Mar 14, 2023

Copy link
Copy Markdown
Contributor

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

netlify Bot commented Mar 14, 2023

Copy link
Copy Markdown

👷 Deploy request for nuxt-image pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit ee1ece6

@ghost

ghost commented Mar 14, 2023

Copy link
Copy Markdown

Live Preview ready!

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

@simonkuran

Copy link
Copy Markdown

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
Copy Markdown
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
Copy Markdown
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

@danielroe danielroe left a comment

Copy link
Copy Markdown
Member

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

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

Copy link
Copy Markdown
Collaborator

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..

Comment thread docs/content/5.configuration.md Outdated
Comment thread docs/content/5.configuration.md
Comment thread src/runtime/components/nuxt-img.ts Outdated
Comment thread src/runtime/utils/index.ts Outdated
Comment thread src/runtime/image.ts Outdated
Comment thread src/runtime/image.ts Outdated
Comment thread src/runtime/image.ts
Comment thread src/runtime/image.ts Outdated
Comment thread src/runtime/image.ts Outdated

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
Copy Markdown
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` ?!?

Comment thread src/module.ts Outdated
@hartmut-co-uk

Copy link
Copy Markdown
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
@hartmut-co-uk
hartmut-co-uk requested a review from danielroe June 9, 2023 22:22
Comment thread src/runtime/providers/directus.ts Outdated
Comment thread docs/content/5.configuration.md Outdated
Comment thread src/runtime/components/nuxt-img.ts
@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
@danielroe
danielroe requested a review from hartmut-co-uk June 9, 2023 22:48
- deep comparison of array elements using `JSON.stringify`
Comment thread src/runtime/image.ts Outdated
Comment thread src/runtime/image.ts Outdated

@danielroe danielroe left a comment

Copy link
Copy Markdown
Member

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