Repository: nuxt/image (ts)

Issue:
Multiple formats
Avif is better than webp, but some of modern browser doesn't support avif yet, like safari.

Would be great if we could use multiple formats, something like:

```vue
 <nuxt-picture
    src="image.png"
    provider="static"
    :formats="['avif','webp']"
/>


 <nuxt-picture
    src="image.png"
    provider="static"
    :formats="['svg']"
/>
 ```

then returns 

```vue
<picture>
    <source srcset="image.avif" type="image/avif">
    <source srcset="image.webp" type="image/webp">
    <source srcset="image.png" type="image/png">
    <img src="image.png">
</picture>
```

this way, all modern browser will have the best options to display

Pull request description:
feat(nuxt-picture): support multiple image formats
This PR should resolve #451.
I'm open for any kind of feedback or improvement.

Patch:
diff --git a/docs/content/3.components/1.nuxt-img.md b/docs/content/3.components/1.nuxt-img.md
index aa2bff4..c564505 100644
--- a/docs/content/3.components/1.nuxt-img.md
+++ b/docs/content/3.components/1.nuxt-img.md
@@ -67,6 +67,22 @@ This a space-separated list of screen size/width pairs. You can [see a list of t
   />
 ```
 
+### `densities`
+
+To generate special versions of images for screens with increased pixel density.
+
+**Example:**
+
+```html
+<nuxt-img
+  src="/logos/nuxt.png"
+  height="50"
+  densities="x1 x2"
+  />
+<!-- <img src="/_ipx/w_50/logos/nuxt.png" 
+    srcset="/_ipx/w_100/logos/nuxt.png x2"/> -->
+```
+
 ### `provider`
 
 Use other provider instead of default [provider option](/configuration#provider) specified in `nuxt.config`
diff --git a/docs/content/3.components/2.nuxt-picture.md b/docs/content/3.components/2.nuxt-picture.md
index 26b29ff..73c473f 100644
--- a/docs/content/3.components/2.nuxt-picture.md
+++ b/docs/content/3.components/2.nuxt-picture.md
@@ -14,6 +14,16 @@ Learn more about the [`<picture>` tag on MDN](https://developer.mozilla.org/en-U
  See props supported by[`<nuxt-img>`](/components/nuxt-img#props)</a>
 ::
 
+### `format`
+
+Format on pictures can be used to serve images in multiple formats. A legacy format will be generated automatically. So in the example below avif, webp and png would be generated. They will be added in the same order they are added to the format attribute.
+
+```html
+<nuxt-picture format="avif,webp" src="/nuxt-icon.png" ... />
+```
+
+Available formats are `webp`, `avif`, `jpeg`, `jpg`, `png` and `gif`. If the format is not specified, it will respect the default image format.
+
 ### `legacyFormat`
 
 Format used for fallback. Default is conditional:
diff --git a/docs/content/5.configuration.md b/docs/content/5.configuration.md
index da8d2bf..2c5db20 100644
--- a/docs/content/5.configuration.md
+++ b/docs/content/5.configuration.md
@@ -154,6 +154,22 @@ export default defineNuxtConfig({
 })
 ```
 
+## `densities`
+
+Default: `[1, 2]`
+
+Specify a value to work with `devicePixelRatio` > 1 (these are devices with retina display and others). You must specify for which `devicePixelRatio` values you want to adapt images.
+
+You can [read more about `devicePixelRatio` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio).
+
+```ts [nuxt.config.ts]
+export default defineNuxtConfig({
+  image: {
+    densities: [1, 2, 3],
+  }
+})
+```
+
 ## `staticFilename`
 
 You can use this option to change filename and location for the static image generation.
diff --git a/package.json b/package.json
index dccdd5f..299605a 100755
--- a/package.json
+++ b/package.json
@@ -42,7 +42,7 @@
     "@nuxt/module-builder": "0.4.0",
     "@nuxt/test-utils": "^3.5.3",
     "@nuxtjs/eslint-config-typescript": "^12.0.0",
-    "@types/node": "^18.16.16",
+    "@types/node": "^18.16.17",
     "@vitest/coverage-c8": "^0.32.0",
     "@vue/test-utils": "^2.3.2",
     "changelogen": "^0.5.3",
@@ -52,7 +52,7 @@
     "jiti": "1.18.2",
     "nuxt": "^3.5.3",
     "nuxt-vitest": "^0.8.5",
-    "playwright": "^1.34.3",
+    "playwright": "^1.35.0",
     "typescript": "5.1.3",
     "vitest": "^0.32.0",
     "vue-tsc": "^1.6.5"
diff --git a/playground/pages/picture.vue b/playground/pages/picture.vue
index 9dbb492..64d32cf 100644
--- a/playground/pages/picture.vue
+++ b/playground/pages/picture.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <nuxt-picture src="/images/colors.jpg" width="500" height="500" @load="isLoaded = true" />
+    <nuxt-picture src="/images/colors.jpg" format="avif,webp" width="500" height="500" @load="isLoaded = true" />
     <p>Received onLoad event: {{ isLoaded }}</p>
   </div>
 </template>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 60dd3b7..4b8ff0a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -55,13 +55,13 @@ importers:
         version: 0.4.0(@nuxt/kit@3.5.3)(nuxi@3.5.3)
       '@nuxt/test-utils':
         specifier: ^3.5.3
-        version: 3.5.3(playwright@1.34.3)(rollup@3.23.0)(vitest@0.32.0)(vue@3.3.4)
+        version: 3.5.3(playwright@1.35.0)(rollup@3.23.0)(vitest@0.32.0)(vue@3.3.4)
       '@nuxtjs/eslint-config-typescript':
         specifier: ^12.0.0
         version: 12.0.0(eslint@8.42.0)(typescript@5.1.3)
       '@types/node':
-        specifier: ^18.16.16
-        version: 18.16.16
+        specifier: ^18.16.17
+        version: 18.16.17
       '@vitest/coverage-c8':
         specifier: ^0.32.0
         version: 0.32.0(vitest@0.32.0)
@@ -82,19 +82,19 @@ importers:
         version: 1.18.2
       nuxt:
         specifier: ^3.5.3
-        version: 3.5.3(@types/node@18.16.16)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)
+        version: 3.5.3(@types/node@18.16.17)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)
       nuxt-vitest:
         specifier: ^0.8.5
         version: 0.8.5(@vitejs/plugin-vue-jsx@3.0.1)(@vitejs/plugin-vue@4.2.3)(rollup@3.23.0)(vite@4.3.9)(vitest@0.32.0)(vue-router@4.2.2)(vue@3.3.4)
       playwright:
