Migrating from EPiFocalPoint to ImagePointEditor in a CMS 12 upgrade
Upgraded a CMS 11 solution that was using EPiFocalPoint.
EPiFocalPoint by Stephan Lonntorp is a now archived repository that provided a custom property type where the editor could set a focal point in images that ImageResizer then used when cropping.
The repository produced the NuGet package ImageResizer.Plugins.EPiFocalPoint.
Since the package wasn't compatible with modern dotnet and CMS 12 it was removed in the upgraded solution.
To be able to work with the old data I added this base class to the CMS 12 solution.
using System.ComponentModel.DataAnnotations;
using EPiServer.Core;
using EPiServer.Web;
// ReSharper disable once CheckNamespace
namespace ImageResizer.Plugins.EPiFocalPoint;
public abstract class FocalPointImageData : ImageData
{
[UIHint(UIHint.Textarea)]
[Display(
Name = "Focal Point (old plugin)",
Order = 620)]
public virtual string FocalPoint { get; set; }
[Display(
Name = "Width (old plugin)",
Order = 621)]
public virtual int? OriginalWidth { get; set; }
[Display(
Name = "Height (old plugin)",
Order = 622)]
public virtual int? OriginalHeight { get; set; }
}
To remove the dependency on the old custom property type I also needed to run this SQL. Be careful and check that it doesn't do too much in your database!
UPDATE [tblPropertyDefinition]
SET fkPropertyDefinitionTypeID = (SELECT pkID FROM [tblPropertyDefinitionType] WHERE [Name] = 'LongString')
WHERE [Name] = 'FocalPoint'
Now we could read the old plugin's JSON data in a default LongString on images inheriting from FocalPointImageData
.
ImagePointEditor by Erik Henningson seemed like a good replacement in the CMS 12 solution, where the NuGet package Baaijte.Optimizely.ImageSharp.Web was also pinpointed to be used.
It seemed wise to use the same new property name as the documentation suggested. Also followed the other parts and checked that the focal point UI worked.
[UIHint(ImagePoint.UIHint)]
[Display(
Name = "Focal Point",
Order = 610)]
public virtual string ImageFocalPoint { get; set; }
At this stage everything was ready to copy the coordinates from the old JSON data into the new tab delimited values that ImagePointEditor uses.
I created a scheduled job to handle this.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAccess;
using EPiServer.PlugIn;
using EPiServer.Scheduler;
using EPiServer.Security;
[ScheduledPlugIn(
DisplayName = "[TEMP] Migrate Focal Points to ImagePointEditor",
Description = "Converts focal point coordinates to match how new plugin works.",
SortIndex = 1200,
GUID = "fef529d7-7a40-40f8-bb81-735e31867be0")]
public class MigrateFocalPoint : ScheduledJobBase
{
private static readonly CultureInfo UsCultureInfo = new("en-US");
private readonly IContentTypeRepository contentTypeRepository;
private readonly IContentModelUsage contentModelUsage;
private readonly IContentRepository contentRepository;
public MigrateFocalPoint(
IContentTypeRepository contentTypeRepository,
IContentModelUsage contentModelUsage,
IContentRepository contentRepository)
{
this.contentTypeRepository = contentTypeRepository;
this.contentModelUsage = contentModelUsage;
this.contentRepository = contentRepository;
}
public override string Execute()
{
this.OnStatusChanged($"Starts processing");
var processed = 0;
var errors = new List<string>();
// Adjust for the types your solution uses
var imageFileContentType = this.contentTypeRepository.Load(typeof(ImageFile));
this.ProcessType<ImageFile>(imageFileContentType, ref processed, errors);
var svgFileContentType = this.contentTypeRepository.Load(typeof(SvgFile));
this.ProcessType<SvgFile>(svgFileContentType, ref processed, errors);
return $"Handled conversions: {processed}. Errors: {errors.Count}. ({string.Join(", ", errors)})";
}
private void ProcessType<T>(ContentType contentType, ref int processed, List<string> errors)
where T : ImageFile
{
var list = this.contentModelUsage.ListContentOfContentType(contentType);
foreach (var file in list)
{
if (!contentRepository.TryGet<T>(file.ContentLink, out var content))
{
continue;
}
if (content == null)
{
continue;
}
if (content.Status != VersionStatus.Published)
{
continue;
}
if (content.FocalPoint == null || !content.FocalPoint.Contains(":"))
{
continue;
}
if (content.ImageFocalPoint != null)
{
continue;
}
processed++;
this.OnStatusChanged($"{processed}: {content.Name}");
var oldFocalPoint = JsonSerializer.Deserialize<OldFocalPointJson>(content.FocalPoint);
var newX = oldFocalPoint.X / 100;
var newY = oldFocalPoint.Y / 100;
try
{
var clone = (T)content.CreateWritableClone();
clone.ImageFocalPoint = $"{newX.ToString(UsCultureInfo)}|{newY.ToString(UsCultureInfo)}";
clone.SetChangedOnPublish = true;
this.contentRepository.Save(clone, SaveAction.Publish, AccessLevel.NoAccess);
}
catch (Exception)
{
errors.Add($"{content.Name} ID: {content.ContentLink.ID}");
}
}
}
}
public class OldFocalPointJson
{
[JsonPropertyName("x")]
public double X { get; set; }
[JsonPropertyName("y")]
public double Y { get; set; }
}
After running, focal points stored in the new property matched the old value's intent.
Using the focal point
If you want to rewrite image URLs on your own, without using the PictureRenderer.Optimizely package also by Erik Henningson, you need to set a query parameter named rxy
that Baaijte.Optimizely.ImageSharp.Web picks up.
Here's some pseudo code for that, coming into place from inspecting the mentioned repositories.
string focalPoint = null;
if (image?.Property["ImageFocalPoint"]?.Value != null)
{
focalPoint = image.Property["ImageFocalPoint"].ToString();
}
if (focalPoint != null
&& focalPoint.Contains("|"))
{
var split = focalPoint.Split('|');
if (split.Length == 2
&& double.TryParse(split[0], UsCultureInfo, out var x)
&& double.TryParse(split[1], UsCultureInfo, out var y))
{
var rxy = $"{this.FormatFocalPointForResizer(x)},{this.FormatFocalPointForResizer(y)}";
// TODO: Add this as query parameter value for key "rxy"
}
}
private string FormatFocalPointForResizer(double input)
{
return Math.Round(input, 3).ToString(UsCultureInfo);
}
Comments?
Published and tagged with these categories: Optimizely