Skip to content

Commit

Permalink
Storyblok and Cloudflare processing (#13)
Browse files Browse the repository at this point in the history
* Add Cloudflare processing
* Add Storyblok processing
* Remove usage of Uri.IsWellFormedUriString(). Issue #12
  • Loading branch information
ErikHen authored May 1, 2023
1 parent 0be2104 commit b000c2c
Show file tree
Hide file tree
Showing 14 changed files with 468 additions and 100 deletions.
29 changes: 28 additions & 1 deletion PictureRenderer/Picture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public static string Render(string imagePath, PictureProfileBase profile, LazyLo
return Render(imagePath, profile, string.Empty, lazyLoading);
}

/// <summary>
/// Render different images in the same picture element.
/// </summary>
public static string Render(string[] imagePaths, PictureProfileBase profile, LazyLoading lazyLoading)
{
return Render(imagePaths, profile, string.Empty, lazyLoading);
Expand All @@ -23,6 +26,9 @@ public static string Render(string imagePath, PictureProfileBase profile, (doubl
return Render(imagePath, profile, string.Empty, LazyLoading.Browser, focalPoint);
}

/// <summary>
/// Render different images in the same picture element.
/// </summary>
public static string Render(string[] imagePaths, PictureProfileBase profile, (double x, double y)[] focalPoints)
{
return Render(imagePaths, profile, string.Empty, LazyLoading.Browser, focalPoints);
Expand All @@ -33,6 +39,9 @@ public static string Render(string imagePath, PictureProfileBase profile, string
return Render(imagePath, profile, altText, LazyLoading.Browser, focalPoints);
}

/// <summary>
/// Render different images in the same picture element.
/// </summary>
public static string Render(string[] imagePaths, PictureProfileBase profile, string altText, (double x, double y)[] focalPoints)
{
return Render(imagePaths, profile, altText, LazyLoading.Browser, focalPoints);
Expand All @@ -43,11 +52,19 @@ public static string Render(string imagePath, PictureProfileBase profile, string
return Render(imagePath, profile, altText, LazyLoading.Browser, cssClass: cssClass);
}

/// <summary>
/// Render different images in the same picture element.
/// </summary>
public static string Render(string[] imagePaths, PictureProfileBase profile, string altText, string cssClass)
{
return Render(imagePaths, profile, altText, LazyLoading.Browser, focalPoints: default, cssClass: cssClass);
}

/// <summary>
/// Render picture element.
/// </summary>
/// <param name="focalPoint">Value range: 0-1 for ImageSharp, 1-[image width/height] for Storyblok.</param>
/// <returns></returns>
public static string Render(string imagePath, PictureProfileBase profile, string altText = "", LazyLoading lazyLoading = LazyLoading.Browser, (double x, double y) focalPoint = default, string cssClass = "", string imgWidth = "")
{
var pictureData = PictureUtils.GetPictureData(imagePath, profile, altText, focalPoint, cssClass);
Expand Down Expand Up @@ -160,8 +177,18 @@ private static string RenderInfoElements(PictureProfileBase pictureProfile, Pic
{
return string.Empty;
}

var formatFunction = $"function format{pictureData.UniqueId}(input) {{ return input.split('/').pop().replace('?', '\\n').replaceAll('&', ', ').replace('%2c', ',').replace('rxy', 'focal point'); }}";
if (pictureProfile is StoryblokProfile)
{
formatFunction = $"function format{pictureData.UniqueId}(input) {{ return input.split('/m/').pop().replaceAll('/', ', '); }}";
}
if (pictureProfile is CloudflareProfile)
{
formatFunction = $"function format{pictureData.UniqueId}(input) {{ return input.split('/cdn-cgi/image/').pop().replace('/http', ', http'); }}";
}
var infoDiv = $"<div id=\"pinfo{pictureData.UniqueId}\" style=\"position: absolute; margin-top:-60px; padding:0 5px 2px 5px; font-size:0.8rem; text-align:left; background-color:rgba(255, 255, 255, 0.8);\"></div>";
var script =$"<script type=\"text/javascript\"> window.addEventListener(\"load\",function () {{ const pictureInfo = document.getElementById('pinfo{pictureData.UniqueId}'); var image = document.getElementById('{pictureData.UniqueId}'); pictureInfo.innerText = format{pictureData.UniqueId}(image.currentSrc); image.onload = function () {{ pictureInfo.innerText = format{pictureData.UniqueId}(image.currentSrc); }}; function format{pictureData.UniqueId}(input) {{ return input.split('/').pop().replace('?', '\\n').replaceAll('&', ', ').replace('%2c', ',').replace('rxy', 'focal point'); }} }}, false);</script>";
var script =$"<script type=\"text/javascript\"> window.addEventListener(\"load\",function () {{ const pictureInfo = document.getElementById('pinfo{pictureData.UniqueId}'); var image = document.getElementById('{pictureData.UniqueId}'); pictureInfo.innerText = format{pictureData.UniqueId}(image.currentSrc); image.onload = function () {{ pictureInfo.innerText = format{pictureData.UniqueId}(image.currentSrc); }}; {formatFunction} }}, false);</script>";
return "\n" + infoDiv + "\n" + script;
}
}
Expand Down
6 changes: 3 additions & 3 deletions PictureRenderer/PictureRenderer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>3.6</Version>
<Version>3.7</Version>
<Authors>Erik Henningson</Authors>
<Company />
<Product />
<PackageIcon>picture_icon.png</PackageIcon>
<PackageIconUrl />
<PackageProjectUrl>https://github.com/ErikHen/PictureRenderer</PackageProjectUrl>
<PackageTags>Picture;element Responsive ImageSharp Webp</PackageTags>
<Description>Simplify rendering of HTML picture element. With support for responsive, lazy loaded images in Webp format.</Description>
<PackageTags>Picture;element Responsive ImageSharp Storyblok Cloudflare Webp</PackageTags>
<Description>Simplify rendering of HTML picture element. With support for responsive, lazy loaded images in the most optimal format. Works with Storyblok Image Service, Cloudflare Image Resizing, and ImageSharp (used by for example Umbraco CMS and Optimizely CMS).</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>

Expand Down
138 changes: 58 additions & 80 deletions PictureRenderer/PictureUtils.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System;
using PictureRenderer.Profiles;
using PictureRenderer.UrlBuilders;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using PictureRenderer.Profiles;

namespace PictureRenderer
{
Expand All @@ -24,14 +23,14 @@ public static PictureData GetPictureData(string imagePath, PictureProfileBase pr
var pData = new PictureData
{
AltText = altText,
ImgSrc = BuildQueryString(uri, profile, profile.FallbackWidth, string.Empty, focalPoint),
ImgSrc = BuildImageUrl(uri, profile, profile.FallbackWidth, string.Empty, focalPoint),
CssClass = cssClass,
SrcSet = BuildSrcSet(uri, profile, string.Empty, focalPoint),
SizesAttribute = string.Join(", ", profile.Sizes),
UniqueId = profile.ShowInfo ? Guid.NewGuid().ToString("n").Substring(0, 10) : string.Empty
};

if (ShouldCreateWebp(profile, uri))
if (ShouldRenderWebp(profile, uri))
{
pData.SrcSetWebp = BuildSrcSet(uri, profile, ImageFormat.Webp, focalPoint);
}
Expand Down Expand Up @@ -66,8 +65,8 @@ public static MediaImagesPictureData GetMultiImagePictureData(string[] imagePath
var imageUri = GetUriFromPath(imagePath);
mediaImagePaths.Add(new MediaImagePaths()
{
ImagePath = BuildQueryString(imageUri, profile, profile.MultiImageMediaConditions[i].Width, null, imageFocalPoint),
ImagePathWebp = ShouldCreateWebp(profile, imageUri) ? BuildQueryString(imageUri, profile, profile.MultiImageMediaConditions[i].Width, ImageFormat.Webp, imageFocalPoint) : string.Empty,
ImagePath = BuildImageUrl(imageUri, profile, profile.MultiImageMediaConditions[i].Width, null, imageFocalPoint),
ImagePathWebp = ShouldRenderWebp(profile, imageUri) ? BuildImageUrl(imageUri, profile, profile.MultiImageMediaConditions[i].Width, ImageFormat.Webp, imageFocalPoint) : string.Empty,
MediaCondition = profile.MultiImageMediaConditions[i].Media
});

Expand All @@ -83,128 +82,107 @@ public static MediaImagesPictureData GetMultiImagePictureData(string[] imagePath
{
MediaImages = mediaImagePaths,
AltText = altText,
ImgSrc = BuildQueryString(fallbackImageUri, profile, profile.FallbackWidth, string.Empty, fallbackImageFocalPoint),
ImgSrc = BuildImageUrl(fallbackImageUri, profile, profile.FallbackWidth, string.Empty, fallbackImageFocalPoint),
CssClass = cssClass,
UniqueId = profile.ShowInfo ? Guid.NewGuid().ToString("n").Substring(0, 10) : string.Empty
};

return pData;
}


private static string BuildQueryString(Uri uri, PictureProfileBase profile, int imageWidth, string wantedFormat, (double x, double y) focalPoint)
private static string BuildImageUrl(Uri uri, PictureProfileBase profile, int imageWidth, string wantedFormat, (double x, double y) focalPoint)
{
var queryItems = HttpUtility.ParseQueryString(uri.Query);

if (!string.IsNullOrEmpty(wantedFormat))
if (profile is ImageSharpProfile imageSharpProfile)
{
queryItems.Remove("format"); //remove if it already exists
queryItems.Add("format", wantedFormat);
return ImageSharpUrlBuilder.BuildImageUrl(uri, imageSharpProfile, imageWidth, wantedFormat, focalPoint);
}

queryItems.Add("width", imageWidth.ToString());

queryItems = AddHeightQuery(imageWidth, queryItems, profile);

queryItems = AddFocalPointQuery(focalPoint, queryItems);

// "quality" have to be after "format".
queryItems = AddQualityQuery(queryItems, profile);

var domain = string.Empty;
if (!uri.Host.Contains("dummy-xyz.com"))
if (profile is StoryblokProfile storyblokProfile)
{
//keep the original image url domain.
domain = uri.GetLeftPart(UriPartial.Authority);
return StoryblokUrlBuilder.BuildStoryblokUrl(uri, storyblokProfile, imageWidth, focalPoint);
}

return domain + uri.AbsolutePath + "?" + queryItems.ToString();
}

private static NameValueCollection AddFocalPointQuery((double x, double y) focalPoint, NameValueCollection queryItems)
{
if ((focalPoint.x > 0 || focalPoint.y > 0) && queryItems["rxy"] == null)
if (profile is CloudflareProfile cloudflareProfile)
{
var x = Math.Round(focalPoint.x, 3).ToString(CultureInfo.InvariantCulture);
var y = Math.Round(focalPoint.y, 3).ToString(CultureInfo.InvariantCulture);
queryItems.Add("rxy", $"{x},{y}");
return CloudflareUrlBuilder.BuildCloudflareUrl(uri, cloudflareProfile, imageWidth, focalPoint);
}

return queryItems;
return string.Empty;
}

private static NameValueCollection AddHeightQuery(int imageWidth, NameValueCollection queryItems, PictureProfileBase profile)
internal static (string x, string y) FocalPointAsString((double x, double y) focalPoint)
{
//Do nothing if height is already in the querystring.
if (queryItems["height"] != null)
{
return queryItems;
}

//Add height based on aspect ratio, or from FixedHeight.
if (profile.AspectRatio > 0)
{
queryItems.Add("height", Convert.ToInt32(imageWidth / profile.AspectRatio).ToString());
}
else if (profile.FixedHeight != null && profile.FixedHeight > 0)
{
queryItems.Add("height", profile.FixedHeight.ToString());
}

return queryItems;
var x = Math.Round(focalPoint.x, 3).ToString(CultureInfo.InvariantCulture);
var y = Math.Round(focalPoint.y, 3).ToString(CultureInfo.InvariantCulture);

return (x,y);
}

private static NameValueCollection AddQualityQuery(NameValueCollection queryItems, PictureProfileBase profile)
internal static int GetImageHeight(int imageWidth, PictureProfileBase profile)
{
//TODO: Ignore quality for png etc
if (queryItems["quality"] == null)
//Add height based on aspect ratio, or from FixedHeight.
if (profile.AspectRatio > 0)
{
if (profile.Quality != null)
{
//Add quality value from profile.
queryItems.Add("quality", profile.Quality.ToString());
}
return Convert.ToInt32(imageWidth / profile.AspectRatio);
}
else
else if (profile.FixedHeight != null && profile.FixedHeight > 0)
{
//Quality value already exists in querystring. Don't change it, but make sure it's last (after format).
var quality = queryItems["quality"];
queryItems.Remove("quality");
queryItems.Add("quality", quality);
return profile.FixedHeight.Value;
}

return queryItems;
return 0;
}

private static string BuildSrcSet(Uri imageUrl, PictureProfileBase profile, string wantedFormat, (double x, double y) focalPoint)
{
var srcSetBuilder = new StringBuilder();
foreach (var width in profile.SrcSetWidths)
{
srcSetBuilder.Append(BuildQueryString(imageUrl, profile, width, wantedFormat, focalPoint) + " " + width + "w, ");
srcSetBuilder.Append(BuildImageUrl(imageUrl, profile, width, wantedFormat, focalPoint) + " " + width + "w, ");
}

return srcSetBuilder.ToString().TrimEnd(',', ' ');
}

private static Uri GetUriFromPath(string imagePath)
{
if (!Uri.IsWellFormedUriString(imagePath, UriKind.Absolute))
if (!IsValidHttpUri(imagePath, out var uri))
{
imagePath = "https://dummy-xyz.com" + imagePath; //to be able to use the Uri object.
if (!Uri.IsWellFormedUriString(imagePath, UriKind.Absolute))
//A Uri object must have a domain, but imagePath might be just a path. Add dummy domain, and test again.
imagePath = "https://dummy-xyz.com" + imagePath;
if (!IsValidHttpUri(imagePath, out uri))
{
throw new ArgumentException($"Image url '{imagePath}' is not well formatted.");
}
}

return new Uri(imagePath, UriKind.Absolute);
return uri;
}

private static bool ShouldCreateWebp(PictureProfileBase profile, Uri imageUri)
private static bool IsValidHttpUri(string uriString, out Uri uri) {
return Uri.TryCreate(uriString, UriKind.Absolute, out uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}

internal static string GetImageDomain(Uri uri)
{
var originalFormat = GetFormatFromExtension(imageUri.AbsolutePath);
return profile.CreateWebpForFormat != null && profile.CreateWebpForFormat.Contains(originalFormat);
var domain = string.Empty;
if (!uri.Host.Contains("dummy-xyz.com"))
{
//return the original image url domain.
domain = uri.GetLeftPart(UriPartial.Authority);
}
return domain;
}

private static bool ShouldRenderWebp(PictureProfileBase profile, Uri imageUri)
{
if (profile is ImageSharpProfile imageSharpProfile)
{
var originalFormat = GetFormatFromExtension(imageUri.AbsolutePath);
return imageSharpProfile.CreateWebpForFormat != null && imageSharpProfile.CreateWebpForFormat.Contains(originalFormat);
}

return false;
}

private static string GetFormatFromExtension(string filePath)
Expand Down
11 changes: 11 additions & 0 deletions PictureRenderer/Profiles/CloudflareProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace PictureRenderer.Profiles
{
public class CloudflareProfile : PictureProfileBase
{

}
}
12 changes: 11 additions & 1 deletion PictureRenderer/Profiles/ImageSharpProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ namespace PictureRenderer.Profiles
{
public class ImageSharpProfile : PictureProfileBase
{

/// <summary>
/// The image formats that should be offered as webp versions.
/// PictureRenderer.ImageFormat.Jpeg is added by default.
/// </summary>
public string[] CreateWebpForFormat { get; set; }

public ImageSharpProfile() : base()
{
Quality = 80;
CreateWebpForFormat = new[] { ImageFormat.Jpeg };
}
}
}
9 changes: 2 additions & 7 deletions PictureRenderer/Profiles/PictureProfileBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ public abstract class PictureProfileBase
/// </summary>
public int? Quality { get; set; }

/// <summary>
/// The image formats that should be offered as webp versions.
/// PictureRenderer.ImageFormat.Jpeg is added by default.
/// </summary>
public string[] CreateWebpForFormat { get; set; }


/// <summary>
/// Image width for browsers without support for picture element. Will use the largest image if not set.
Expand Down Expand Up @@ -76,8 +72,7 @@ public int FallbackWidth

protected PictureProfileBase()
{
Quality = 80;
CreateWebpForFormat = new string[] {ImageFormat.Jpeg};

ImageDecoding = ImageDecoding.Async;
ShowInfo = false;
}
Expand Down
22 changes: 22 additions & 0 deletions PictureRenderer/Profiles/StoryblokProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace PictureRenderer.Profiles
{
public class StoryblokProfile : PictureProfileBase
{
/// <summary>
/// Converts a Storyblok "focus" value from string to numeric values (only using the [left] + [top] values).
/// </summary>
/// <param name="storyblokFocalPoint">Value in the format [left]x[top]:[right]x[bottom].</param>
/// <returns></returns>
public static (double x, double y) ConvertStoryblokFocalPoint(string storyblokFocalPoint)
{
var xValue = storyblokFocalPoint.Split(':')[0].Split('x')[0];
var yValue = storyblokFocalPoint.Split(':')[0].Split('x')[1];

return (double.Parse(xValue, System.Globalization.CultureInfo.InvariantCulture), double.Parse(yValue, System.Globalization.CultureInfo.InvariantCulture));
}
}
}
Loading

0 comments on commit b000c2c

Please sign in to comment.