-        specifier: ^1.34.3
-        version: 1.34.3
+        specifier: ^1.35.0
+        version: 1.35.0
       typescript:
         specifier: 5.1.3
         version: 5.1.3
       vitest:
         specifier: ^0.32.0
-        version: 0.32.0(playwright@1.34.3)
+        version: 0.32.0(playwright@1.35.0)
       vue-tsc:
         specifier: ^1.6.5
         version: 1.6.5(typescript@5.1.3)
@@ -109,7 +109,7 @@ importers:
         version: 0.2.1(rollup@3.23.0)
       nuxt:
         specifier: ^3.5.3
-        version: 3.5.3(@types/node@18.16.16)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)
+        version: 3.5.3(@types/node@18.16.17)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)
 
   example:
     devDependencies:
@@ -118,7 +118,7 @@ importers:
         version: link:..
       nuxt:
         specifier: latest
-        version: 3.5.3(@types/node@18.16.16)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)
+        version: 3.5.3(@types/node@18.16.17)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)
 
   playground:
     devDependencies:
@@ -127,7 +127,7 @@ importers:
         version: link:..
       nuxt:
         specifier: latest
-        version: 3.5.3(@types/node@18.16.16)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)
+        version: 3.5.3(@types/node@18.16.17)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)
 
 packages:
 
@@ -1085,7 +1085,7 @@ packages:
       - supports-color
     dev: true
 
-  /@nuxt/test-utils@3.5.3(playwright@1.34.3)(rollup@3.23.0)(vitest@0.32.0)(vue@3.3.4):
+  /@nuxt/test-utils@3.5.3(playwright@1.35.0)(rollup@3.23.0)(vitest@0.32.0)(vue@3.3.4):
     resolution: {integrity: sha512-ujkOrn3Qvd+6TREsg4h6Vm1YbPgwl70qh9s6jQiEIGhaH9wqXUUgbHEdWelUMeRO+jbV1X4rtd7Sdww/rfR5lA==}
     engines: {node: ^14.18.0 || >=16.10.0}
     peerDependencies:
@@ -1109,9 +1109,9 @@ packages:
       get-port-please: 3.0.1
       ofetch: 1.0.1
       pathe: 1.1.1
-      playwright: 1.34.3
+      playwright: 1.35.0
       ufo: 1.1.2
-      vitest: 0.32.0(playwright@1.34.3)
+      vitest: 0.32.0(playwright@1.35.0)
       vue: 3.3.4
     transitivePeerDependencies:
       - rollup
@@ -1122,7 +1122,7 @@ packages:
     resolution: {integrity: sha512-PjVETP7+iZXAs5Q8O4ivl4t6qjWZMZqwiTVogUXHoHGZZcw7GZW3u3tzfYfE1HbzyYJfr236IXqQ02MeR8Fz2w==}
     dev: true
 
-  /@nuxt/vite-builder@3.5.3(@types/node@18.16.16)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)(vue@3.3.4):
+  /@nuxt/vite-builder@3.5.3(@types/node@18.16.17)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)(vue@3.3.4):
     resolution: {integrity: sha512-7zEKpGh3iWGRDwbWUa8eRxdLMxZtPzetelmdmXPjtYKGwUebZOcBhpeJ+VgJKOIf4OEj9E7BZS+it/Ji9UG9qw==}
     engines: {node: ^14.18.0 || >=16.10.0}
     peerDependencies:
@@ -1159,8 +1159,8 @@ packages:
       strip-literal: 1.0.1
       ufo: 1.1.2
       unplugin: 1.3.1
-      vite: 4.3.9(@types/node@18.16.16)
-      vite-node: 0.31.4(@types/node@18.16.16)
+      vite: 4.3.9(@types/node@18.16.17)
+      vite-node: 0.31.4(@types/node@18.16.17)
       vite-plugin-checker: 0.6.0(eslint@8.42.0)(typescript@5.1.3)(vite@4.3.9)(vue-tsc@1.6.5)
       vue: 3.3.4
       vue-bundle-renderer: 1.0.3
@@ -1454,7 +1454,7 @@ packages:
   /@types/http-proxy@1.17.11:
     resolution: {integrity: sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==}
     dependencies:
-      '@types/node': 18.16.16
+      '@types/node': 18.16.17
     dev: true
 
   /@types/istanbul-lib-coverage@2.0.4:
@@ -1479,8 +1479,8 @@ packages:
     resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
     dev: true
 
