OrchardCore icon indicating copy to clipboard operation
OrchardCore copied to clipboard

Responsive images support

Open arkadiuszwojcik opened this issue 5 years ago • 7 comments

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.

arkadiuszwojcik avatar Mar 02 '19 14:03 arkadiuszwojcik

Are you wanting automatic responsive images or the ability to specify different image sizes (like a tag helper for instance).

scleaver avatar Mar 03 '19 21:03 scleaver

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.

arkadiuszwojcik avatar Mar 04 '19 11:03 arkadiuszwojcik

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?

arkadiuszwojcik avatar Mar 04 '19 11:03 arkadiuszwojcik

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

scleaver avatar Mar 05 '19 12:03 scleaver

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?

arkadiuszwojcik avatar Mar 06 '19 19:03 arkadiuszwojcik

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 manual sizes specification.
  • Two render modes: picture (that gives some extra features like art direction) or img with srcset and sizes
  • 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?

arkadiuszwojcik avatar Mar 08 '19 01:03 arkadiuszwojcik

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">

arkadiuszwojcik avatar Sep 22 '19 11:09 arkadiuszwojcik