Skip to content

[13.x] Add toHtml() to Image#60796

Open
shaedrich wants to merge 18 commits into
laravel:13.xfrom
shaedrich:image-to-html
Open

[13.x] Add toHtml() to Image#60796
shaedrich wants to merge 18 commits into
laravel:13.xfrom
shaedrich:image-to-html

Conversation

@shaedrich

Copy link
Copy Markdown
Contributor

Current

use Illuminate\Support\Facades\Image;

$image = Image::fromStorage('avatars/photo.jpg', 'public')
    ->cover(400, 400)
    ->toWebp()
    ->quality(80);
<img width="400" height="400" src="{{ $image->toDataUrl() }}">

As you can see, information that have already been given above (cover(400, 400)) have to be mirrored/duplicated within blade (width="400" height="400"), which can result in a lot of boilerplate.

Proposed

use Illuminate\Support\Facades\Image;

$path = Image::fromStorage('avatars/photo.jpg', 'public')
    ->cover(400, 400)
    ->toWebp()
    ->quality(80);
{!! $image->toHtml() !!}

This makes Image usable ergonomically within Blade as if it is a first-class citizen, just reusing what it already knows with the option to add more attributes:

{!! $image->toHtml(['alt' => 'My avatar']) !!}
<!-- Results in: <img width="400" height="400" src="data:…" alt="My avatar"> -->

@timacdonald

timacdonald commented Jul 16, 2026

Copy link
Copy Markdown
Member

Something Vite does, which is kinda cool, is gives you a control of how big an image is before it is rendered via a data URL vs referenced as an HTTP URL.

Imported or referenced assets that are smaller than this threshold will be inlined as base64 URLs to avoid extra http requests. Set to 0 to disable inlining altogether.

Source

I wonder if there is something interesting we can do here, in tandem with a public disk, where generated images of smaller sizes are rendered as a data url and others are stored on a public disk and rendered as a http url, all handled seamlessly.

Note that I haven't deep dived the image feature, so I don't know the internals or how things work under the hood to know if this totally makes sense as a suggestion.

@valorin

valorin commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Note that attribute values aren't escaped in there, which means in the case of your example $image->toHtml(['alt' => 'My avatar']), if the user provides the value for alt, then it opens up an easy XSS vector. This is something Laravel prevents with attributes on Blade components by default. Plus, Htmlable allows it to be used as {{ $image }} - which makes it feel safer, even though it's not.

For example:

// $user->display_name => "><script>alert(document.cookie)</script>

$image->toHtml(['alt' => $user->display_name]);

Outputs:

<img src="data:image/png;base64,…" width="100" height="100" alt=""><script>alert(document.cookie)</script>">

At a minimum, wrapping the values in e() should lock it down:

->map(fn ($value, $key) => sprintf('%s="%s"', e($key), e($value)))

But I would suggest looking into how Blade components handle attributes in Illuminate\View\ComponentAttributeBag to handle more than just string attributes.

@shaedrich
shaedrich marked this pull request as draft July 16, 2026 09:10
Comment thread src/Illuminate/Image/Image.php Outdated
shaedrich and others added 12 commits July 16, 2026 20:09
Co-authored-by: Stephen Rees-Carter <stephen@rees-carter.net>
and filter out invalid ones

Co-authored-by: Ricardo Cerljenko <ricardo@lloyds-digital.com>

See laravel#60796 (comment)
Co-authored-by: Ricardo Cerljenko <ricardo@lloyds-digital.com>

See laravel#60796 (comment)
Co-authored-by: Ricardo Cerljenko <ricardo@lloyds-digital.com>

See laravel#60796 (comment)
@browner12

Copy link
Copy Markdown
Contributor

I think a component would be more appropriate to solve this problem.

@shaedrich

shaedrich commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@browner12 I thought about that, too, but Laravel doesn't ship its own components, does it?

@shaedrich

shaedrich commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Something Vite does, which is kinda cool, is gives you a control of how big an image is before it is rendered via a data URL vs referenced as an HTTP URL.

Imported or referenced assets that are smaller than this threshold will be inlined as base64 URLs to avoid extra http requests. Set to 0 to disable inlining altogether.
Source

I wonder if there is something interesting we can do here, in tandem with a public disk, where generated images of smaller sizes are rendered as a data url and others are stored on a public disk and rendered as a http url, all handled seamlessly.

Note that I haven't deep dived the image feature, so I don't know the internals or how things work under the hood to know if this totally makes sense as a suggestion.

@timacdonald Great idea, I thought about this as well, however,

  • this would require a new config value and I intentionally left the scope small (arguably, additional attributes already have their complexity ^^)
  • it could be confusing if "Turn into an HTML tag" interacts with the file system

@browner12

Copy link
Copy Markdown
Contributor

AFAIK it doesn't currently, although I think there are some situations it should. Basically anytime we're using a method like this to spit out HTML, a component is IMO better suited to solve the problem. For example, I PR'ed a Pagination component many months ago that got rejected. So rather than

{!! $users->links() !!}

it would be something like

<x-laravel.pagination
    :models="$users"
    :perpage="5"
    :showPages="true"
></x-laravel.pagination>

I've been tempted to try this again lately, but Taylor obviously was uninterested before.

If that's still the case, my opinion would be this belongs as a userland solution.

@shaedrich

shaedrich commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

this would require a new config value and I intentionally left the scope small (arguably, additional attributes already have their complexity ^^)

Other things, I thought about but considered out of scope for now:

  • outputting a <figure> with a <figcaption>
  • outputting a <picture> (this would be interesting when we could create multiple versions of an image simultaneously)
  • Having the additional attributes as part of the classes metadata as class properties
  • Using file metadata for alt (e.g. COMMENT or COMPUTED.UserComment)

@shaedrich

shaedrich commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

AFAIK it doesn't currently, although I think there are some situations it should. Basically anytime we're using a method like this to spit out HTML, a component is IMO better suited to solve the problem. For example, I PR'ed a Pagination component many months ago that got rejected. So rather than

{!! $users->links() !!}

it would be something like

<x-laravel.pagination
    :models="$users"
    :perpage="5"
    :showPages="true"
></x-laravel.pagination>

I've been tempted to try this again lately, but Taylor obviously was uninterested before.

@browner12 Yeah, a penny for this man's thoughts … ^^

I mean, same with official enums, value objects, etc.

Maybe, we first need an official laravel/components for this 😂

@shaedrich

Copy link
Copy Markdown
Contributor Author

Note that attribute values aren't escaped in there, which means in the case of your example $image->toHtml(['alt' => 'My avatar']), if the user provides the value for alt, then it opens up an easy XSS vector. This is something Laravel prevents with attributes on Blade components by default.

At a minimum, wrapping the values in e() should lock it down

@valorin Good catch! I went with the e() solution 👍🏻

@shaedrich
shaedrich marked this pull request as ready for review July 17, 2026 11:59
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.

5 participants