-  /@types/node@18.16.16:
-    resolution: {integrity: sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g==}
+  /@types/node@18.16.17:
+    resolution: {integrity: sha512-QAkjjRA1N7gPJeAP4WLXZtYv6+eMXFNviqktCDt4GLcmCugMr5BcRHfkOjCQzvCsnMp+L79a54zBkbw356xv9Q==}
     dev: true
 
   /@types/normalize-package-data@2.4.1:
@@ -1714,7 +1714,7 @@ packages:
       '@babel/core': 7.21.8
       '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.21.8)
       '@vue/babel-plugin-jsx': 1.1.1(@babel/core@7.21.8)
-      vite: 4.3.9(@types/node@18.16.16)
+      vite: 4.3.9(@types/node@18.16.17)
       vue: 3.3.4
     transitivePeerDependencies:
       - supports-color
@@ -1727,7 +1727,7 @@ packages:
       vite: ^4.0.0
       vue: ^3.2.25
     dependencies:
-      vite: 4.3.9(@types/node@18.16.16)
+      vite: 4.3.9(@types/node@18.16.17)
       vue: 3.3.4
     dev: true
 
@@ -1741,7 +1741,7 @@ packages:
       magic-string: 0.30.0
       picocolors: 1.0.0
       std-env: 3.3.3
-      vitest: 0.32.0(playwright@1.34.3)
+      vitest: 0.32.0(playwright@1.35.0)
     dev: true
 
   /@vitest/expect@0.32.0:
@@ -2028,7 +2028,7 @@ packages:
       '@vueuse/core': 10.1.2(vue@3.3.4)
       '@vueuse/metadata': 10.1.2
       local-pkg: 0.4.3
-      nuxt: 3.5.3(@types/node@18.16.16)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)
+      nuxt: 3.5.3(@types/node@18.16.17)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)
       vue-demi: 0.14.5(vue@3.3.4)
     transitivePeerDependencies:
       - '@vue/composition-api'
@@ -6048,8 +6048,8 @@ packages:
       get-port-please: 3.0.1
       perfect-debounce: 1.0.0
       std-env: 3.3.3
-      vite: 4.3.9(@types/node@18.16.16)
-      vitest: 0.32.0(playwright@1.34.3)
+      vite: 4.3.9(@types/node@18.16.17)
+      vitest: 0.32.0(playwright@1.35.0)
       vitest-environment-nuxt: 0.8.5(rollup@3.23.0)(vitest@0.32.0)(vue-router@4.2.2)(vue@3.3.4)
     transitivePeerDependencies:
       - rollup
@@ -6058,7 +6058,7 @@ packages:
       - vue-router
     dev: true
 
-  /nuxt@3.5.3(@types/node@18.16.16)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5):
+  /nuxt@3.5.3(@types/node@18.16.17)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5):
     resolution: {integrity: sha512-fG39BZ5N5ATtmx2vuxN8APQPSlSsCDpfkJ0k581gMc7eFztqrBzPncZX5w3RQLW7AiGBE2yYEfqiwC6AVODBBg==}
     engines: {node: ^14.18.0 || >=16.10.0}
     hasBin: true
@@ -6074,8 +6074,8 @@ packages:
       '@nuxt/schema': 3.5.3(rollup@3.23.0)
       '@nuxt/telemetry': 2.2.0(rollup@3.23.0)
       '@nuxt/ui-templates': 1.1.1
-      '@nuxt/vite-builder': 3.5.3(@types/node@18.16.16)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)(vue@3.3.4)
-      '@types/node': 18.16.16
+      '@nuxt/vite-builder': 3.5.3(@types/node@18.16.17)(eslint@8.42.0)(rollup@3.23.0)(typescript@5.1.3)(vue-tsc@1.6.5)(vue@3.3.4)
+      '@types/node': 18.16.17
       '@unhead/ssr': 1.1.27
       '@unhead/vue': 1.1.27(vue@3.3.4)
       '@vue/shared': 3.3.4
@@ -6519,19 +6519,19 @@ packages:
     engines: {node: '>=10'}
     dev: true
 
-  /playwright-core@1.34.3:
-    resolution: {integrity: sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==}
-    engines: {node: '>=14'}
+  /playwright-core@1.35.0:
+    resolution: {integrity: sha512-muMXyPmIx/2DPrCHOD1H1ePT01o7OdKxKj2ebmCAYvqhUy+Y1bpal7B0rdoxros7YrXI294JT/DWw2LqyiqTPA==}
+    engines: {node: '>=16'}
     hasBin: true
     dev: true
 
-  /playwright@1.34.3:
-    resolution: {integrity: sha512-UOOVE4ZbGfGkP1KVqWTdXOmm8Pw2pBhfbmlqKMkpiRCQjL5W+J+xRQXpgutFr0iM4pWl8g0GyyASMsqjQfFohw==}
-    engines: {node: '>=14'}
+  /playwright@1.35.0:
+    resolution: {integrity: sha512-xhFhsoBmKPQfj3dM+HbIiFVlqRCZp2rwdJd/QFd9YBuidabo3TkVv0iuxPQ4vZoMwtSI7qzjY93f5ohdC97hww==}
+    engines: {node: '>=16'}
     hasBin: true
     requiresBuild: true
     dependencies:
-      playwright-core: 1.34.3
+      playwright-core: 1.35.0
     dev: true
 
   /pluralize@8.0.0:
@@ -8426,7 +8426,7 @@ packages:
       vfile-message: 3.1.4
     dev: true
 
