OrchardCore
OrchardCore copied to clipboard
Responsive images support
I would like to ask design question here. Recently while developing my website I encounter problem of missing support for responsive images in Orchard Core. A lot of other popular CMS frameworks have build in support for that: WordPress Drupal My question is where is proper place for such feature? For sure it need to know about theme breakpoints, is it something that user can define right now in theme metadata? Should all of this be part of core module like media module or should it be outside core features? I would appreaciate hearing your thoughts on this.
Are you wanting automatic responsive images or the ability to specify different image sizes (like a tag helper for instance).
I can image this feature as follows. From theme metadata we read all breakpoints so we know what aspect ratio of images should be supported, besides we need to fill sizes
property in img
element. When user upload image by media library those aspec retio and sizes should be used to auto generate croped/scaled images so we can fill srcset
property (user can later manualy replace some of those images if defaut auto crop don't give us satisfactory results). Those sizes can much differ from current predefined set of possible auto gen image sizes avaliable in OrchardCore.
Or maybe it doesn't have to be done by media library when uploading image but instead in image tag helper when image is rendered for a first time and responsive images are enabled?
Yeah I can't see how the system could know automatically what width and height to resize an image too for every single place it is used within the website based on the fact that it will be different containers within the layout.
I think a tag helper that supports srcset and picture would be best. The size params could be set in the template
May I ask where exactly in template we could store size params? Drupal for example use optional breakpoints.yml
file in template so some modules like Responsive Images module can use it. Are you proposing such file or to use something else?
I would like to give quick summary here. In my opinion such feature should look like this:
- New PictureField element in Media module or different one
- PictureField allows to upload multiple images that are logically coupled (same image but in different sizes, formats), each path must have additional info about image width and pixel density, image type (jpg, webP, etc.) so those metadata can be use to render
srcset
correctly. Probably it should also have extra field for manualsizes
specification. - Two render modes:
picture
(that gives some extra features like art direction) orimg
withsrcset
andsizes
- Optional feature: crop tool, so user can upload image and make some crop operations to generate other sizes
@scleaver what do you think about it? Can we make some proper spec from it?
My current solution:
public class ResponsiveImageTagFilter : ILiquidFilter
{
private readonly IMediaFileStore _mediaFileStore;
public ResponsiveImageTagFilter(IMediaFileStore mediaFileStore)
{
_mediaFileStore = mediaFileStore;
}
public async ValueTask<FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext ctx)
{
var imgUrlWidthPairs = input.Enumerate()
.Select(mediaFile => GetImageUrlWidthPairAsync(mediaFile.ToStringValue()))
.ToArray();
await Task.WhenAll(imgUrlWidthPairs);
var sortedImgUrlWidthPairs = imgUrlWidthPairs
.Select(e => e.Result)
.Where(e => e != null)
.OrderBy(i => i.ImageWidth)
.ToArray();
var src = sortedImgUrlWidthPairs
.LastOrDefault()?.ImageUrl;
var srcset = sortedImgUrlWidthPairs
.Select(i => $"{i.ImageUrl} {i.ImageWidth}w")
.Aggregate("", (i, j) => i + "," + j);
var imgTag = $"<img srcset=\"{srcset}\" src=\"{src}\"";
foreach (var name in arguments.Names)
{
imgTag += $" {name.Replace("_", "-")}=\"{arguments[name].ToStringValue()}\"";
}
imgTag += " />";
return new StringValue(imgTag) { Encode = false };
}
private async Task<ImageUrlWidthPair> GetImageUrlWidthPairAsync(string imagePath)
{
var imageUrl = _mediaFileStore.MapPathToPublicUrl(imagePath) ?? imagePath;
using (var stream = await _mediaFileStore.GetFileStreamAsync(imagePath))
{
var imageInfo = Image.Identify(stream);
if (imageInfo != null)
return new ImageUrlWidthPair(imageUrl, imageInfo.Width);
return null;
}
}
class ImageUrlWidthPair
{
public ImageUrlWidthPair(string imageUrl, int imageWidth)
{
ImageUrl = imageUrl;
ImageWidth = imageWidth;
}
public string ImageUrl { get; }
public int ImageWidth { get; }
}
}
Example usage:
{% assign default_sizes = '(max-width: 1024px) 100%, 1024px' %}
{{ Model.ContentItem.Content.ImagePart.Files.Paths | responsive_img_tag: sizes:default_sizes, class:"some-css-class" }}
and output:
<img srcset="image-480.jpg 480w, image-800.jpg 800w, image-1024.jpg 1024w" src="image-1024.jpg" sizes="(max-width: 1024px) 100%, 1024px" class="some-css-class">