From ec14a16aecc20afb08795b220fc2f5d806511d9f Mon Sep 17 00:00:00 2001 From: perploug Date: Mon, 17 Feb 2014 16:15:39 +0100 Subject: [PATCH] ImageCropper Razor models + helpers --- src/Umbraco.Web/ImageCropperBaseExtensions.cs | 169 +++++++++++++++++ .../ImageCropperTemplateExtensions.cs | 170 ++++++++++++++++++ src/Umbraco.Web/Models/ImageCropData.cs | 100 +++++++++++ .../ImageCropperPrevalueController.cs | 27 +++ .../ImageCropperPropertyEditorHelper.cs | 54 ++++++ .../ImageCropperPropertyValueEditor.cs | 121 +++++++++++++ 6 files changed, 641 insertions(+) create mode 100644 src/Umbraco.Web/ImageCropperBaseExtensions.cs create mode 100644 src/Umbraco.Web/ImageCropperTemplateExtensions.cs create mode 100644 src/Umbraco.Web/Models/ImageCropData.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ImageCropperPrevalueController.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditorHelper.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs diff --git a/src/Umbraco.Web/ImageCropperBaseExtensions.cs b/src/Umbraco.Web/ImageCropperBaseExtensions.cs new file mode 100644 index 0000000000..53a62853a2 --- /dev/null +++ b/src/Umbraco.Web/ImageCropperBaseExtensions.cs @@ -0,0 +1,169 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Web.Models; + +namespace Umbraco.Web +{ + internal static class ImageCropperBaseExtensions + { + internal static bool IsJson(this string input) + { + input = input.Trim(); + return input.StartsWith("{") && input.EndsWith("}") || input.StartsWith("[") && input.EndsWith("]"); + } + + internal static ImageCropData GetImageCrop(this string json, string id) + { + var ic = new ImageCropData(); + if (IsJson(json)) + { + try + { + var imageCropperSettings = JsonConvert.DeserializeObject>(json); + ic = imageCropperSettings.First(p => p.Alias == id); + } + catch { } + } + return ic; + } + + + internal static ImageCropDataSet SerializeToCropDataSet(this string json) + { + var imageCrops = new ImageCropDataSet(); + if (IsJson(json)) + { + try + { + imageCrops = JsonConvert.DeserializeObject(json); + } + catch + { + + } + } + + return imageCrops; + } + + + + internal static bool HasPropertyAndValue(this IPublishedContent publishedContent, string propertyAlias) + { + try + { + if (propertyAlias != null && publishedContent.HasProperty(propertyAlias) + && publishedContent.HasValue(propertyAlias)) + { + var propertyAliasValue = publishedContent.GetPropertyValue(propertyAlias); + if (propertyAliasValue.IsJson() && propertyAliasValue.Length <= 2) + { + return false; + } + return true; + } + } + catch (Exception ex) + { + LogHelper.Warn("The cache unicorn is not happy with node id: " + publishedContent.Id + " - http://issues.umbraco.org/issue/U4-4146"); + + var cropsProperty = publishedContent.Properties.FirstOrDefault(x => x.PropertyTypeAlias == propertyAlias); + + if (cropsProperty != null && !string.IsNullOrEmpty(cropsProperty.Value.ToString())) + { + var propertyAliasValue = cropsProperty.Value.ToString(); + if (propertyAliasValue.IsJson() && propertyAliasValue.Length <= 2) + { + return false; + } + return true; + } + } + return false; + } + + internal static bool HasPropertyAndValueAndCrop(this IPublishedContent publishedContent, string propertyAlias, string cropAlias) + { + try + { + if (propertyAlias != null && publishedContent.HasProperty(propertyAlias) + && publishedContent.HasValue(propertyAlias)) + { + var propertyAliasValue = publishedContent.GetPropertyValue(propertyAlias); + if (propertyAliasValue.IsJson() && propertyAliasValue.Length <= 2) + { + return false; + } + var allTheCrops = propertyAliasValue.SerializeToCropDataSet(); + if (allTheCrops != null && allTheCrops.Crops.Any()) + { + var crop = cropAlias != null + ? allTheCrops.Crops[cropAlias] + : allTheCrops.Crops.First().Value; + if (crop != null) + { + return true; + } + } + return false; + } + } + catch (Exception ex) + { + LogHelper.Warn("The cache unicorn is not happy with node id: " + publishedContent.Id + " - http://issues.umbraco.org/issue/U4-4146"); + var cropsProperty = publishedContent.Properties.FirstOrDefault(x => x.PropertyTypeAlias == propertyAlias); + + if (cropsProperty != null && !string.IsNullOrEmpty(cropsProperty.Value.ToString())) + { + var propertyAliasValue = cropsProperty.Value.ToString(); + if (propertyAliasValue.IsJson() && propertyAliasValue.Length <= 2) + { + return false; + } + var allTheCrops = propertyAliasValue.SerializeToCropDataSet(); + if (allTheCrops != null && allTheCrops.Crops.Any()) + { + var crop = cropAlias != null + ? allTheCrops.Crops[cropAlias] + : allTheCrops.Crops.First().Value; + if (crop != null) + { + return true; + } + } + return false; + } + } + return false; + } + + internal static string GetPropertyValueHack(this IPublishedContent publishedContent, string propertyAlias) + { + string propertyValue = null; + try + { + if (propertyAlias != null && publishedContent.HasProperty(propertyAlias) + && publishedContent.HasValue(propertyAlias)) + { + propertyValue = publishedContent.GetPropertyValue(propertyAlias); + } + } + catch (Exception ex) + { + var cropsProperty = publishedContent.Properties.FirstOrDefault(x => x.PropertyTypeAlias == propertyAlias); + if (cropsProperty != null) + { + propertyValue = cropsProperty.Value.ToString(); + } + } + return propertyValue; + } + } +} diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs new file mode 100644 index 0000000000..62fb504f7a --- /dev/null +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Web +{ + public static class ImageCropperTemplateExtensions + { + public static bool HasCrop(this IPublishedContent publishedContent, string propertyAlias, string cropAlias) + { + return ImageCropperPropertyEditorHelper.GetCrop(publishedContent.ContentType.Alias, cropAlias) != null; + } + + //this only takes the crop json into account + public static string Crop(this IPublishedContent mediaItem, string propertyAlias, string cropAlias) + { + var property = mediaItem.GetPropertyValue(propertyAlias); + + if (string.IsNullOrEmpty(property)) + return string.Empty; + + if (property.IsJson()) + { + var cropDataSet = property.SerializeToCropDataSet(); + var currentCrop = cropDataSet.Crops[cropAlias]; + return cropDataSet.Src + currentCrop.ToUrl(); + } + else + { + //must be a string + var cropData = ImageCropperPropertyEditorHelper.GetCrop(mediaItem.ContentType.Alias, cropAlias); + return property + cropData.ToUrl(); + } + } + + + + + + public static string Crop( + this IPublishedContent mediaItem, + int? width = null, + int? height = null, + int? quality = null, + Mode? mode = null, + Anchor? anchor = null, + string imageCropperAlias = null, + string imageCropperCropId = null, + string furtherOptions = null, + bool slimmage = false) + { + string imageCropperValue = null; + + if (mediaItem.HasPropertyAndValueAndCrop(imageCropperAlias, imageCropperCropId)) + { + imageCropperValue = mediaItem.GetPropertyValueHack(imageCropperAlias); + } + + return mediaItem != null ? Crop(mediaItem.Url, width, height, quality, mode, anchor, imageCropperValue, imageCropperCropId, furtherOptions, slimmage) : string.Empty; + } + + + public static string Crop( + this string imageUrl, + int? width = null, + int? height = null, + int? quality = null, + Mode? mode = null, + Anchor? anchor = null, + string imageCropperValue = null, + string imageCropperCropId = null, + string furtherOptions = null, + bool slimmage = false) + { + if (!string.IsNullOrEmpty(imageUrl)) + { + var imageResizerUrl = new StringBuilder(); + imageResizerUrl.Append(imageUrl); + + if (!string.IsNullOrEmpty(imageCropperValue) && imageCropperValue.IsJson()) + { + var allTheCrops = imageCropperValue.SerializeToCropDataSet(); + if (allTheCrops != null && allTheCrops.Crops.Any()) + { + var crop = imageCropperCropId != null + ? allTheCrops.Crops[imageCropperCropId] + : allTheCrops.Crops.First().Value; + if (crop != null) + { + imageResizerUrl.Append(crop.ToUrl()); + } + } + } + else + { + if (mode == null) + { + mode = Mode.Pad; + } + imageResizerUrl.Append("?mode=" + mode.ToString().ToLower()); + + if (anchor != null) + { + imageResizerUrl.Append("&anchor=" + anchor.ToString().ToLower()); + } + } + + if (quality != null) + { + imageResizerUrl.Append("&quality=" + quality); + } + + if (width != null) + { + imageResizerUrl.Append("&width=" + width); + } + + if (height != null) + { + imageResizerUrl.Append("&height=" + height); + } + + if (slimmage) + { + if (width == null) + { + imageResizerUrl.Append("&width=300"); + } + if (quality == null) + { + imageResizerUrl.Append("&quality=90"); + } + imageResizerUrl.Append("&slimmage=true"); + } + + if (furtherOptions != null) + { + imageResizerUrl.Append(furtherOptions); + } + + return imageResizerUrl.ToString(); + + } + return string.Empty; + } + + + + public enum Mode + { + Crop, + Max, + Strech, + Pad + } + + public enum Anchor + { + Center, + Top, + Right, + Bottom, + Left + } + } +} diff --git a/src/Umbraco.Web/Models/ImageCropData.cs b/src/Umbraco.Web/Models/ImageCropData.cs new file mode 100644 index 0000000000..ad0448e8a7 --- /dev/null +++ b/src/Umbraco.Web/Models/ImageCropData.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.Models +{ + [DataContract(Name="imageCropDataSet")] + public class ImageCropDataSet + { + [DataMember(Name="src")] + public string Src { get; set;} + + [DataMember(Name = "imagePropertyAlias")] + public string ImagePropertyAlias { get; set; } + + [DataMember(Name = "focalPointPropertyAlias")] + public string FocalPointPropertyAlias { get; set; } + + [DataMember(Name = "focalPoint")] + public ImageCropFocalPoint FocalPoint { get; set; } + + [DataMember(Name = "crops")] + public IDictionary Crops { get; set; } + } + + [DataContract(Name = "imageCropFocalPoint")] + public class ImageCropFocalPoint{ + + [DataMember(Name = "left")] + public decimal Left { get; set; } + + [DataMember(Name = "top")] + public decimal Top { get; set; } + } + + [DataContract(Name = "imageCropCoordinates")] + public class ImageCropCoordinates + { + [DataMember(Name = "x1")] + public decimal X1 { get; set; } + + [DataMember(Name = "y1")] + public decimal Y1 { get; set; } + + [DataMember(Name = "x2")] + public decimal X2 { get; set; } + + [DataMember(Name = "y2")] + public decimal Y2 { get; set; } + } + + + [DataContract(Name = "imageCropData")] + public class ImageCropData + { + [DataMember(Name = "alias")] + public string Alias { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "width")] + public int Width { get; set; } + + [DataMember(Name = "height")] + public int Height { get; set; } + + [DataMember(Name = "coordinates")] + public ImageCropCoordinates Coordinates { get; set; } + + public string ToUrl() + { + StringBuilder sb = new StringBuilder(); + if (Coordinates != null) + { + sb.Append("?crop="); + sb.Append(Coordinates.X1).Append(","); + sb.Append(Coordinates.Y1).Append(","); + sb.Append(Coordinates.X2).Append(","); + sb.Append(Coordinates.Y2); + sb.Append("&cropmode=percentage"); + } + else + { + sb.Append("?anchor=center"); + sb.Append("&mode=crop"); + } + + sb.Append("&width=").Append(Width); + sb.Append("&height=").Append(Height); + sb.Append("&rnd=").Append(DateTime.Now.Ticks); + return sb.ToString(); + + } + } + +} diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPrevalueController.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPrevalueController.cs new file mode 100644 index 0000000000..1a1f7ae6ec --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPrevalueController.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Configuration; +using Umbraco.Web.Editors; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.PropertyEditors +{ + + public class ImageCropperPreValueController : UmbracoAuthorizedJsonController + { + //Returns the collection of allowed crops on a given media alias type + public Models.ImageCropDataSet GetConfiguration(string mediaTypeAlias) + { + return ImageCropperPropertyEditorHelper.GetConfigurationForType(mediaTypeAlias); + } + + //Returns a specific crop on a media type + public Models.ImageCropData GetCrop(string mediaTypeAlias, string cropAlias) + { + return ImageCropperPropertyEditorHelper.GetCrop(mediaTypeAlias, cropAlias); + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditorHelper.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditorHelper.cs new file mode 100644 index 0000000000..ff9432a5af --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditorHelper.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web.PropertyEditors +{ + internal class ImageCropperPropertyEditorHelper + { + //Returns the collection of allowed crops on a given media alias type + internal static Models.ImageCropDataSet GetConfigurationForType(string mediaTypeAlias) + { + var defaultModel = new Models.ImageCropDataSet(); + defaultModel.FocalPoint = new Models.ImageCropFocalPoint() { Left = 0.5M, Top = 0.5M }; + + var configuredCrops = UmbracoConfig.For.UmbracoSettings().Content.ImageCrops.Crops.FirstOrDefault(x => x.MediaTypeAlias == mediaTypeAlias); + if (configuredCrops == null || !configuredCrops.CropSizes.Any()) + return defaultModel; + + var crops = new Dictionary(); + foreach (var cropSize in configuredCrops.CropSizes) + crops.Add(cropSize.Alias, new Models.ImageCropData() { Alias = cropSize.Alias, Height = cropSize.Height, Width = cropSize.Width }); + + + defaultModel.Crops = crops; + return defaultModel; + } + + internal static Umbraco.Web.Models.ImageCropData GetCrop(string mediaTypeAlias, string cropAlias){ + + var _crops = GetConfigurationForType(mediaTypeAlias); + + if (_crops == null || _crops.Crops == null) + return null; + + return _crops.Crops[cropAlias]; + } + + //this queries all crops configured + internal static Umbraco.Web.Models.ImageCropData GetCrop(string cropAlias) + { + foreach (var typeCrops in UmbracoConfig.For.UmbracoSettings().Content.ImageCrops.Crops) + { + var cropSize = typeCrops.CropSizes.FirstOrDefault(x => x.Alias == cropAlias); + if(cropSize != null) + return new Models.ImageCropData() { Alias = cropSize.Alias, Height = cropSize.Height, Width = cropSize.Width }; + } + + return null; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs new file mode 100644 index 0000000000..35f24aa23c --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -0,0 +1,121 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Media; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.PropertyEditors +{ + internal class ImageCropperPropertyValueEditor : PropertyValueEditorWrapper + { + public ImageCropperPropertyValueEditor(PropertyValueEditor wrapped) : base(wrapped) + { + + } + + /// + /// Overrides the deserialize value so that we can save the file accordingly + /// + /// + /// This is value passed in from the editor. We normally don't care what the editorValue.Value is set to because + /// we are more interested in the files collection associated with it, however we do care about the value if we + /// are clearing files. By default the editorValue.Value will just be set to the name of the file (but again, we + /// just ignore this and deal with the file collection in editorValue.AdditionalData.ContainsKey("files") ) + /// + /// + /// The current value persisted for this property. This will allow us to determine if we want to create a new + /// file path or use the existing file path. + /// + /// + public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) + { + string oldFile = string.Empty; + string newFile = string.Empty; + JObject newJson = null; + JObject oldJson = null; + + //get the old src path + if (currentValue != null) + { + oldJson = currentValue as JObject; + if (oldJson != null && oldJson["src"] != null) + oldFile = oldJson["src"].Value(); + } + + //get the new src path + if (editorValue.Value != null) + { + newJson = editorValue.Value as JObject; + if (newJson != null && newJson["src"] != null) + newFile = newJson["src"].Value(); + } + + //compare old and new src path + //if not alike, that means we have a new file, or delete the current one... + if (oldFile != newFile) + { + var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + + //if we have an existing file, delete it + if (!string.IsNullOrEmpty(oldFile)) + fs.DeleteFile(fs.GetRelativePath(oldFile), true); + else + oldFile = string.Empty; + + //if we have a new file, add it to the media folder and set .src + + if (editorValue.AdditionalData.ContainsKey("files")) + { + var files = editorValue.AdditionalData["files"] as IEnumerable; + if (files != null && files.Any()) + { + var file = files.First(); + + if (UploadFileTypeValidator.ValidateFileExtension(file.FileName)) + { + //create name and folder number + var name = IOHelper.SafeFileName(file.FileName.Substring(file.FileName.LastIndexOf(IOHelper.DirSepChar) + 1, file.FileName.Length - file.FileName.LastIndexOf(IOHelper.DirSepChar) - 1).ToLower()); + + //try to reuse the folder number from the current file + var subfolder = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories + ? oldFile.Replace(fs.GetUrl("/"), "").Split('/')[0] + : oldFile.Substring(oldFile.LastIndexOf("/", StringComparison.Ordinal) + 1).Split('-')[0]; + + //if we dont find one, create a new one + int subfolderId; + var numberedFolder = int.TryParse(subfolder, out subfolderId) + ? subfolderId.ToString(CultureInfo.InvariantCulture) + : MediaSubfolderCounter.Current.Increment().ToString(CultureInfo.InvariantCulture); + + //set a file name or full path + var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories + ? Path.Combine(numberedFolder, name) + : numberedFolder + "-" + name; + + //save file and assign to the json + using (var fileStream = System.IO.File.OpenRead(file.TempFilePath)) + { + var umbracoFile = UmbracoMediaFile.Save(fileStream, fileName); + newJson["src"] = umbracoFile.Url; + + return newJson.ToString(); + } + } + } + } + } + + return editorValue.Value.ToString(); + } + } +}