-  /vite-node@0.31.4(@types/node@18.16.16):
+  /vite-node@0.31.4(@types/node@18.16.17):
     resolution: {integrity: sha512-uzL377GjJtTbuc5KQxVbDu2xfU/x0wVjUtXQR2ihS21q/NK6ROr4oG0rsSkBBddZUVCwzfx22in76/0ZZHXgkQ==}
     engines: {node: '>=v14.18.0'}
     hasBin: true
@@ -8436,7 +8436,7 @@ packages:
       mlly: 1.3.0
       pathe: 1.1.1
       picocolors: 1.0.0
-      vite: 4.3.9(@types/node@18.16.16)
+      vite: 4.3.9(@types/node@18.16.17)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -8447,7 +8447,7 @@ packages:
       - terser
     dev: true
 
-  /vite-node@0.32.0(@types/node@18.16.16):
+  /vite-node@0.32.0(@types/node@18.16.17):
     resolution: {integrity: sha512-220P/y8YacYAU+daOAqiGEFXx2A8AwjadDzQqos6wSukjvvTWNqleJSwoUn0ckyNdjHIKoxn93Nh1vWBqEKr3Q==}
     engines: {node: '>=v14.18.0'}
     hasBin: true
@@ -8457,7 +8457,7 @@ packages:
       mlly: 1.3.0
       pathe: 1.1.1
       picocolors: 1.0.0
-      vite: 4.3.9(@types/node@18.16.16)
+      vite: 4.3.9(@types/node@18.16.17)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -8514,7 +8514,7 @@ packages:
       strip-ansi: 6.0.1
       tiny-invariant: 1.3.1
       typescript: 5.1.3
-      vite: 4.3.9(@types/node@18.16.16)
+      vite: 4.3.9(@types/node@18.16.17)
       vscode-languageclient: 7.0.0
       vscode-languageserver: 7.0.0
       vscode-languageserver-textdocument: 1.0.7
@@ -8522,7 +8522,7 @@ packages:
       vue-tsc: 1.6.5(typescript@5.1.3)
     dev: true
 
-  /vite@4.3.9(@types/node@18.16.16):
+  /vite@4.3.9(@types/node@18.16.17):
     resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==}
     engines: {node: ^14.18.0 || >=16.0.0}
     hasBin: true
@@ -8547,7 +8547,7 @@ packages:
       terser:
         optional: true
     dependencies:
-      '@types/node': 18.16.16
+      '@types/node': 18.16.17
       esbuild: 0.17.19
       postcss: 8.4.24
       rollup: 3.23.0
@@ -8571,7 +8571,7 @@ packages:
       magic-string: 0.30.0
       ofetch: 1.0.1
       unenv: 1.5.1
-      vitest: 0.32.0(playwright@1.34.3)
+      vitest: 0.32.0(playwright@1.35.0)
       vue: 3.3.4
       vue-router: 4.2.2(vue@3.3.4)
     transitivePeerDependencies:
@@ -8579,7 +8579,7 @@ packages:
       - supports-color
     dev: true
 
-  /vitest@0.32.0(playwright@1.34.3):
+  /vitest@0.32.0(playwright@1.35.0):
     resolution: {integrity: sha512-SW83o629gCqnV3BqBnTxhB10DAwzwEx3z+rqYZESehUB+eWsJxwcBQx7CKy0otuGMJTYh7qCVuUX23HkftGl/Q==}
     engines: {node: '>=v14.18.0'}
     hasBin: true
@@ -8612,7 +8612,7 @@ packages:
     dependencies:
       '@types/chai': 4.3.5
       '@types/chai-subset': 1.3.3
-      '@types/node': 18.16.16
+      '@types/node': 18.16.17
       '@vitest/expect': 0.32.0
       '@vitest/runner': 0.32.0
       '@vitest/snapshot': 0.32.0
@@ -8628,13 +8628,13 @@ packages:
       magic-string: 0.30.0
       pathe: 1.1.1
       picocolors: 1.0.0
-      playwright: 1.34.3
+      playwright: 1.35.0
       std-env: 3.3.3
       strip-literal: 1.0.1
       tinybench: 2.5.0
       tinypool: 0.5.0
-      vite: 4.3.9(@types/node@18.16.16)
-      vite-node: 0.32.0(@types/node@18.16.16)
+      vite: 4.3.9(@types/node@18.16.17)
+      vite-node: 0.32.0(@types/node@18.16.17)
       why-is-node-running: 2.2.2
     transitivePeerDependencies:
       - less
diff --git a/src/module.ts b/src/module.ts
index 130360b..ab0ee4e 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -16,6 +16,7 @@ export interface ModuleOptions extends ImageProviders {
   screens: CreateImageOptions['screens']
   internalUrl: string
   providers: { [name: string]: InputProvider | any } & ImageProviders
+  densities: number[]
   [key: string]: any
 }
 
