Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions docs/content/3.providers/picsum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: Picsum
description: Nuxt Image with Lorem Picsum placeholder images.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/image/blob/main/src/runtime/providers/picsum.ts
size: xs
---

Integration between [Lorem Picsum](https://picsum.photos/) and the image module.

Lorem Picsum provides random placeholder images. It is useful during development or for demo purposes.

To use this provider you just need to enable it in your Nuxt config:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
image: {
picsum: {}
}
})
```

## Usage

### Random image

Get a random image with specific dimensions:

```vue
<NuxtImg
provider="picsum"
src="/"
width="200"
height="300"
/>
```

### Specific image by ID

Use `id/{id}` as the `src` to get a specific image:

```vue
<NuxtImg
provider="picsum"
src="id/237"
width="200"
height="300"
/>
```

### Seeded random image

Use `seed/{seed}` as the `src` to get a consistent random image based on a seed string:

```vue
<NuxtImg
provider="picsum"
src="seed/picsum"
width="200"
height="300"
/>
```

## Modifiers

### `grayscale`

Return the image in grayscale.

```vue
<NuxtImg
provider="picsum"
src="id/870"
width="300"
height="200"
:modifiers="{ grayscale: true }"
/>
```

### `blur`

Apply a blur effect to the image. Accepts values from 1 to 10.

```vue
<NuxtImg
provider="picsum"
src="id/1025"
width="250"
height="250"
:modifiers="{ blur: 5 }"
/>
```

::note
The `src` value must be either `id/{id}` for a specific image, `seed/{seed}` for a seeded random image, or `/` for a fully random image. Other `src` values are ignored and a random image will be returned.
::

::note
Picsum does not support height-only resizing. When only `height` is provided without `width`, a square image of that dimension will be returned.
::
1 change: 1 addition & 0 deletions docs/public/providers/picsum.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions playground/app/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,38 @@ export const providers: Provider[] = [
},
],
},
// Picsum (Lorem Picsum placeholder images)
{
name: 'picsum',
samples: [
{
src: 'id/237',
width: 200,
height: 300,
},
{
src: 'id/870',
width: 300,
height: 200,
modifiers: {
grayscale: true,
},
},
{
src: 'id/1025',
width: 250,
height: 250,
modifiers: {
blur: 5,
},
},
{
src: 'seed/picsum',
width: 200,
height: 200,
},
],
},
// Prepr
{
name: 'prepr',
Expand Down
1 change: 1 addition & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export default defineNuxtConfig({
baseURL: 'https://netlify-photo-gallery.netlify.app',
},
prismic: {},
picsum: {},
prepr: {
projectName: 'nuxt-prepr-demo',
},
Expand Down
1 change: 1 addition & 0 deletions src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const BuiltInProviders = [
'netlify',
'netlifyLargeMedia',
'netlifyImageCdn',
'picsum',
'prepr',
'none',
'prismic',
Expand Down
74 changes: 74 additions & 0 deletions src/runtime/providers/picsum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// https://picsum.photos/ - Lorem Picsum placeholder images

import { joinURL, withQuery } from 'ufo'
import { defineProvider } from '../utils/provider'

interface PicsumModifiers {
grayscale?: boolean
blur?: number
}

interface PicsumOptions {
baseURL?: string
modifiers?: PicsumModifiers
}

export const picsumCDN = 'https://picsum.photos/'

export default defineProvider<PicsumOptions>({
getImage: (src, { modifiers, baseURL = picsumCDN }) => {
const { width, height, grayscale, blur, ...otherModifiers } = modifiers || {}

// Build the path
// Picsum URL format: https://picsum.photos/[id/{id}/]{width}[/{height}]
// Examples:
// - Random: https://picsum.photos/200/300
// - Specific ID: https://picsum.photos/id/237/200/300
// - Square: https://picsum.photos/200

const parts: string[] = []

// If src is provided and not empty, it could be:
// - "id/237" for a specific image
// - "seed/picsum" for a seeded image
if (src && src !== '/') {
const [type, id] = (src.startsWith('/') ? src.slice(1) : src).split('/')
if (type && (type === 'id' || type === 'seed')) {
parts.push(`${type}/${id}`)
}
Comment on lines +34 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing validation: id can be undefined when src is "id" or "seed".

If src is "id" (without a trailing ID value), split('/') yields ["id"], so id is undefined, and parts.push(\${type}/${id}`)produces"id/undefined"` in the URL.

Proposed fix
     if (src && src !== '/') {
       const [type, id] = (src.startsWith('/') ? src.slice(1) : src).split('/')
-      if (type && (type === 'id' || type === 'seed')) {
+      if (type && id && (type === 'id' || type === 'seed')) {
         parts.push(`${type}/${id}`)
       }
     }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (src && src !== '/') {
const [type, id] = (src.startsWith('/') ? src.slice(1) : src).split('/')
if (type && (type === 'id' || type === 'seed')) {
parts.push(`${type}/${id}`)
}
if (src && src !== '/') {
const [type, id] = (src.startsWith('/') ? src.slice(1) : src).split('/')
if (type && id && (type === 'id' || type === 'seed')) {
parts.push(`${type}/${id}`)
}
}
πŸ€– Prompt for AI Agents
In `@src/runtime/providers/picsum.ts` around lines 34 - 38, The code currently
pushes `${type}/${id}` even when id is undefined (e.g., src === "id"), producing
"id/undefined"; update the conditional around the split result to ensure id is
present and non-empty before pushing into parts β€” for example change the check
that uses src/type/id to require id (e.g., if (type && (type === 'id' || type
=== 'seed') && typeof id === 'string' && id.trim().length > 0) {
parts.push(`${type}/${id}`) }) so you only append valid id segments and avoid
"undefined" in URLs.

}

// Add dimensions - these come after the ID/seed path
if (width) {
parts.push(String(width))
}
if (height) {
parts.push(String(height))
}
Comment on lines +41 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Height without width produces an ambiguous Picsum URL.

When only height is provided, only one path segment is appended (e.g., /200), which Picsum treats as a square image (width = height = 200). This silently ignores the user's intent of specifying only the height dimension.

Consider either: (a) documenting this as a known limitation, or (b) emitting both segments (e.g., using a default width or swapping to /{height}/{height}).

πŸ€– Prompt for AI Agents
In `@src/runtime/providers/picsum.ts` around lines 41 - 47, The Picsum URL builder
currently appends only a single segment when height is provided but width is
missing, causing Picsum to interpret it as a square and ignore the user's
intent; update the logic in the function that builds the parts array
(referencing parts, width, height in src/runtime/providers/picsum.ts) so that if
height is provided and width is not you append two segments (e.g., use height
for both width and height) or otherwise supply a sensible default width before
pushing the height segment; ensure both path segments are always present when
either dimension is supplied so the generated Picsum URL is unambiguous.


// Build query parameters for modifiers
const query: Record<string, string | number> = {}

if (grayscale) {
query.grayscale = ''
}

if (blur !== undefined && blur > 0) {
// Picsum blur accepts values from 1-10
query.blur = Math.min(Math.max(Math.round(blur), 1), 10)
}

// Add any other custom modifiers (excluding standard ones that don't apply to picsum)
for (const [key, value] of Object.entries(otherModifiers)) {
if (value !== undefined && value !== null && !['fit', 'format', 'quality', 'background'].includes(key)) {
query[key] = value as string | number
}
}

const url = joinURL(baseURL, ...parts)

return {
url: Object.keys(query).length > 0 ? withQuery(url, query) : url,
}
},
})
14 changes: 14 additions & 0 deletions test/e2e/__snapshots__/picsum.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"requests": [
"https://picsum.photos/id/1025/250/250?blur=5",
"https://picsum.photos/id/237/200/300",
"https://picsum.photos/id/870/300/200?grayscale",
"https://picsum.photos/seed/picsum/200/200",
],
"sources": [
"https://picsum.photos/id/237/200/300",
"https://picsum.photos/id/870/300/200?grayscale",
"https://picsum.photos/id/1025/250/250?blur=5",
"https://picsum.photos/seed/picsum/200/200",
],
}
11 changes: 11 additions & 0 deletions test/nuxt/providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import cloudflare from '../../dist/runtime/providers/cloudflare'
import cloudinary from '../../dist/runtime/providers/cloudinary'
import twicpics from '../../dist/runtime/providers/twicpics'
import fastly from '../../dist/runtime/providers/fastly'
import picsum from '../../dist/runtime/providers/picsum'
import prepr from '../../dist/runtime/providers/prepr'
import glide from '../../dist/runtime/providers/glide'
import imgix from '../../dist/runtime/providers/imgix'
Expand Down Expand Up @@ -462,6 +463,16 @@ describe('Providers', () => {
expect(generated).toMatchObject(image.prepr)
}
})
it('picsum', () => {
const providerOptions = {}

for (const image of images) {
const [_src, modifiers] = image.args
const generated = picsum().getImage('', { modifiers: { ...modifiers }, ...providerOptions }, emptyContext)
expect(generated).toMatchObject(image.picsum)
}
})

it('contentful', () => {
const providerOptions = {
baseURL: '',
Expand Down
6 changes: 6 additions & 0 deletions test/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const images = [
hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/auto_image/cltsrex89477t08unlckqx9ue' },
caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg' },
bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net' },
picsum: { url: 'https://picsum.photos/' },
},
{
args: ['/test.png', { width: 200 }],
Expand Down Expand Up @@ -76,6 +77,7 @@ export const images = [
hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200/auto_image/cltsrex89477t08unlckqx9ue' },
caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200' },
bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200' },
picsum: { url: 'https://picsum.photos/200' },
},
{
args: ['/test.png', { height: 200 }],
Expand Down Expand Up @@ -114,6 +116,7 @@ export const images = [
hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=height:200/auto_image/cltsrex89477t08unlckqx9ue' },
caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?h=200' },
bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?height=200' },
picsum: { url: 'https://picsum.photos/200' },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Height-only requests produce a square image, not a height-constrained one.

When only height: 200 is specified (no width), the generated URL is https://picsum.photos/200, which Picsum interprets as a 200Γ—200 square image β€” not a height-only constraint. This is a semantic mismatch with the user's intent. The same URL is produced for width: 200 (line 80).

For a placeholder image provider this is likely acceptable, but it may be worth documenting this limitation or considering emitting https://picsum.photos/200/200 explicitly for clarity.

πŸ€– Prompt for AI Agents
In `@test/providers.ts` at line 119, The picsum placeholder currently uses the URL
'https://picsum.photos/200' which Picsum treats as a square (200Γ—200) when only
height is intended; update the picsum provider so that when only height is
specified it emits an explicit height/width path (e.g.
'https://picsum.photos/200/200') or adjust the URL generation logic in the
picsum provider to compose `${height}/${height}` when width is undefined; locate
the picsum entry (symbol: picsum) in test/providers.ts and change its url
generation to produce the explicit 'height/height' form or add a note
documenting the limitation if you prefer not to change the URL.

},
{
args: ['/test.png', { width: 200, height: 200 }],
Expand Down Expand Up @@ -152,6 +155,7 @@ export const images = [
hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200/auto_image/cltsrex89477t08unlckqx9ue' },
caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' },
bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' },
picsum: { url: 'https://picsum.photos/200/200' },
},
{
args: ['/test.png', { width: 200, height: 200, fit: 'contain' }],
Expand Down Expand Up @@ -190,6 +194,7 @@ export const images = [
hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200,fit:max/auto_image/cltsrex89477t08unlckqx9ue' },
caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' },
bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' },
picsum: { url: 'https://picsum.photos/200/200' },
},
{
args: ['/test.png', { width: 200, height: 200, fit: 'contain', format: 'jpeg' }],
Expand Down Expand Up @@ -228,5 +233,6 @@ export const images = [
hygraph: { url: 'https://eu-central-1-shared-euc1-02.graphassets.com/cltsj3mii0pvd07vwb5cyh1ig/resize=width:200,height:200,fit:max/output=format:jpeg/cltsrex89477t08unlckqx9ue' },
caisy: { url: 'https://assets.caisy.io/assets/b76210be-a043-4989-98df-ecaf6c6e68d8/056c27e2-81f5-4cd3-b728-cef181dfe7dc/d83ea6f0-f90a-462c-aebd-b8bc615fdce0pexelsmiguelapadrinan1591056.jpg?w=200&h=200' },
bunny: { url: 'https://bunnyoptimizerdemo.b-cdn.net?width=200&height=200' },
picsum: { url: 'https://picsum.photos/200/200' },
},
] as const
Loading