Inline Images in ASP.NET MVC Core

I have blogged extensively about Data URIs in the past. It allows us to render external contents inside of the page’s HTML, avoiding additional HTTP requests, but enlarging the HTML to serve. Sometimes, it does make sense, especially because the whole page can be made cacheable.

MVC does not offer any mechanism for serving images as inline Data URIs, and that is the reason for this post! Winking smile

For this example, I added an extension method to the IHtmlHelper class, InlineImage:

public static class HtmlHelperExtensions
{
    private static string GetFileContentType(string path)
    {
        if (path.EndsWith(".JPG", StringComparison.OrdinalIgnoreCase) == true)
        {
            return "image/jpeg";
        }
        else if (path.EndsWith(".GIF", StringComparison.OrdinalIgnoreCase) == true)
        {
            return "image/gif";
        }
        else if (path.EndsWith(".PNG", StringComparison.OrdinalIgnoreCase) == true)
        {
            return "image/png";
        }
 
        throw new ArgumentException("Unknown file type");
    }
 
    public static HtmlString InlineImage(this IHtmlHelper html, string path, object attributes = null)
    {
        var contentType = GetFileContentType(path);
        var env = html.ViewContext.HttpContext.ApplicationServices.GetService(typeof (IHostingEnvironment)) as IHostingEnvironment;
 
        using (var stream = env.WebRootFileProvider.GetFileInfo(path).CreateReadStream())
        {
            var array = new byte[stream.Length];
            stream.Read(array, 0, array.Length);
 
            var base64 = Convert.ToBase64String(array);
 
            var props = (attributes != null) ? attributes.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(attributes)) : null;
 
            var attrs = (props == null)
                ? string.Empty
                : string.Join(" ", props.Select(x => string.Format("{0}=\"{1}\"", x.Key, x.Value)));
 
            var img = $"<img src=\"data:{contentType};base64,{base64}\" {attrs}/>";
 
            return new HtmlString(img);
        }
    }
}

You can see that I only support three image formats: JPEG, GIF and PNG. These are the safe formats that can be rendered by all browsers. The format is inferred from the file name’s extension.

The InlineImage method takes a local file name that will be considered from the site’s root, and an optional collection of attributes. If present, these attributes are added to the generated IMG tag as-is.

To use this extension, all we need is to add this code in a Razor view:

@Html.InlineImage("image.jpg", new { width = "200", height = "200" });

And the output should look like this (trimmed):

<img src="data:image/jpeg;base64,SGVsbG8sIFdvcmxkIQ%3D%3D...=" width="200" height="200" />

This code works as is in ASP.NET MVC Core, but can be easily changed to work in MVC 5 or prior: just use Server.MapPath to get the physical address of the file to load.



                             