@@ -42,7 +43,8 @@ export default defineNuxtModule<ModuleOptions>({
     },
     internalUrl: '',
     providers: {},
-    alias: {}
+    alias: {},
+    densities: [1, 2]
   }),
   meta: {
     name: '@nuxt/image',
@@ -70,13 +72,15 @@ export default defineNuxtModule<ModuleOptions>({
     if (options.provider) {
       options[options.provider] = options[options.provider] || {}
     }
+    options.densities = options.densities || []
 
     const imageOptions: Omit<CreateImageOptions, 'providers' | 'nuxt'> = pick(options, [
       'screens',
       'presets',
       'provider',
       'domains',
-      'alias'
+      'alias',
+      'densities'
     ])
 
     const providers = await resolveProviders(nuxt, options)
@@ -129,10 +133,8 @@ ${providers.map(p => `  ['${p.name}']: { provider: ${p.importName}, defaults: ${
         imageOptions.provider = options.provider = nitro.options.node ? 'ipx' : 'none'
         options[options.provider] = options[options.provider] || {}
 
-        if (options.provider === 'none') { return }
-
-        const p = await resolveProvider(nuxt, 'ipx', options.ipx as {})
-        if (!providers.some(p => p.name === 'ipx')) {
+        const p = await resolveProvider(nuxt, options.provider, options[options.provider])
+        if (!providers.some(p => p.name === options.provider)) {
           providers.push(p)
         }
         if (typeof p.setup === 'function') {
diff --git a/src/runtime/components/_base.ts b/src/runtime/components/_base.ts
index 4b1eefe..149fe3f 100644
--- a/src/runtime/components/_base.ts
+++ b/src/runtime/components/_base.ts
@@ -18,6 +18,7 @@ export const baseImageProps = {
   provider: { type: String, default: undefined },
 
   sizes: { type: [Object, String] as unknown as () => string | Record<string, any>, default: undefined },
+  densities: { type: String, default: undefined },
   preload: { type: Boolean, default: undefined },
 
   // <img> attributes
diff --git a/src/runtime/components/nuxt-img.ts b/src/runtime/components/nuxt-img.ts
index d1d3dba..14d36d4 100644
--- a/src/runtime/components/nuxt-img.ts
+++ b/src/runtime/components/nuxt-img.ts
@@ -29,6 +29,7 @@ export default defineComponent({
     const sizes = computed(() => $img.getSizes(props.src!, {
       ..._base.options.value,
       sizes: props.sizes,
+      densities: props.densities,
       modifiers: {
         ..._base.modifiers.value,
         width: parseSize(props.width),
@@ -41,6 +42,16 @@ export default defineComponent({
       if (props.sizes && (!props.placeholder || placeholderLoaded.value)) {
         attrs.sizes = sizes.value.sizes
         attrs.srcset = sizes.value.srcset
+      } else if (props.densities || $img.options.densities) {
+        attrs.srcset = $img.getDensitySet(props.src!, {
+          ..._base.options.value,
+          densities: props.densities,
+          modifiers: {
+            ..._base.modifiers.value,
+            width: parseSize(props.width),
+            height: parseSize(props.height)
+          }
+        })
       }
       return attrs
     })
diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts
index 53517fd..d1c366d 100644
--- a/src/runtime/components/nuxt-picture.ts
+++ b/src/runtime/components/nuxt-picture.ts
@@ -18,30 +18,29 @@ export default defineComponent({
     const $img = useImage()
     const _base = useBaseImage(props)
 
-    const isTransparent = computed(() => ['png', 'webp', 'gif'].includes(originalFormat.value))
+    const isTransparent = computed(() => ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value))
 
     const originalFormat = computed(() => getFileExtension(props.src))
 
-    const format = computed(() => props.format || (originalFormat.value === 'svg' ? 'svg' : 'webp'))
-
     const legacyFormat = computed(() => {
       if (props.legacyFormat) { return props.legacyFormat }
-      const formats: Record<string, string> = {
-        webp: isTransparent.value ? 'png' : 'jpeg',
-        svg: 'png'
-      }
-      return formats[format.value] || originalFormat.value
+      return isTransparent.value ? 'png' : 'jpeg'
     })
 
-    type Source = { srcset: string, src?: string, type?: string, sizes?: string }
+    type Source = { srcset?: string, src?: string, type?: string, sizes?: string }
     const sources = computed<Source[]>(() => {
-      if (format.value === 'svg') {
+      const format = props.format || (originalFormat.value === 'svg' ? 'svg' : 'webp')
+      const formats = format.split(',')
+      if (format === 'svg') {
         return [<Source>{ src: props.src }]
       }
 
-      const formats = legacyFormat.value !== format.value
-        ? [legacyFormat.value, format.value]
-        : [format.value]
+      if (!formats.includes(legacyFormat.value)) {
+        formats.push(legacyFormat.value)
+      } else {
+        formats.splice(formats.indexOf(legacyFormat.value), 1)
+        formats.push(legacyFormat.value)
+      }
 
       return formats.map((format) => {
         const { srcset, sizes, src } = $img.getSizes(props.src!, {
@@ -50,16 +49,15 @@ export default defineComponent({
           modifiers: { ..._base.modifiers.value, format }
         })
 
-        return <Source> { src, type: `image/${format}`, sizes, srcset }
+        return <Source>{ src, type: `image/${format}`, sizes, srcset }
       })
     })
+    const lastSourceIndex = computed(() => sources.value.length - 1)
 
     if (props.preload) {
-      const srcKey = sources.value?.[1] ? 1 : 0
+      const link: any = { rel: 'preload', as: 'image', imagesrcset: sources.value[0].srcset }
 
-      const link: any = { rel: 'preload', as: 'image', imagesrcset: sources.value[srcKey].srcset }
-
-      if (sources.value?.[srcKey]?.sizes) { link.imagesizes = sources.value[srcKey].sizes }
+      if (sources.value?.[0]?.sizes) { link.imagesizes = sources.value[0].sizes }
 
       useHead({ link: [link] })
     }
@@ -94,23 +92,24 @@ export default defineComponent({
       }
     })
 
-    return () => h('picture', { key: sources.value[0].src }, [
-      ...(sources.value?.[1]
-        ? [h('source', {
-            type: sources.value[1].type,
-            sizes: sources.value[1].sizes,
-            srcset: sources.value[1].srcset
-          })]
-        : []),
-      h('img', {
-        ref: imgEl,
-        ..._base.attrs.value,
-        ...process.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {},
-        ...imgAttrs,
-        src: sources.value[0].src,
-        sizes: sources.value[0].sizes,
-        srcset: sources.value[0].srcset
-      })
-    ])
+    return () =>
+      h('picture', { key: sources.value[0].src }, [
+        ...sources.value.slice(0, -1).map((source) => {
+          return h('source', {
+            type: source.type,
+            sizes: source.sizes,
+            srcset: source.srcset
+          })
+        }),
+        h('img', {
+          ref: imgEl,
+          ..._base.attrs.value,
+          ...(process.server ? { onerror: "this.setAttribute('data-error', 1)" } : {}),
+          ...imgAttrs,
+          src: sources.value[lastSourceIndex.value].src,
+          sizes: sources.value[lastSourceIndex.value].sizes,
+          srcset: sources.value[lastSourceIndex.value].srcset
+        })
+      ])
   }
 })
diff --git a/src/runtime/image.ts b/src/runtime/image.ts
index 1cd645f..fed0edf 100644
--- a/src/runtime/image.ts
+++ b/src/runtime/image.ts
@@ -2,7 +2,7 @@ import { defu } from 'defu'
 import { hasProtocol, parseURL, joinURL, withLeadingSlash } from 'ufo'
 import type { ImageOptions, ImageSizesOptions, CreateImageOptions, ResolvedImage, ImageCTX, $Img } from '../types/image'
 import { imageMeta } from './utils/meta'
-import { parseSize } from './utils'
+import { parseDensities, parseSize } from './utils'
 import { prerenderStaticImages } from './utils/prerender'
 
 export function createImage (globalOptions: CreateImageOptions) {
@@ -37,6 +37,7 @@ export function createImage (globalOptions: CreateImageOptions) {
   $img.getImage = getImage
   $img.getMeta = ((input: string, options?: ImageOptions) => getMeta(ctx, input, options)) as $Img['getMeta']
   $img.getSizes = ((input: string, options: ImageSizesOptions) => getSizes(ctx, input, options)) as $Img['getSizes']
+  $img.getDensitySet = ((input: string, options: ImageSizesOptions) => getDensitySet(ctx, input, options)) as $Img['getDensitySet']
 
   ctx.$img = $img as $Img
 
@@ -129,8 +130,11 @@ function getPreset (ctx: ImageCTX, name?: string): ImageOptions {
 function getSizes (ctx: ImageCTX, input: string, opts: ImageSizesOptions) {
   const width = parseSize(opts.modifiers?.width)
   const height = parseSize(opts.modifiers?.height)
+  const densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities
+
   const hwRatio = (width && height) ? height / width : 0
-  const variants = []
+  const sizeVariants = []
+  const srcsetVariants = []
 
   const sizes: Record<string, string> = {}
 
@@ -163,25 +167,85 @@ function getSizes (ctx: ImageCTX, input: string, opts: ImageSizesOptions) {
       _cWidth = Math.round((_cWidth / 100) * screenMaxWidth)
     }
     const _cHeight = hwRatio ? Math.round(_cWidth * hwRatio) : height
-    variants.push({
-      width: _cWidth,
+
+    // add size variant with 'media'
+    sizeVariants.push({
       size,
       screenMaxWidth,
-      media: `(max-width: ${screenMaxWidth}px)`,
-      src: ctx.$img!(input, { ...opts.modifiers, width: _cWidth, height: _cHeight }, opts)
+      media: `(max-width: ${screenMaxWidth}px)`
     })
+
+    if (densities) {
+      // add srcset variants for all densities (for current 'size' processed)
+      for (const density of densities) {
+        srcsetVariants.push({
+          width: _cWidth * density,
+          src: ctx.$img!(input, { ...opts.modifiers, width: _cWidth * density, height: _cHeight ? _cHeight * density : undefined }, opts)
+        })
+      }
+    }
+  }
+
+  sizeVariants.sort((v1, v2) => v1.screenMaxWidth - v2.screenMaxWidth)
+
+  // for last size variant, always remove `media` (convention)
+  if (sizeVariants[sizeVariants.length - 1]) {
+    sizeVariants[sizeVariants.length - 1].media = ''
+  }
+
+  // de-duplicate size variants (by key `media`)
+  let previousMedia = null
+  for (let i = sizeVariants.length - 1; i >= 0; i--) {
+    const sizeVariant = sizeVariants[i]
+    if (sizeVariant.media === previousMedia) {
+      sizeVariants.splice(i, 1)
+    }
+    previousMedia = sizeVariant.media
   }
 
-  variants.sort((v1, v2) => v1.screenMaxWidth - v2.screenMaxWidth)
+  srcsetVariants.sort((v1, v2) => v1.width - v2.width)
 
-  const defaultVar = variants[variants.length - 1]
-  if (defaultVar) {
-    defaultVar.media = ''
+  // de-duplicate srcset variants (by key `width`)
+  let previousWidth = null
+  for (let i = srcsetVariants.length - 1; i >= 0; i--) {
+    const sizeVariant = srcsetVariants[i]
+    if (sizeVariant.width === previousWidth) {
+      srcsetVariants.splice(i, 1)
+    }
+    previousWidth = sizeVariant.width
   }
 
+  // use last (:= largest) srcset variant as the image `src`
+  const defaultVar = srcsetVariants[srcsetVariants.length - 1]
+
   return {
-    sizes: variants.map(v => `${v.media ? v.media + ' ' : ''}${v.size}`).join(', '),
-    srcset: variants.map(v => `${v.src} ${v.width}w`).join(', '),
+    sizes: sizeVariants.map(v => `${v.media ? v.media + ' ' : ''}${v.size}`).join(', '),
+    srcset: srcsetVariants.map(v => `${v.src} ${v.width}w`).join(', '),
     src: defaultVar?.src
   }
 }
+
+function getDensitySet (ctx: ImageCTX, input: string, opts: ImageSizesOptions): string | undefined {
+  const densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities
+  if (densities.length === 0) {
+    return undefined
+  }
+
+  const srcsetVariants: Array<{ density: string, src: string }> = []
+
+  for (const density of densities) {
+    const modifiers = { ...opts.modifiers }
+    if (modifiers.width) {
+      modifiers.width = modifiers.width * density
+    }
+    if (modifiers.height) {
+      modifiers.height = modifiers.height * density
+    }
+    srcsetVariants.push({
+      density: `${density}x`,
+      src: ctx.$img!(input, modifiers, opts)
+    })
+  }
+
+  return srcsetVariants.map(v => `${v.src} ${v.density}`).join(', ')
+}
diff --git a/src/runtime/providers/directus.ts b/src/runtime/providers/directus.ts
index 96756fb..cdc171b 100644
--- a/src/runtime/providers/directus.ts
+++ b/src/runtime/providers/directus.ts
@@ -8,8 +8,10 @@ export const operationsGenerator = createOperationsGenerator({
 
 export const getImage: ProviderGetImage = (src, { modifiers = {}, baseURL } = {}) => {
   // Separating the transforms from the rest of the modifiers
-  const transforms = modifiers.transforms
-  if (transforms && transforms.length > 0) {
+  let transforms = modifiers.transforms
+  if (transforms && Array.isArray(transforms) && transforms.length > 0) {
+    // de-duplicate (can get multiplied when having >1 densities configured)
+    transforms = Array.from(new Set(transforms.map(obj => JSON.stringify(obj)))).map(value => JSON.parse(value))
     // We stringify and encode in URL the list of lists, then apply it back to the modifiers
     modifiers.transforms = new URLSearchParams(JSON.stringify(transforms)).toString().replace(/=+$/, '') as unknown as (string | number)[][]
   }
diff --git a/src/runtime/utils/index.ts b/src/runtime/utils/index.ts
index 7dce406..3c7ddb6 100644
--- a/src/runtime/utils/index.ts
+++ b/src/runtime/utils/index.ts
@@ -91,3 +91,14 @@ export function parseSize (input: string | number | undefined = '') {
     }
   }
 }
+
+export function parseDensities (input: string | undefined = ''): number[] {
+  if (input === undefined || !input.length) {
+    return []
+  }
+
+  const densities = input.split(' ').map(size => parseInt(size.replace('x', '')))
+
+  // de-duplicate and return
+  return densities.filter((value, index) => densities.indexOf(value) === index)
+}
diff --git a/src/types/image.ts b/src/types/image.ts
index 964a4c1..3da7328 100644
--- a/src/types/image.ts
+++ b/src/types/image.ts
@@ -9,6 +9,7 @@ export interface ImageModifiers {
 export interface ImageOptions {
   provider?: string
   preset?: string
+  densities?: string
   modifiers?: Partial<ImageModifiers>
   [key: string]: any
 }
@@ -42,6 +43,7 @@ export interface CreateImageOptions {
   screens: Record<string, number>
   alias: Record<string, string>
   domains: string[]
+  densities: number[]
 }
 
 export interface ImageInfo {


Test patch:
diff --git a/test/e2e/generate.test.ts b/test/e2e/generate.test.ts
index 1805a32..5d4a43d 100644
--- a/test/e2e/generate.test.ts
+++ b/test/e2e/generate.test.ts
@@ -34,6 +34,10 @@ describe('ipx provider', () => {
         "/_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",
       ]
     `)
   })
diff --git a/test/unit/image.test.ts b/test/unit/image.test.ts
index 80d7a4b..02ea192 100644
--- a/test/unit/image.test.ts
+++ b/test/unit/image.test.ts
@@ -19,12 +19,12 @@ describe('Renders simple image', () => {
   })
 
   it('Matches snapshot', () => {
-    expect(wrapper.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_900x900/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\\">"')
+    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_900x900/image.png"')
+    expect(domSrc).toMatchInlineSnapshot('"/_ipx/s_1800x1800/image.png"')
   })
 
   it('props.src is reactive', async () => {
@@ -33,7 +33,7 @@ describe('Renders simple image', () => {
     await nextTick()
 
     const domSrc = wrapper.find('img').element.getAttribute('src')
-    expect(domSrc).toMatchInlineSnapshot('"/_ipx/s_900x900/image.jpeg"')
+    expect(domSrc).toMatchInlineSnapshot('"/_ipx/s_1800x1800/image.jpeg"')
   })
 
   it('sizes', () => {
@@ -41,6 +41,27 @@ describe('Renders simple image', () => {
     expect(sizes).toBe('(max-width: 500px) 500px, 900px')
   })
 
+  it('applies densities', () => {
+    const img = mountImage({
+      width: 200,
+      height: 300,
+      sizes: '300:300px,400:400px',
+      densities: '1x 2x 3x',
+      src: 'image.png'
+    })
+    expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1200x1800/image.png\\" width=\\"200\\" height=\\"300\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 300px) 300px, 400px\\" srcset=\\"/_ipx/s_300x450/image.png 300w, /_ipx/s_400x600/image.png 400w, /_ipx/s_600x900/image.png 600w, /_ipx/s_800x1200/image.png 800w, /_ipx/s_900x1350/image.png 900w, /_ipx/s_1200x1800/image.png 1200w\\">"')
+  })
+
+  it('de-duplicates sizes & srcset', () => {
+    const img = mountImage({
+      width: 200,
+      height: 300,
+      sizes: '200:200px,300:200px,400:400px,400:400px,500:500px,800:800px',
+      src: 'image.png'
+    })
+    expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1600x2400/image.png\\" width=\\"200\\" height=\\"300\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 200px) 200px, (max-width: 300px) 200px, (max-width: 400px) 400px, (max-width: 500px) 500px, 800px\\" srcset=\\"/_ipx/s_200x300/image.png 200w, /_ipx/s_400x600/image.png 400w, /_ipx/s_500x750/image.png 500w, /_ipx/s_800x1200/image.png 800w, /_ipx/s_1000x1500/image.png 1000w, /_ipx/s_1600x2400/image.png 1600w\\">"')
+  })
+
   it('encodes characters', () => {
     const img = mountImage({
       width: 200,
@@ -48,7 +69,7 @@ describe('Renders simple image', () => {
       sizes: '200,500:500,900:900',
       src: '/汉字.png'
     })
-    expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_900x900/%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\\">"')
+    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', () => {
@@ -58,7 +79,17 @@ describe('Renders simple image', () => {
       height: 2000,
       sizes: 'xs:100vw sm:100vw md:300px lg:350px xl:350px 2xl:350px'
     })
-    expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_350x700/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_320x640/image.png 320w, /_ipx/s_640x1280/image.png 640w, /_ipx/s_300x600/image.png 300w, /_ipx/s_350x700/image.png 350w, /_ipx/s_350x700/image.png 350w, /_ipx/s_350x700/image.png 350w\\">"')
+    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\\">"')
+  })
+
+  it('without sizes, but densities', () => {
+    const img = mountImage({
+      src: '/image.png',
+      width: 300,
+      height: 400,
+      densities: '1x 2x 3x'
+    })
+    expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_300x400/image.png\\" width=\\"300\\" height=\\"400\\" data-nuxt-img=\\"\\" srcset=\\"/_ipx/s_300x400/image.png 1x, /_ipx/s_600x800/image.png 2x, /_ipx/s_900x1200/image.png 3x\\">"')
   })
 })
 
diff --git a/test/unit/picture.test.ts b/test/unit/picture.test.ts
index 77693f3..aee7ada 100644
--- a/test/unit/picture.test.ts
+++ b/test/unit/picture.test.ts
@@ -1,7 +1,7 @@
 // @vitest-environment nuxt
 
-import { beforeEach, describe, it, expect } from 'vitest'
 import { VueWrapper, mount } from '@vue/test-utils'
+import { beforeEach, describe, expect, it } from 'vitest'
 import { NuxtPicture } from '#components'
 
 describe('Renders simple image', () => {
@@ -46,7 +46,7 @@ describe('Renders simple image', () => {
   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\\"><img width=\\"200\\" height=\\"200\\" data-nuxt-pic=\\"\\" src=\\"/_ipx/f_png&s_900x900/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\\">
+        <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>"
     `)
   })
@@ -64,6 +64,47 @@ describe('Renders simple image', () => {
     expect(wrapper.find('[type="image/webp"]').exists()).toBe(true)
   })
 
+  it('renders single format and fallback image tag', () => {
+    const img = mount(NuxtPicture, {
+      propsData: {
+        width: 200,
+        height: 200,
+        format: 'avif',
+        src
+      }
+    })
+    expect(img.find('source[type="image/avif"]').exists()).toBe(true)
+    expect(img.find('img').exists()).toBe(true)
+  })
+
+  it('renders avif, webp and fallback image tag', () => {
+    const img = mount(NuxtPicture, {
+      propsData: {
+        width: 200,
+        height: 200,
+        format: 'avif,webp',
+        src
+      }
+    })
+    expect(img.find('source[type="image/avif"]').exists()).toBe(true)
+    expect(img.find('source[type="image/webp"]').exists()).toBe(true)
+    expect(img.find('img').exists()).toBe(true)
+  })
+
+  it('renders avif, gif and fallback image tag', () => {
+    const img = mount(NuxtPicture, {
+      propsData: {
+        width: 200,
+        height: 200,
+        format: 'avif,gif',
+        src
+      }
+    })
+    expect(img.find('source[type="image/avif"]').exists()).toBe(true)
+    expect(img.find('source[type="image/gif"]').exists()).toBe(true)
+    expect(img.find('img').exists()).toBe(true)
+  })
+
   it('props.src is reactive', async () => {
     const newSource = '/image.jpeg'
     wrapper.setProps({ src: newSource })
@@ -102,8 +143,18 @@ describe('Renders simple image', () => {
     })
     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\\"><img width=\\"200\\" height=\\"200\\" data-nuxt-pic=\\"\\" src=\\"/_ipx/f_png&s_900x900/%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\\">
+        <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>"
     `)
   })
+
+  it('renders <img> with custom format', () => {
+    const img = mount(NuxtPicture, {
+      propsData: {
+        format: 'avif',
+        src: '/test.png'
+      }
+    })
+    expect(img.find('img').exists()).toBe(true)
+  })
 })