Repository: nuxt/image
Pull request #884 (2023-06-21 08:27:25)

feat: allow setting global `quality` defaults in config
Hi there 👋 

This PR allows setting the quality of the generated images in the module configuration.

Thank you very much for the amazing work 🚀 

nuxt-studio[bot]: 
### <span aria-hidden="true">✅</span> Live Preview ready!

| Name | Edit | Preview | Latest Commit |
| :--- | :----- | :------ | :------ |
| **Image** | [Edit on Studio ↗︎](https://nuxt.studio/@nuxt/image/studio/content?refId=%23884) | [View Live Preview](https://v1.image.nuxtjs.org?preview=9429ed81bfe48d911dd789b6c67da447) | e806b0417be57a7c158b823deb23e8ee6f3bfb1c |

<!-- This comment is generated by [Nuxt Studio](https://nuxt.studio) -->
<!-- Do not remove or edit this comment. -->

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy request for *nuxt-image* pending review.
[Visit the deploys page to approve it](https://app.netlify.com/sites/nuxt-image/deploys)

|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | e806b0417be57a7c158b823deb23e8ee6f3bfb1c |
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/nuxt/image/pull/884?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt) Report
Patch coverage: **`100.00`**% and project coverage change: **`+0.05`** :tada:
> Comparison is base [(`919d9fd`)](https://app.codecov.io/gh/nuxt/image/commit/919d9fdadd39e8b7a4aa662bc785a4c5619be29a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt) 89.71% compared to head [(`e806b04`)](https://app.codecov.io/gh/nuxt/image/pull/884?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt) 89.76%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #884      +/-   ##
==========================================
+ Coverage   89.71%   89.76%   +0.05%     
==========================================
  Files          43       43              
  Lines        2770     2775       +5     
  Branches      311      312       +1     
==========================================
+ Hits         2485     2491       +6     
+ Misses        284      283       -1     
  Partials        1        1              
```


| [Impacted Files](https://app.codecov.io/gh/nuxt/image/pull/884?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt) | Coverage Δ | |
|---|---|---|
| [src/module.ts](https://app.codecov.io/gh/nuxt/image/pull/884?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt#diff-c3JjL21vZHVsZS50cw==) | `91.61% <100.00%> (+0.70%)` | :arrow_up: |
| [src/runtime/components/\_base.ts](https://app.codecov.io/gh/nuxt/image/pull/884?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt#diff-c3JjL3J1bnRpbWUvY29tcG9uZW50cy9fYmFzZS50cw==) | `100.00% <100.00%> (ø)` | |
| [src/types/image.ts](https://app.codecov.io/gh/nuxt/image/pull/884?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt#diff-c3JjL3R5cGVzL2ltYWdlLnRz) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/nuxt/image/pull/884?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nuxt).

danielroe: You are a star - thank you ❤️ 
Tragio: > Great! The solution should also apply `quality` at module config level to `<nuxt-picture>`, right?
> 
> Could you please also add tests to `test/unit/picture.test.ts`?

Thank you very much 🙏  I only added one test, should I add all the tests that I did for `nuxt-img`? Just don't want to be redundant, since all is being [passed to `nuxt-picture`](https://github.com/nuxt/image/blob/main/src/runtime/components/nuxt-picture.ts#L19). If you want I can do it without any problem 🙂 
hartmut-co-uk: Fair enough, I think this is good to go! 💪 

Patch:
diff --git a/docs/content/5.configuration.md b/docs/content/5.configuration.md
index c03ed06..b11c473 100644
--- a/docs/content/5.configuration.md
+++ b/docs/content/5.configuration.md
@@ -31,6 +31,20 @@ export default defineNuxtConfig({
 })
 ```
 
+## `quality`
+
+The quality for the generated image(s).
+
+You can also override this option at the component level by using the [`quality` prop.](/components/nuxt-img#quality)
+
+```ts [nuxt.config.ts]
+export default defineNuxtConfig({
+  image: {
+    quality: 80,
+  }
+})
+```
+
 ## `format`
 
 Default: `['webp']`
diff --git a/src/module.ts b/src/module.ts
index d62790d..3e820ab 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -18,6 +18,7 @@ export interface ModuleOptions extends ImageProviders {
   providers: { [name: string]: InputProvider | any } & ImageProviders
   densities: number[]
   format: CreateImageOptions['format']
+  quality?: CreateImageOptions['quality'],
   [key: string]: any
 }
 
@@ -83,7 +84,8 @@ export default defineNuxtModule<ModuleOptions>({
       'domains',
       'alias',
       'densities',
-      'format'
+      'format',
+      'quality'
     ])
 
     const providers = await resolveProviders(nuxt, options)
diff --git a/src/runtime/components/_base.ts b/src/runtime/components/_base.ts
index 149fe3f..c4f7fb2 100644
--- a/src/runtime/components/_base.ts
+++ b/src/runtime/components/_base.ts
@@ -88,13 +88,15 @@ export const useBaseImage = (props: ExtractPropTypes<typeof baseImageProps>) =>
     }
   })
 
+  const $img = useImage()
+
   const modifiers = computed<BaseImageModifiers>(() => {
     return <BaseImageModifiers> {
       ...props.modifiers,
       width: parseSize(props.width),
       height: parseSize(props.height),
       format: props.format,
-      quality: props.quality,
+      quality: props.quality || $img.options.quality,
       background: props.background,
       fit: props.fit
     }
diff --git a/src/types/image.ts b/src/types/image.ts
index 23877bb..fd8ed68 100644
--- a/src/types/image.ts
+++ b/src/types/image.ts
@@ -45,6 +45,7 @@ export interface CreateImageOptions {
   domains: string[]
   densities: number[]
   format: string[]
+  quality?: number
 }
 
 export interface ImageInfo {


Test patch:
diff --git a/test/unit/image.test.ts b/test/unit/image.test.ts
index 608c48d..74ef506 100644
--- a/test/unit/image.test.ts
+++ b/test/unit/image.test.ts
@@ -2,8 +2,10 @@
 
 import { beforeEach, describe, it, expect, vi } from 'vitest'
 import { ComponentMountingOptions, VueWrapper, mount } from '@vue/test-utils'
-
+// @ts-expect-error virtual file
+import { imageOptions } from '#build/image-options'
 import { NuxtImg } from '#components'
+import { createImage } from '#image'
 
 describe('Renders simple image', () => {
   let wrapper: VueWrapper<any>
@@ -236,4 +238,77 @@ describe('Renders placeholded image', () => {
   })
 })
 
+describe('Renders image, applies module config', () => {
+  const nuxtApp = useNuxtApp()
+  const config = useRuntimeConfig()
+  const src = '/image.png'
+
+  beforeEach(() => {
+    delete nuxtApp._img
+  })
+
+  it('Module config .quality applies', () => {
+    nuxtApp._img = createImage({
+      ...imageOptions,
+      nuxt: {
+        baseURL: config.app.baseURL
+      },
+      quality: 75
+    })
+    const img = mount(NuxtImg, {
+      propsData: {
+        src,
+        width: 200,
+        height: 200,
+        sizes: '200,500:500,900:900'
+      }
+    })
+    expect(img.html()).toMatchInlineSnapshot(`
+      "<img src=\\"/_ipx/q_75&s_1800x1800/image.png\\" width=\\"200\\" height=\\"200\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 200px) 200px, (max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/q_75&s_200x200/image.png 200w, /_ipx/q_75&s_400x400/image.png 400w, /_ipx/q_75&s_500x500/image.png 500w, /_ipx/q_75&s_900x900/image.png 900w, /_ipx/q_75&s_1000x1000/image.png 1000w, /_ipx/q_75&s_1800x1800/image.png 1800w\\">"
+    `)
+  })
+
+  it('Module config .quality + props.quality => props.quality applies', () => {
+    nuxtApp._img = createImage({
+      ...imageOptions,
+      nuxt: {
+        baseURL: config.app.baseURL
+      },
+      quality: 75
+    })
+    const img = mount(NuxtImg, {
+      propsData: {
+        src,
+        width: 200,
+        height: 200,
+        sizes: '200,500:500,900:900',
+        quality: 90
+      }
+    })
+    expect(img.html()).toMatchInlineSnapshot(`
+      "<img src=\\"/_ipx/q_90&s_1800x1800/image.png\\" width=\\"200\\" height=\\"200\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 200px) 200px, (max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/q_90&s_200x200/image.png 200w, /_ipx/q_90&s_400x400/image.png 400w, /_ipx/q_90&s_500x500/image.png 500w, /_ipx/q_90&s_900x900/image.png 900w, /_ipx/q_90&s_1000x1000/image.png 1000w, /_ipx/q_90&s_1800x1800/image.png 1800w\\">"
+    `)
+  })
+
+  it('Without quality config => default image', () => {
+    nuxtApp._img = createImage({
+      ...imageOptions,
+      nuxt: {
+        baseURL: config.app.baseURL
+      }
+    })
+    const img = mount(NuxtImg, {
+      propsData: {
+        src,
+        width: 200,
+        height: 200,
+        sizes: '200,500:500,900:900'
+      }
+    })
+    expect(img.html()).toMatchInlineSnapshot(`
+      "<img src=\\"/_ipx/s_1800x1800/image.png\\" width=\\"200\\" height=\\"200\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 200px) 200px, (max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/s_200x200/image.png 200w, /_ipx/s_400x400/image.png 400w, /_ipx/s_500x500/image.png 500w, /_ipx/s_900x900/image.png 900w, /_ipx/s_1000x1000/image.png 1000w, /_ipx/s_1800x1800/image.png 1800w\\">"
+    `)
+  })
+})
+
 const mountImage = (props: ComponentMountingOptions<typeof NuxtImg>['props']) => mount(NuxtImg, { props })
diff --git a/test/unit/picture.test.ts b/test/unit/picture.test.ts
index 4ab51ea..1932dde 100644
--- a/test/unit/picture.test.ts
+++ b/test/unit/picture.test.ts
@@ -277,4 +277,28 @@ describe('Renders image, applies module config', () => {
     })
     expect(picture.html()).toMatchInlineSnapshot('"<picture><img width=\\"200\\" height=\\"200\\" data-nuxt-pic=\\"\\" src=\\"image.svg\\"></picture>"')
   })
+
+  it('Module config .quality applies', () => {
+    nuxtApp._img = createImage({
+      ...imageOptions,
+      nuxt: {
+        baseURL: config.app.baseURL
+      },
+      quality: 75
+    })
+    const picture = mount(NuxtPicture, {
+      propsData: {
+        src,
+        width: 200,
+        height: 200,
+        sizes: '200,500:500,900:900'
+      }
+    })
+
+    expect(picture.html()).toMatchInlineSnapshot(`
+      "<picture>
+        <source type=\\"image/webp\\" sizes=\\"(max-width: 200px) 200px, (max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_webp&q_75&s_200x200/image.png 200w, /_ipx/f_webp&q_75&s_400x400/image.png 400w, /_ipx/f_webp&q_75&s_500x500/image.png 500w, /_ipx/f_webp&q_75&s_900x900/image.png 900w, /_ipx/f_webp&q_75&s_1000x1000/image.png 1000w, /_ipx/f_webp&q_75&s_1800x1800/image.png 1800w\\"><img width=\\"200\\" height=\\"200\\" data-nuxt-pic=\\"\\" src=\\"/_ipx/f_png&q_75&s_1800x1800/image.png\\" sizes=\\"(max-width: 200px) 200px, (max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_png&q_75&s_200x200/image.png 200w, /_ipx/f_png&q_75&s_400x400/image.png 400w, /_ipx/f_png&q_75&s_500x500/image.png 500w, /_ipx/f_png&q_75&s_900x900/image.png 900w, /_ipx/f_png&q_75&s_1000x1000/image.png 1000w, /_ipx/f_png&q_75&s_1800x1800/image.png 1800w\\">
+      </picture>"
+    `)
+  })
 })