OrchardCMS / OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
https://orchardcore.net
BSD 3-Clause "New" or "Revised" License
7.23k stars 2.34k forks source link

Responsive images support #3273

Open arkadiuszwojcik opened 5 years ago

arkadiuszwojcik commented 5 years ago

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.

scleaver commented 5 years ago

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

arkadiuszwojcik commented 5 years ago

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 commented 5 years ago

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?

scleaver commented 5 years ago

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

arkadiuszwojcik commented 5 years ago

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 commented 5 years ago

I would like to give quick summary here. In my opinion such feature should look like this:

@scleaver what do you think about it? Can we make some proper spec from it?

arkadiuszwojcik commented 4 years ago

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