13 Comments

  • It is very nice extension for MVC Razor syntex, but you please have to tell where to apply Server.MapPath: in the extension method or in HTML.

  • Awesome!

    Though I wonder if this would be simpler (and perhaps more performant, as the dependency on IHostingEnvironment would be cached) if this were written as an service; and using it via the @inject directive in the new Razor version. I never quite liked the way Html helpers are written in MVC, and one of the reasons I'm really looking forward to ASP.NET MVC Core is the ability to inject in helpers as needed.

  • Hi.

    You don't escape the HTML attributes (both keys and values) when you output them - it is potential XSS vulnerability. Check out some code for existing form controls and how they handle attribute values (they use TagHelper I think).

  • @Muhammad: the .NET code for the "full" version (not Core) should look like this (out of my head):

    public static HtmlString InlineImage(this IHtmlHelper html, string path, object attributes = null)
    {
    var contentType = GetFileContentType(path);

    var array = File.ReadAllBytes(HttpContext.Current.Server.MapPath(path));

    var base64 = Convert.ToBase64String(array);

    var props = (attributes != null) ? attributes.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(attributes)) : null;

    var attrs = (props == null)
    ? string.Empty
    : string.Join(" ", props.Select(x => string.Format("{0}=\"{1}\"", x.Key, x.Value)));

    var img = $"<img src=\"data:{contentType};base64,{base64}\" {attrs}/>";

    return new HtmlString(img);
    }

  • @tompazourek: which ones? Nothing here comes from the client!

  • @Nelson: sure, plenty of room for improvement! :-)

  • Very cool, you inspired me to try this as a TagHelper instead, so the syntax would be <img src="images/graphicToInline.png" inline> http://adventuresinwebprogramming.blogspot.com/2016/02/inline-image-taghelper.html

  • @Rich: great! Thanks for sharing!

  • @RicardoPeres: The claim that "nothing comes from the client" depends on how you use it in your example.

    Lets say that somebody uses @Html.InlineImage to display an avatar of some user with an alt attribute containing the user's nickname.

    If that user then changes his nickname to something like "\" onload=\"alert('XSS')\" data-alt=\"", he can use XSS to attack through the alt tag.

    It's a common practice to escape all values of all HTML attributes unless you have very good reason not to do so, which you don't. That's why all the HTML helpers in ASP.NET MVC escape all HTML attributes by default.

  • Hi, Tom!
    Always enjoy a good discussion! :-)
    Sure, but my example was about loading a *local* image! You are right, in other scenarios, we need to consider other things!
    I am expecting to read something about this in your blog... ? ;-)
    Thanks!

  • @RicardoPeres: I don't have any blog yet unfortunately. The problem is not local vs. remote images. With my example of the user's avatar, the image is still local (otherwise it couldn't be embedded/inlined this easily), it's the value of the alt tag that would be the problem. The values of HTML attributes always need to be encoded. Even when nothing (not even the alt tag) comes from the user, you still might get into trouble. What if you want to add HTML attribute alt with value like '3" scale' (3 inch scale)? This will not work correctly with your code. If you just used HTML encoding to process the attribute value, it would solve this as well.

    btw. Another different issue with your handling of HTML attributes: there's no way of specifying data attributes. What if I want to render the img tag, and want to specify something like data-trigger="tooltip"?

    These are not huge issues, I am just pointing out that the way you are handling HTML attributes is not completely correct and that there are better, proven ways to handle them. Why not to use the same approach that is used in existing helpers like @Html.TextBoxFor?

    If you used the methods that exist in MVC already (TagHelper, its MergeAttributes method, HtmlHelper.AnonymousObjectToHtmlAttributes method) all these problems would be solved and your code would work as well as other HTML helpers.

  • But what alt tag are you talking about? My code *just outputs a local image as an inline Data URI*!
    I am not handling *any* attributes!
    I am sorry, but I will not continue this discussion. Security should always be a concern, but it has nothing to do with this code example.

  • Good to see other's are dealing with this problem as well. In my case, the issue was I was storing the image in a database, so it was just binary data! In Azure, I want to be able to scale wide, so storing user images in the file system became problematic.

    The problem I ran into was, I was attempting to work with responsive layouts, which seemed to break-down under certain scenarios when the browser wasn't directly in charge of the image (data uri's are handled differently, though I don't know exactly how).

    Also, I ended up in a situation, where nearly EVERY javascript library I found for doing lightbox style images or carousels would fail on a link to a data:uri rather than an actual image. So here's what I ended up doing:

    public class ImageResult : ActionResult
    {
    public ImageResult(Stream imageStream, string contentType)
    {
    if (imageStream == null) throw new ArgumentNullException();
    if (contentType == null) throw new ArgumentNullException();

    this.ImageStream = imageStream;
    this.ContentType = contentType;

    }

    public string ContentType { get; private set; }
    public Stream ImageStream { get; private set; }

    public override void ExecuteResult(ActionContext context)
    {
    if (context == null) throw new ArgumentNullException("context");

    var response = context.HttpContext.Response;

    response.ContentType = this.ContentType;

    byte[] buffer = new byte[4096];
    while(true)
    {
    int read = this.ImageStream.Read(buffer, 0, buffer.Length);
    if (read == 0)
    break;

    response.Body.Write(buffer, 0, read);

    }
    }
    }

    So in essence, we're doing very similar to what you're upto here, but then you can use the image tags appropriately and they load via the browsers image loading capability like this!

    <a class="col-xs-12 col-sm-3 col-md-2 col-lg-2" data-lightbox="@Model.GalleryId" data-title="@photo.FileName" href="@Url.RouteUrl("GetImage", new { id = photo.PhotoId })">
    <img class="thumbnail img-thumbnail" src="@Url.RouteUrl("GetImage", new { id = photo.PhotoId })" alt="">
    </a>

    Sorry, your blogs comments messed the code formatting all up, but you'll get the point I'm sure.

Add a Comment

As it will appear on the website

Not displayed

Your website