Here's the scenario; we use a page's teaser image to populate meta tags such as og:image
or twitter:image
so that if someone were to try and share one of our pages on social media, that is the image which is auto-populated in whatever sharing method / tool they are using. We love a good SVG, but as it turns out, some of the social sharing tools don't. Facebook for example won't render SVGs and so that image is either left blank when someone shares or that sharer has to be diligent enough to manually select an image - doesn't happen that often.
There were a few avenues we could explore as solutions to this. We looked at just having one static image for sharing - company logo perhaps, which seemed a bit boring. Or possibly a suite of static images which could be swapped based on page type. That seemed a bit more flexible and dynamic, but if we wanted dynamic then why not do something with the SVG itself? Like converting the SVG to a PNG on the fly! We looked around online, but couldn't see anyone who'd already developed this before and it sounded like the most fun approach so that's what we went with.
The last thing to note is that we're running EPiServer 11 in DXC which means Azure Blobs!
In _Root.cshtml
we had code that looked like this:
<meta name="twitter:image" content="@Model.CurrentPage.TeaserImage.ContentExternalUrl(System.Globalization.CultureInfo.InvariantCulture, true)" />
We wanted to replace the call to ContentExternalUrl()
with some helper method which would check for the presence of an SVG and convert that to a suitably sized PNG and deliver that to the browser. Thinking ahead, we expected that conversion process to impact server performance to some degree so we only wanted to do the work when needed. With that in mind we wanted the helper method to check to see if we'd already done the conversion work before and deliver that image instead of doing it again and again.
We looked around online and decided that the package Svg v2.4.2 could do what we wanted.
I'll paste the code below which is reasonably commented, but the _Root.cshtml now contains:
<meta name="twitter:image" content="@SocialImageSharingHelper.ConvertImage(Model.CurrentPage.TeaserImage, ".png", Model.CurrentPage.ContentLink).ContentExternalUrl(System.Globalization.CultureInfo.InvariantCulture, true)" />
and here is the helper:
public static class SocialImageSharingHelper
{
//Summary:
// This helper is intended to be used in _Root.cshtml
// When pages have a teaser image in SVG format they are not
// always shared through social media in a desired way as
// we set the teaser image to the meta property og-url
//
//Parameters:
// imageReference:
// The ContentReference of the image to be converted.
//
// fileExtension:
// string value of the file type the image is to be converted to. Must include the .
//
// imageFolderReference:
// The ContentReference to the folder the current image is held.
//
//Returns:
// The ContentReference to the converted image, if conversion has happened. Else the original ContentReference
//
//TODO convert this to a service and use Dependency Injection (construction)
public static ContentReference ConvertImage(ContentReference imageReference, string fileExtension, ContentReference imageFolderReference)
{
var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
var blobFactory = ServiceLocator.Current.GetInstance<IBlobFactory>();
var contentAssetHelper = ServiceLocator.Current.GetInstance<ContentAssetHelper>();
var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
//get the current image
ImageData currentImage = contentRepository.Get<ImageData>(imageReference);
//get the path to the current image
string currentImagePath = ServiceLocator.Current.GetInstance<UrlResolver>().GetVirtualPath(imageReference).GetUrl(true);
//get the extension of the file
string extension = Path.GetExtension(currentImagePath);
//if the file isn't an SVG then we don't need to convert, return early with the original ContentReference
if (extension != ".svg")
{
return imageReference;
}
//we've got this far so we know we want to convert, unless the work has already been done!
//TODO: Check to see if converted file already exists and if so return that reference
//GetBlob only works with Uri id, so we can't just use a file name.
//explore the current page's assets folder (as this is where we would have stored any converted files)
string theFileWeAreLookingFor = $"{Path.GetFileNameWithoutExtension(currentImagePath)}{fileExtension}";
var folder = contentAssetHelper.GetAssetFolder(imageFolderReference);
foreach (var item in contentLoader.GetChildren<MediaData>(folder.ContentLink))
{
//check if item is the converted file we were looking for
if ( item.Name.Equals(theFileWeAreLookingFor))
{
return item.ContentLink;
}
}
SvgDocument svgDocument;
try
{
//SVgDocument.Open() likes to read files on disk and as ours are in Azure they aren't on disk, so we have
//to use the overload for Stream
//use the BlobFactory to get the image as an array of bytes
byte[] blobBytes = blobFactory.GetBlob(currentImage.BinaryData.ID).ReadAllBytes();
//read the byte array into memory as a stream
using (MemoryStream ms = new MemoryStream(blobBytes))
{
svgDocument = SvgDocument.Open<SvgDocument>(ms);
}
}
catch(FileNotFoundException exception)
{
//something went wrong
Console.Write(exception);
return imageReference;
}
//imageFile is the blob container, location and data etc...
var imageFile = contentRepository.GetDefault<ImageFile>(contentAssetHelper.GetAssetFolder(imageFolderReference).ContentLink);
//Render the SVG as Bitmap using .Draw()
//passing in 0 as the width element to keep aspect ratio
var svgFileContents = svgDocument.Draw(0, 100);
//Convert the Bitmap rendering
ImageConverter converter = new ImageConverter();
byte[] data = (byte[])converter.ConvertTo(svgFileContents, typeof(byte[]));
//Create a blog with the file extension and everything.
var blob = blobFactory.CreateBlob(imageFile.BinaryDataContainer, fileExtension);
//File the blob with byte array data
using (var s = blob.OpenWrite())
{
var w = new StreamWriter(s);
w.BaseStream.Write(data, 0, data.Length);
w.Flush(); //flush the writer once we're done
}
//assign the converted blob data
imageFile.BinaryData = blob;
/* Set the name of the file */
imageFile.Name = $"{Path.GetFileNameWithoutExtension(currentImagePath)}{fileExtension}";
/*add alt text otherwise the SaveAction.Publish will error*/
imageFile.Description = $"{currentImage.Name} - This image was autogenerated.";
//imageFile.Description = $"{currentImage.Description} - This image was autogenerated.";
/* Consider providing an async .Save as per DXC Training page 62*/
ContentReference convertedImage = contentRepository.Save(imageFile, SaveAction.Publish, AccessLevel.NoAccess);
return convertedImage;
}
}
Lot's of borrowed code up and typos in there which we've ungracefully smashed together until the errors went away and converted PNGs were delivered to the browser!
So, as the title says, how would you improve this code? Any thoughts or constructive critisisms are most welcome.
Alex