diff --git a/components/editorControls/imagecropper/Config.cs b/components/editorControls/imagecropper/Config.cs new file mode 100644 index 0000000000..1ee4be2f51 --- /dev/null +++ b/components/editorControls/imagecropper/Config.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections; + +namespace umbraco.editorControls.imagecropper +{ + public class Config + { + public string UploadPropertyAlias { get; set; } + public bool GenerateImages { get; set; } + public int Quality { get; set; } + public bool ShowLabel { get; set; } + public ArrayList presets { get; set; } + + public Config(string configuration) + { + presets = new ArrayList(); + + string[] configData = configuration.Split('|'); + + if (configData.Length != 2) return; + + string[] generalSettings = configData[0].Split(','); + + UploadPropertyAlias = generalSettings[0]; + GenerateImages = generalSettings[1] == "1"; + ShowLabel = generalSettings[2] == "1"; + + int _quality; + if(generalSettings.Length >= 4 && Int32.TryParse(generalSettings[3], out _quality)) + { + Quality = _quality; + } + else + { + Quality = 90; + } + + string[] presetData = configData[1].Split(';'); + + for (int i=0; i < presetData.Length; i++) + { + string[] p = presetData[i].Split(','); + + int targetWidth, targetHeight; + + if (p.Length >= 4 && Int32.TryParse(p[1], out targetWidth) && Int32.TryParse(p[2], out targetHeight)) + { + char[] cropPosition = { 'C', 'M' }; + + if(p.Length >= 5) + { + cropPosition = p[4].ToCharArray(); + } + + presets.Add(new Preset(p[0], targetWidth, targetHeight, p[3] == "1" ? true : false, cropPosition[0].ToString(), cropPosition[1].ToString())); + } + } + } + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/Data.cs b/components/editorControls/imagecropper/Data.cs new file mode 100644 index 0000000000..b8219279e7 --- /dev/null +++ b/components/editorControls/imagecropper/Data.cs @@ -0,0 +1,119 @@ +namespace umbraco.editorControls.imagecropper +{ + struct Crop + { + public int X; + public int Y; + public int X2; + public int Y2; + + public Crop(int x, int y, int x2, int y2) + { + X = x; + Y = y; + X2 = x2; + Y2 = y2; + } + } + + enum DefaultCropPosition + { + CenterCenter = 0, + CenterTop, + CenterBottom, + LeftCenter, + LeftTop, + LeftBottom, + RightCenter, + RightTop, + RightBottom + } + + struct Preset + { + public string Name; + public int TargetWidth; + public int TargetHeight; + public bool KeepAspect; + public string PositionH; + public string PositionV; + + public float Aspect + { + get { return (float)TargetWidth / TargetHeight; } + } + + public Crop Fit(ImageInfo imageInfo) + { + Crop crop; + + if (Aspect >= imageInfo.Aspect) + { + // crop widest hor ver + // relevant positioning: center top, center center, center bottom + + float h = ((float)imageInfo.Width / TargetWidth) * TargetHeight; + + crop.X = 0; + crop.X2 = imageInfo.Width; + + switch(PositionV) + { + case "T": + crop.Y = 0; + crop.Y2 = (int)h; + break; + case "B": + crop.Y = imageInfo.Height - (int)h; + crop.Y2 = imageInfo.Height; + break; + default: // CC + crop.Y = (int)(imageInfo.Height - h) / 2; + crop.Y2 = (int)(crop.Y + h); + break; + } + } + else + { + + // image widest + // relevant positioning: left/right center, left/right top, left/right bottom + + float w = ((float)imageInfo.Height / TargetHeight) * TargetWidth; + + crop.Y = 0; + crop.Y2 = imageInfo.Height; + + switch (PositionH) + { + case "L": + crop.X = 0; + crop.X2 = (int)w; + break; + case "R": + crop.X = imageInfo.Width - (int)w; + crop.X2 = imageInfo.Width; + break; + default: // CC + crop.X = (int) (imageInfo.Width - w)/2; + crop.X2 = (int) (crop.X + w); + break; + } + + } + + return crop; + } + + public Preset(string name, int targetWidth, int targetHeight, bool keepAspect, string positionH, string positionV) + { + Name = name; + TargetWidth = targetWidth; + TargetHeight = targetHeight; + KeepAspect = keepAspect; + PositionH = positionH; + PositionV = positionV; + } + + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/DataEditor.cs b/components/editorControls/imagecropper/DataEditor.cs new file mode 100644 index 0000000000..20abb3f990 --- /dev/null +++ b/components/editorControls/imagecropper/DataEditor.cs @@ -0,0 +1,192 @@ +using System; +using System.Web.UI; +using System.Web.UI.WebControls; +using umbraco.cms.businesslogic.property; +using umbraco.cms.businesslogic.web; +using System.Xml; +using System.Text; +using umbraco.editorControls.imagecropper; + +namespace umbraco.editorControls.imagecropper +{ + public class DataEditor : PlaceHolder, umbraco.interfaces.IDataEditor + { + private umbraco.interfaces.IData data; + private Config config; + private XmlDocument _xml; + + public Image imgImage = new Image(); + public HiddenField hdnJson = new HiddenField(); + public HiddenField hdnRaw = new HiddenField(); + public HiddenField hdnSer = new HiddenField(); + + public DataEditor(umbraco.interfaces.IData Data, string Configuration) + { + data = Data; + config = new Config(Configuration); + } + + public virtual bool TreatAsRichTextEditor { get { return false; } } + + public bool ShowLabel { get { return config.ShowLabel; } } + + public Control Editor { get { return this; } } + + protected override void OnInit(EventArgs e) + //protected override void OnLoad(EventArgs e) + { + this.ID = "ImageCropper"; + //base.OnInit(e); + + int propertyId = ((umbraco.cms.businesslogic.datatype.DefaultData) data).PropertyId; + + int currentDocumentId = ((umbraco.cms.businesslogic.datatype.DefaultData)data).NodeId; + Document currentDocument = new Document(currentDocumentId); + + Property uploadProperty = currentDocument.getProperty(config.UploadPropertyAlias); + + if(uploadProperty == null) return; + + string relativeImagePath = uploadProperty.Value.ToString(); + + ImageInfo imageInfo = new ImageInfo(relativeImagePath); + + imgImage.ImageUrl = relativeImagePath; + imgImage.ID = String.Format("cropBox_{0}", propertyId); + + StringBuilder sbJson = new StringBuilder(); + StringBuilder sbRaw = new StringBuilder(); + + try + { + _xml = new XmlDocument(); + _xml.LoadXml(data.Value.ToString()); + } + catch + { + _xml = createBaseXmlDocument(); + } + + sbJson.Append("{ \"current\": 0, \"crops\": ["); + + for (int i = 0; i < config.presets.Count; i++) + { + Preset preset = (Preset) config.presets[i]; + Crop crop; + + sbJson.Append("{\"name\":'" + preset.Name + "'"); + + sbJson.Append(",\"config\":{" + + String.Format("\"targetWidth\":{0},\"targetHeight\":{1},\"keepAspect\":{2}", + preset.TargetWidth, preset.TargetHeight, + (preset.KeepAspect ? "true" : "false") + "}")); + + if (imageInfo.Exists) + { + crop = preset.Fit(imageInfo); + } + else + { + crop.X = 0; + crop.Y = 0; + crop.X2 = preset.TargetWidth; + crop.Y2 = preset.TargetHeight; + } + + // stored + if (_xml.DocumentElement != null && _xml.DocumentElement.ChildNodes.Count == config.presets.Count) + { + XmlNode xmlNode = _xml.DocumentElement.ChildNodes[i]; + + int xml_x = Convert.ToInt32(xmlNode.Attributes["x"].Value); + int xml_y = Convert.ToInt32(xmlNode.Attributes["y"].Value); + int xml_x2 = Convert.ToInt32(xmlNode.Attributes["x2"].Value); + int xml_y2 = Convert.ToInt32(xmlNode.Attributes["y2"].Value); + + DateTime fileDate = Convert.ToDateTime(_xml.DocumentElement.Attributes["date"].Value); + + // only use xml values if image is the same and different from defaults (document is stored inbetween image upload and cropping) + //if (xml_x2 - xml_x != preset.TargetWidth || xml_y2 - xml_y != preset.TargetHeight) + //fileDate == imageInfo.DateStamp && ( + + if(crop.X != xml_x || crop.X2 != xml_x2 || crop.Y != xml_y || crop.Y2 != xml_y2) + { + crop.X = xml_x; + crop.Y = xml_y; + crop.X2 = xml_x2; + crop.Y2 = xml_y2; + } + } + + sbJson.Append(",\"value\":{" + String.Format("\"x\":{0},\"y\":{1},\"x2\":{2},\"y2\":{3}", crop.X, crop.Y, crop.X2, crop.Y2) + "}}"); + sbRaw.Append(String.Format("{0},{1},{2},{3}", crop.X, crop.Y, crop.X2, crop.Y2)); + + if (i < config.presets.Count - 1) + { + sbJson.Append(","); + sbRaw.Append(";"); + } + } + + sbJson.Append("]}"); + + hdnJson.Value = sbJson.ToString(); + //hdnJson.ID = String.Format("json_{0}", propertyId); + hdnRaw.Value = sbRaw.ToString(); + //hdnRaw.ID = String.Format("raw_{0}", propertyId); + + Controls.Add(imgImage); + + Controls.Add(hdnJson); + Controls.Add(hdnRaw); + + string imageCropperInitScript = + "initImageCropper('" + + imgImage.ClientID + "', '" + + hdnJson.ClientID + "', '" + + hdnRaw.ClientID + + "');"; + + Page.ClientScript.RegisterStartupScript(GetType(), ClientID + "_imageCropper", imageCropperInitScript, true); + Page.ClientScript.RegisterClientScriptBlock(Resources.json2Script.GetType(), "json2Script", Resources.json2Script, true); + Page.ClientScript.RegisterClientScriptBlock(Resources.jCropCSS.GetType(), "jCropCSS", Resources.jCropCSS); + Page.ClientScript.RegisterClientScriptBlock(Resources.jCropScript.GetType(), "jCropScript", Resources.jCropScript, true); + Page.ClientScript.RegisterClientScriptBlock(Resources.imageCropperScript.GetType(), "imageCropperScript", Resources.imageCropperScript, true); + + + base.OnInit(e); + } + + + /// + /// Store data as string XML (overridden by ToXMl to store "real" XML + /// XML format: + /// + /// + /// + /// + public void Save() + { + ImageInfo imageInfo = new ImageInfo(imgImage.ImageUrl); + if (!imageInfo.Exists) + { + data.Value = ""; + } + else + { + SaveData saveData = new SaveData(hdnRaw.Value); + data.Value = saveData.Xml(config, imageInfo); + imageInfo.GenerateThumbnails(saveData, config); + } + } + + private static XmlDocument createBaseXmlDocument() + { + XmlDocument doc = new XmlDocument(); + XmlNode root = doc.CreateElement("crops"); + doc.AppendChild(root); + return doc; + } + + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/DataType.cs b/components/editorControls/imagecropper/DataType.cs new file mode 100644 index 0000000000..6dd266de12 --- /dev/null +++ b/components/editorControls/imagecropper/DataType.cs @@ -0,0 +1,60 @@ +using System; +using System.Reflection; + +namespace umbraco.editorControls.imagecropper +{ + public class DataType : umbraco.cms.businesslogic.datatype.BaseDataType, umbraco.interfaces.IDataType + { + private umbraco.interfaces.IDataEditor _editor; + private umbraco.interfaces.IData _baseData; + private PrevalueEditor _prevalueEditor; + + public override umbraco.interfaces.IDataEditor DataEditor + { + get + { + if (_editor == null) + _editor = new DataEditor(Data, ((PrevalueEditor)PrevalueEditor).Configuration); + return _editor; + } + } + + public override umbraco.interfaces.IData Data + { + get + { + if (_baseData == null) + _baseData = new DataTypeData(this); + return _baseData; + } + } + public override Guid Id + { + get { return new Guid("7A2D436C-34C2-410F-898F-4A23B3D79F54"); } + } + + public override string DataTypeName + { + get { return "Image Cropper"; } + } + + public override umbraco.interfaces.IDataPrevalue PrevalueEditor + { + get + { + if (_prevalueEditor == null) + _prevalueEditor = new PrevalueEditor(this); + return _prevalueEditor; + } + } + + public static int Version + { + get + { + Version version = Assembly.GetExecutingAssembly().GetName().Version; + return version.Major*1000 + version.Minor*100; + } + } + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/DataTypeData.cs b/components/editorControls/imagecropper/DataTypeData.cs new file mode 100644 index 0000000000..7c667aaa24 --- /dev/null +++ b/components/editorControls/imagecropper/DataTypeData.cs @@ -0,0 +1,21 @@ +using System.Xml; + +namespace umbraco.editorControls.imagecropper +{ + public class DataTypeData : umbraco.cms.businesslogic.datatype.DefaultData + { + public DataTypeData(umbraco.cms.businesslogic.datatype.BaseDataType DataType) : base(DataType) { } + + public override XmlNode ToXMl(XmlDocument data) + { + if (Value.ToString() != "") { + XmlDocument xd = new XmlDocument(); + xd.LoadXml(Value.ToString()); + return data.ImportNode(xd.DocumentElement, true); + } else { + return base.ToXMl(data); + } + + } + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/Helper.cs b/components/editorControls/imagecropper/Helper.cs new file mode 100644 index 0000000000..1294ae367e --- /dev/null +++ b/components/editorControls/imagecropper/Helper.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Xml.Serialization; + +namespace umbraco.editorControls.imagecropper +{ + class Helper + { + + + public static string SerializeToString(object obj) + { + XmlSerializer serializer = new XmlSerializer(obj.GetType()); + + using (StringWriter writer = new StringWriter()) + { + serializer.Serialize(writer, obj); + + return writer.ToString(); + } + } + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/ImageInfo.cs b/components/editorControls/imagecropper/ImageInfo.cs new file mode 100644 index 0000000000..da2849081e --- /dev/null +++ b/components/editorControls/imagecropper/ImageInfo.cs @@ -0,0 +1,106 @@ +using System; +using System.Drawing; +using System.IO; +using System.Web; +using umbraco.editorControls.imagecropper; + +namespace umbraco.editorControls.imagecropper +{ + public class ImageInfo + { + public Image image { get; set; } + public string Name { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public float Aspect { get; set; } + public DateTime DateStamp { get; set; } + public string Path { get; set; } + public string RelativePath { get; set; } + + public ImageInfo(string relativePath) + { + RelativePath = relativePath; + Path = HttpContext.Current.Server.MapPath(relativePath); + if (File.Exists(Path)) + { + string fileName = Path.Substring(Path.LastIndexOf('\\') + 1); + Name = fileName.Substring(0, fileName.LastIndexOf('.')); + + byte[] buffer = null; + + using (FileStream fs = new FileStream(Path, FileMode.Open, FileAccess.Read)) + { + buffer = new byte[fs.Length]; + fs.Read(buffer, 0, (int) fs.Length); + fs.Close(); + } + + try + { + image = Image.FromStream(new MemoryStream(buffer)); + + Width = image.Width; + Height = image.Height; + Aspect = (float) Width/Height; + DateStamp = File.GetLastWriteTime(Path); + } + catch (Exception) + { + Width = 0; + Height = 0; + Aspect = 0; + } + + } + else + { + Width = 0; + Height = 0; + Aspect = 0; + } + } + + public bool Exists + { + get { return Width > 0 && Height > 0; } + } + + public string Directory + { + get { return Path.Substring(0, Path.LastIndexOf('\\')); } + } + + public void GenerateThumbnails(SaveData saveData, Config config) + { + if (config.GenerateImages) + { + for (int i = 0; i < config.presets.Count; i++) + { + Crop crop = (Crop) saveData.data[i]; + Preset preset = (Preset) config.presets[i]; + + // Crop rectangle bigger than actual image + if(crop.X2 - crop.X > Width || crop.Y2 - crop.Y > Height) + { + crop = preset.Fit(this); + } + + ImageTransform.Execute( + Path, + String.Format("{0}_{1}", Name, preset.Name), + crop.X, + crop.Y, + crop.X2 - crop.X, + crop.Y2 - crop.Y, + preset.TargetWidth, + preset.TargetHeight, + config.Quality + ); + //BasePage bp = new BasePage(); + //bp.speechBubble(BasePage.speechBubbleIcon.error, "Error", + // "One or more crops are out of bounds. Please correct and try again."); + } + } + } + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/ImageManipulation.cs b/components/editorControls/imagecropper/ImageManipulation.cs new file mode 100644 index 0000000000..3536b3a037 --- /dev/null +++ b/components/editorControls/imagecropper/ImageManipulation.cs @@ -0,0 +1,212 @@ +using System; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Drawing; +using System.IO; + +namespace umbraco.editorControls.imagecropper +{ + public class ImageTransform + { + public static void Execute(string sourceFile, string name, int cropX, int cropY, int cropWidth, int cropHeight, int sizeWidth, int sizeHeight, long quality) + { + if (!File.Exists(sourceFile)) return; + + string path = sourceFile.Substring(0, sourceFile.LastIndexOf('\\')); + + // TODO: Make configurable and move to imageInfo + //if(File.Exists(String.Format(@"{0}\{1}.jpg", path, name))) return; + + byte[] buffer = null; + + using(FileStream fs = new FileStream(sourceFile, FileMode.Open, FileAccess.Read)) + { + buffer = new byte[fs.Length]; + fs.Read(buffer, 0, (int)fs.Length); + fs.Close(); + } + + Image image = Image.FromStream(new MemoryStream(buffer)); + + DirectoryInfo di = new DirectoryInfo(path); + if (!di.Exists) di.Create(); + + using(Image croppedImage = cropImage(image, new Rectangle(cropX, cropY, cropWidth, cropHeight))) + { + using(Image resizedImage = resizeImage(croppedImage, new Size(sizeWidth, sizeHeight))) + { + using (Bitmap b = new Bitmap(resizedImage)) + { + saveJpeg(String.Format("{0}/{1}.jpg", path, name), b, quality); + } + } + } + + + //saveJpeg( + // String.Format("{0}/{1}.jpg", path, name), + // new Bitmap( + // resizeImage(cropImage(image, new Rectangle(cropX, cropY, cropWidth, cropHeight)), new Size(sizeWidth, sizeHeight))), + // quality + // ); + + //using (FileStream stm = new FileStream(sourceFile, FileMode.Open, FileAccess.Read)) + //{ + //using (Image image = Image.FromStream(stm)) + //{ + + //} + //stm.Close(); + //} + + + //using (Image image = Image.FromFile(sourceFile)) + //{ + // //image = cropImage(image, new Rectangle(cropX, cropY, cropWidth, cropHeight)); + // //cropImage(image, new Rectangle(cropX, cropY, cropWidth, cropHeight)); + // //image = resizeImage(image, new Size(sizeWidth, sizeHeight)); + // //resizeImage(image, new Size(sizeWidth, sizeHeight)); + // string path = sourceFile.Substring(0, sourceFile.LastIndexOf('\\') + 1) + "Crops"; + // DirectoryInfo di = new DirectoryInfo(path); + // if (!di.Exists) di.Create(); + // saveJpeg( + // String.Format("{0}/{1}.jpg", path, name), + // new Bitmap( + // resizeImage(cropImage(image, new Rectangle(cropX, cropY, cropWidth, cropHeight)), new Size(sizeWidth, sizeHeight))), + // quality + // ); + + // image.Dispose(); + //} + } + + private static void saveJpeg(string path, Bitmap img, long quality) + { + // Encoder parameter for image quality + EncoderParameter qualityParam = new EncoderParameter(Encoder.Quality, quality); + + // Jpeg image codec + ImageCodecInfo jpegCodec = getEncoderInfo("image/jpeg"); + + if (jpegCodec == null) + return; + + EncoderParameters encoderParams = new EncoderParameters(1); + encoderParams.Param[0] = qualityParam; + + img.Save(path, jpegCodec, encoderParams); + } + + private static ImageCodecInfo getEncoderInfo(string mimeType) + { + // Get image codecs for all image formats + ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders(); + + // Find the correct image codec + for (int i = 0; i < codecs.Length; i++) + if (codecs[i].MimeType == mimeType) + return codecs[i]; + return null; + } + + private static Image cropImage(Image img, Rectangle cropArea) + { + Bitmap bmpImage = new Bitmap(img); + Bitmap bmpCrop = bmpImage.Clone(cropArea, bmpImage.PixelFormat); + return (Image)(bmpCrop); + } + + private static Image resizeImage(Image imgToResize, Size size) + { + //int sourceWidth = imgToResize.Width; + //int sourceHeight = imgToResize.Height; + + //float nPercent = 0; + //float nPercentW = 0; + //float nPercentH = 0; + + //nPercentW = ((float)size.Width / (float)sourceWidth); + //nPercentH = ((float)size.Height / (float)sourceHeight); + + //if (nPercentH < nPercentW) + // nPercent = nPercentH; + //else + // nPercent = nPercentW; + + //int destWidth = (int)(sourceWidth * nPercent); + //int destHeight = (int)(sourceHeight * nPercent); + + + + + + + + int destWidth = size.Width; + int destHeight = size.Height; + + Bitmap b = new Bitmap(destWidth, destHeight); + + ImageAttributes ia = new ImageAttributes(); + ia.SetWrapMode(WrapMode.TileFlipXY); + + Graphics g = Graphics.FromImage(b); + g.PixelOffsetMode = PixelOffsetMode.HighQuality; + g.Clear(Color.White); + g.InterpolationMode = InterpolationMode.HighQualityBicubic; + g.CompositingQuality = CompositingQuality.HighQuality; + g.SmoothingMode = SmoothingMode.HighQuality; + g.DrawImage(imgToResize, new Rectangle(0, 0, destWidth, destHeight), 0, 0, imgToResize.Width, + imgToResize.Height, GraphicsUnit.Pixel, ia); + + ia.Dispose(); + g.Dispose(); + + return b; + + + + + +#if false + + int destWidth = size.Width; + int destHeight = size.Height; + + using (Bitmap b = new Bitmap(destWidth, destHeight)) + { + using (Graphics g = Graphics.FromImage(b)) + { + g.PixelOffsetMode = PixelOffsetMode.HighQuality; + using (ImageAttributes ia = new ImageAttributes()) + { + ia.SetWrapMode(WrapMode.TileFlipXY); + g.Clear(Color.White); + g.InterpolationMode = InterpolationMode.HighQualityBicubic; + g.CompositingQuality = CompositingQuality.HighQuality; + g.SmoothingMode = SmoothingMode.HighQuality; + g.DrawImage(imgToResize, new Rectangle(0, 0, destWidth, destHeight), 0, 0, imgToResize.Width, + imgToResize.Height, GraphicsUnit.Pixel, ia); + } + } + return b; + } + +#endif + +#if false + int destWidth = size.Width; + int destHeight = size.Height; + + Bitmap b = new Bitmap(destWidth, destHeight); + Graphics g = Graphics.FromImage((Image)b); + g.InterpolationMode = InterpolationMode.HighQualityBicubic; + + g.DrawImage(imgToResize, 0, 0, destWidth, destHeight); + g.Dispose(); + + return (Image)b; +#endif + } + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/PrevalueEditor.cs b/components/editorControls/imagecropper/PrevalueEditor.cs new file mode 100644 index 0000000000..1050c494b9 --- /dev/null +++ b/components/editorControls/imagecropper/PrevalueEditor.cs @@ -0,0 +1,413 @@ +using System; +using System.Web.UI; +using System.Web.UI.WebControls; +using umbraco.BusinessLogic; +using umbraco.editorControls; +using umbraco.DataLayer; +using umbraco.interfaces; + +namespace umbraco.editorControls.imagecropper +{ + public class PrevalueEditor : PlaceHolder, IDataPrevalue + { + private readonly umbraco.cms.businesslogic.datatype.BaseDataType _dataType; + + private TextBox txtPropertyAlias; + private CheckBox chkGenerateCrops; + private CheckBox chkShowLabel; + private Literal litQuality; + private TextBox txtQuality; + + private SmartListBox slbPresets; + private TextBox txtCropName; + private TextBox txtTargetWidth; + private TextBox txtTargetHeight; + private CheckBox chkKeepAspect; + private DropDownList ddlDefaultPosH; + private DropDownList ddlDefaultPosV; + + private Button btnUp; + private Button btnDown; + private Button btnAdd; + private Button btnRemove; + private Button btnGenerate; + + //private RegularExpressionValidator revName; + //private RequiredFieldValidator rqfName; + //private ValidationSummary vsErrors; + + public PrevalueEditor(umbraco.cms.businesslogic.datatype.BaseDataType dataType) + { + _dataType = dataType; + SetupChildControls(); + } + + public void SetupChildControls() + { + txtPropertyAlias = new TextBox {ID = "upload", Width = Unit.Pixel(100)}; + chkGenerateCrops = new CheckBox {ID = "generateimg", AutoPostBack = true}; + litQuality = new Literal {ID = "qualityLiteral", Text = " Quality ", Visible = false}; + txtQuality = new TextBox {ID = "quality", Width = Unit.Pixel(30), Visible = false}; + chkShowLabel = new CheckBox {ID = "label"}; + slbPresets = new SmartListBox + { + ID = "presets", + SelectionMode = ListSelectionMode.Multiple, + Height = Unit.Pixel(123), + Width = Unit.Pixel(350) + }; + + txtCropName = new TextBox {ID = "presetname", Width = Unit.Pixel(100)}; + txtTargetWidth = new TextBox {ID = "presetw", Width = Unit.Pixel(50)}; + txtTargetHeight = new TextBox {ID = "preseth", Width = Unit.Pixel(50)}; + chkKeepAspect = new CheckBox {ID = "aspect", Checked = true}; + + ddlDefaultPosH = new DropDownList {ID = "posh"}; + ddlDefaultPosH.Items.Add(new ListItem("Left", "L")); + ddlDefaultPosH.Items.Add(new ListItem("Center", "C")); + ddlDefaultPosH.Items.Add(new ListItem("Right", "R")); + + ddlDefaultPosV = new DropDownList {ID = "posv"}; + ddlDefaultPosV.Items.Add(new ListItem("Top", "T")); + ddlDefaultPosV.Items.Add(new ListItem("Middle", "M")); + ddlDefaultPosV.Items.Add(new ListItem("Bottom", "B")); + + btnUp = new Button {ID = "up", Text = "Up", Width = Unit.Pixel(60)}; + btnDown = new Button {ID = "down", Text = "Down", Width = Unit.Pixel(60)}; + btnAdd = new Button {ID = "add", Text = "Add", Width = Unit.Pixel(60)}; + btnRemove = new Button {ID = "remove", Text = "Remove", Width = Unit.Pixel(60)}; + btnGenerate = new Button {ID = "generate", Text = "Generate", Width = Unit.Pixel(60)}; + + //vsErrors = new ValidationSummary {ID = "summary", ValidationGroup = "cropper"}; + //rqfName = new RequiredFieldValidator {ID = "namevalidator", ValidationGroup = "cropper", ControlToValidate = txtCropName.ClientID, ErrorMessage = "Crop name missing", Text="*" }; + + //revName = new RegularExpressionValidator + // { + // ID = "namevalidator", + // ValidationExpression = ".*[a-zA-Z0-9-_ ].*", + // ValidationGroup = "cropper", + // ErrorMessage = "Invalid name. Alphanumerical only please as this will be the filename", + // AssociatedControlID = txtCropName.ID + // }; + + Controls.Add(txtPropertyAlias); + Controls.Add(chkGenerateCrops); + Controls.Add(litQuality); + Controls.Add(txtQuality); + Controls.Add(chkShowLabel); + + Controls.Add(slbPresets); + Controls.Add(txtCropName); + Controls.Add(txtTargetWidth); + Controls.Add(txtTargetHeight); + Controls.Add(chkKeepAspect); + Controls.Add(ddlDefaultPosH); + Controls.Add(ddlDefaultPosV); + + Controls.Add(btnUp); + Controls.Add(btnDown); + Controls.Add(btnAdd); + Controls.Add(btnRemove); + Controls.Add(btnGenerate); + + //Controls.Add(vsErrors); + //Controls.Add(rqfName); + //Controls.Add(revName); + + btnUp.Click += _upButton_Click; + btnDown.Click += _downButton_Click; + btnAdd.Click += _addButton_Click; + btnRemove.Click += _removeButton_Click; + + //btnGenerate.Click += _generateButton_Click; + + chkGenerateCrops.CheckedChanged += _generateImagesCheckBox_CheckedChanged; + + + } + +#if false + void _generateButton_Click(object sender, EventArgs e) + { + Config config = new Config(Configuration); + + // get list of nodeids with this datatype + using (IRecordsReader rdr = SqlHelper.ExecuteReader( + "SELECT DISTINCT contentNodeId, " + + "(SELECT Alias FROM cmsPropertyType WHERE Id = pd.propertyTypeId) AS propertyAlias " + + "FROM cmsPropertyData pd " + + "WHERE PropertyTypeId IN (SELECT Id FROM cmsPropertyType WHERE DataTypeId = " + _dataType.DataTypeDefinitionId + ")")) + { + while (rdr.Read()) + { + int documentId = rdr.GetInt("contentNodeId"); + string propertyAlias = rdr.GetString("propertyAlias"); + + Document document = new Document(documentId); + + Property cropProperty = document.getProperty(propertyAlias); + Property imageProperty = document.getProperty(config.UploadPropertyAlias); + + if (cropProperty != null) // && cropProperty.Value.ToString() == "" + { + ImageInfo imageInfo = new ImageInfo(imageProperty.Value.ToString()); + + if (imageInfo.Exists) + { + SaveData saveData = new SaveData(); + + foreach (Preset preset in config.presets) + { + Crop crop = preset.Fit(imageInfo); + saveData.data.Add(crop); + } + + //cropProperty.Value = saveData.Xml(config, imageInfo); + + imageInfo.GenerateThumbnails(saveData, config); + + if (document.Published) + { + //document.Publish(document.User); + //umbraco.library.UpdateDocumentCache(document.Id); + } + else + { + //document.Save(); + } + } + } + } + } + } +#endif + + void _generateImagesCheckBox_CheckedChanged(object sender, EventArgs e) + { + txtQuality.Visible = chkGenerateCrops.Checked; + litQuality.Visible = chkGenerateCrops.Checked; + } + + void _upButton_Click(object sender, EventArgs e) + { + slbPresets.MoveUp(); + } + + void _downButton_Click(object sender, EventArgs e) + { + slbPresets.MoveDown(); + } + + void _removeButton_Click(object sender, EventArgs e) + { + for (int i = slbPresets.Items.Count - 1; i >= 0; i--) + { + if (slbPresets.Items[i].Selected) + slbPresets.Items.Remove(slbPresets.Items[i]); + } + } + + void _addButton_Click(object sender, EventArgs e) + { + slbPresets.Items.Add( + new ListItem( + getListItemDisplayName( + txtCropName.Text, + txtTargetWidth.Text, + txtTargetHeight.Text, + chkKeepAspect.Checked ? "1" : "0", + String.Concat(ddlDefaultPosH.SelectedValue, ddlDefaultPosV.SelectedValue)), + String.Format("{0},{1},{2},{3},{4}", + txtCropName.Text, + txtTargetWidth.Text, + txtTargetHeight.Text, + chkKeepAspect.Checked ? "1" : "0", + String.Concat(ddlDefaultPosH.SelectedValue, ddlDefaultPosV.SelectedValue)) + ) + ); + txtCropName.Text = ""; + txtTargetWidth.Text = ""; + txtTargetHeight.Text = ""; + chkKeepAspect.Checked = true; + + } + + public Control Editor + { + get + { + return this; + } + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + if (!Page.IsPostBack) + LoadData(); + } + + private void LoadData() + { + if (!string.IsNullOrEmpty(Configuration)) + { + Config config = new Config(Configuration); + + txtPropertyAlias.Text = config.UploadPropertyAlias; + chkGenerateCrops.Checked = config.GenerateImages; + chkShowLabel.Checked = config.ShowLabel; + txtQuality.Visible = chkShowLabel.Checked; + txtQuality.Text = config.Quality.ToString(); + litQuality.Visible = chkShowLabel.Checked; + + foreach (Preset preset in config.presets) + { + slbPresets.Items.Add( + new ListItem( + getListItemDisplayName( + preset.Name, + preset.TargetWidth.ToString(), + preset.TargetHeight.ToString(), + preset.KeepAspect ? "1" : "0", + String.Concat(preset.PositionH, preset.PositionV)), + String.Format("{0},{1},{2},{3},{4}{5}", + preset.Name, + preset.TargetWidth, + preset.TargetHeight, + preset.KeepAspect ? "1" : "0", + preset.PositionH, preset.PositionV))); + } + } + } + + private static string getListItemDisplayName(string presetTemplateName, string width, string height, string keepAspect, string position) + { + return String.Format("{0}, width: {1}px, height: {2}px, keep aspect: {3}, {4}", + presetTemplateName, + width, + height, + keepAspect == "1" ? "yes" : "no", + position); + } + + /// + /// Serialize configuration to: + /// uploadPropertyAlias,generateImages,showLabel|presetTemplateName,targetWidth,targetHeight,keepAspect; + /// + public void Save() + { + _dataType.DBType = (umbraco.cms.businesslogic.datatype.DBTypes)Enum.Parse( + typeof(umbraco.cms.businesslogic.datatype.DBTypes), DBTypes.Ntext.ToString(), true); + string generalData = String.Format("{0},{1},{2}", + txtPropertyAlias.Text, + chkGenerateCrops.Checked ? "1" : "0", + chkShowLabel.Checked ? "1" : "0" + ); + + string templateData = ""; + + for (int i = 0; i < slbPresets.Items.Count; i++) + { + templateData += slbPresets.Items[i].Value; + if (i < slbPresets.Items.Count - 1) templateData += ";"; + } + + string data = String.Format("{0}|{1}", generalData, templateData); + + SqlHelper.ExecuteNonQuery("delete from cmsDataTypePreValues where datatypenodeid = @dtdefid", + SqlHelper.CreateParameter("@dtdefid", _dataType.DataTypeDefinitionId)); + + SqlHelper.ExecuteNonQuery("insert into cmsDataTypePreValues (datatypenodeid,[value],sortorder,alias) values (@dtdefid,@value,0,'')", + SqlHelper.CreateParameter("@dtdefid", _dataType.DataTypeDefinitionId), SqlHelper.CreateParameter("@value", data)); + } + + protected override void Render(HtmlTextWriter writer) + { + writer.Write("

General

"); + writer.Write(""); + + writer.Write(" "); + + writer.Write(" "); + + writer.Write(" "); + + writer.Write("
Property alias: (eg. 'umbracoFile'):"); + txtPropertyAlias.RenderControl(writer); + writer.Write("
Save crop images (/media/(imageid)/(filename)_(cropname).jpg):"); + chkGenerateCrops.RenderControl(writer); + litQuality.RenderControl(writer); + txtQuality.RenderControl(writer); + writer.Write("
Show Label:"); + chkShowLabel.RenderControl(writer); + writer.Write("
"); + + writer.Write("

Crops

"); + + writer.Write(""); + writer.Write(" "); + writer.Write("
"); + + writer.Write(" "); + writer.Write(" "); + writer.Write(" "); + writer.Write(" "); + writer.Write(" "); + writer.Write(" "); + writer.Write("
Name"); + txtCropName.RenderControl(writer); + writer.Write("
Target width"); + txtTargetWidth.RenderControl(writer); + writer.Write(" px
Target height"); + txtTargetHeight.RenderControl(writer); + writer.Write(" px
Default position "); + ddlDefaultPosH.RenderControl(writer); + writer.Write(" "); + ddlDefaultPosV.RenderControl(writer); + writer.Write("
Keep aspect"); + chkKeepAspect.RenderControl(writer); + writer.Write("

"); + btnAdd.RenderControl(writer); + + writer.Write("
  "); + slbPresets.RenderControl(writer); + writer.Write(" "); + btnUp.RenderControl(writer); + writer.Write("
"); + btnDown.RenderControl(writer); + writer.Write("




"); + btnRemove.RenderControl(writer); + writer.Write("
"); + + //_generateButton.RenderControl(writer); + //_vsErrors.RenderControl(writer); + //_revName.RenderControl(writer); + + + } + + public string Configuration + { + get + { + object conf = + SqlHelper.ExecuteScalar("select value from cmsDataTypePreValues where datatypenodeid = @datatypenodeid", + SqlHelper.CreateParameter("@datatypenodeid", _dataType.DataTypeDefinitionId)); + + if (conf != null) + return conf.ToString(); + + return string.Empty; + } + } + + public static ISqlHelper SqlHelper + { + get + { + return Application.SqlHelper; + } + } + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/Resources.Designer.cs b/components/editorControls/imagecropper/Resources.Designer.cs new file mode 100644 index 0000000000..4cdbaf2c02 --- /dev/null +++ b/components/editorControls/imagecropper/Resources.Designer.cs @@ -0,0 +1,156 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:2.0.50727.3074 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace umbraco.editorControls.imagecropper +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("umbraco.editorControls.imagecropper.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to function initImageCropper(imageId, jsonId, rawId) { + /// $(function() { + /// $('.header a').click(function() { + /// initJcrop(imageId, jsonId, rawId); // if other tab + /// }); + /// $('#' + imageId).ready(function() { + /// setTimeout('initJcrop("' + imageId + '","' + jsonId + '","' + rawId + '")',100); + /// //initJcrop(imageId, jsonId, rawId); // if first tab + /// }); + /// }); + ///} + /// + ///function initJcrop(imageId, jsonId, rawId) { + /// + /// if ($('#' + imageId).height() > 0) { + /// if ($('.img' + imageId + '.ic-cr [rest of string was truncated]";. + /// + public static string imageCropperScript { + get { + return ResourceManager.GetString("imageCropperScript", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <style type="text/css"> + ////* Fixes issue here http://code.google.com/p/jcrop/issues/detail?id=1 */ + ///.jcrop-holder { text-align: left; } + /// + ///.jcrop-vline, .jcrop-hline + ///{ + /// font-size: 0; + /// position: absolute; + /// background: white url('/media/Jcrop.gif') top left repeat; + ///} + ///.jcrop-vline { height: 100%; width: 1px !important; } + ///.jcrop-hline { width: 100%; height: 1px !important; } + ///.jcrop-handle { + /// font-size: 1px; + /// width: 7px !important; + /// height: 7px !important; + /// border: 1px #eee solid; + /// background-color: [rest of string was truncated]";. + /// + public static string jCropCSS { + get { + return ResourceManager.GetString("jCropCSS", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /** + /// * jquery.Jcrop.js v0.9.8 + /// * jQuery Image Cropping Plugin + /// * @author Kelly Hallman <khallman@gmail.com> + /// * Copyright (c) 2008-2009 Kelly Hallman - released under MIT License {{{ + /// * + /// * Permission is hereby granted, free of charge, to any person + /// * obtaining a copy of this software and associated documentation + /// * files (the "Software"), to deal in the Software without + /// * restriction, including without limitation the rights to use, + /// * copy, modify, merge, publish, distribute, sublicense, and/or s [rest of string was truncated]";. + /// + public static string jCropScript { + get { + return ResourceManager.GetString("jCropScript", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to /* + /// http://www.JSON.org/json2.js + /// 2008-11-19 + /// + /// Public Domain. + /// + /// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + /// + /// See http://www.JSON.org/js.html + /// + /// This file creates a global JSON object containing two methods: stringify + /// and parse. + /// + /// JSON.stringify(value, replacer, space) + /// value any JavaScript value, usually an object or array. + /// + /// replacer an optional parameter that determines how object + /// values are st [rest of string was truncated]";. + /// + public static string json2Script { + get { + return ResourceManager.GetString("json2Script", resourceCulture); + } + } + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/Resources.resx b/components/editorControls/imagecropper/Resources.resx new file mode 100644 index 0000000000..5c49e7d051 --- /dev/null +++ b/components/editorControls/imagecropper/Resources.resx @@ -0,0 +1,2014 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + function initImageCropper(imageId, jsonId, rawId) { + $(function() { + $('.header a').click(function() { + initJcrop(imageId, jsonId, rawId); // if other tab + }); + $('#' + imageId).ready(function() { + setTimeout('initJcrop("' + imageId + '","' + jsonId + '","' + rawId + '")',100); + //initJcrop(imageId, jsonId, rawId); // if first tab + }); + }); +} + +function initJcrop(imageId, jsonId, rawId) { + + if ($('#' + imageId).height() > 0) { + if ($('.img' + imageId + '.ic-crop').size() == 0) { + + // json object + var json = eval('(' + $('#' + jsonId).val() + ')'); + + // store json object to hidden element + $('#' + jsonId).data('json', json); + + // create jcrop + var API = $.Jcrop('#' + imageId); + + // add current height and width spans + var coords_id = 'img' + imageId + '_coords'; + $('#' + jsonId).after('<div style="float:left;color:#777;padding: 7px 0 0 0" id="' + coords_id + '"></div>'); + $('#' + coords_id).append('<span class="img' + imageId + ' ic-width"></span>'); + $('#' + coords_id).append('<span class="img' + imageId + ' ic-height"></span>'); + + // generate crop links (rel = index) + for (var i = json.crops.length - 1; i >= 0; i--) { + $('#' + jsonId).after('<a href="#" class="img' + imageId + ' ic-crop" rel="' + i + '"><b>Name:</b> ' + json.crops[i].name + '<br/><b>Target:</b> ' + json.crops[i].config.targetWidth + 'x' + json.crops[i].config.targetHeight + '<br/><b>Aspect:</b> ' + (json.crops[i].config.keepAspect ? 'yes' : 'no') + '</a>'); + } + + icValidateHealth(imageId, jsonId); + + // set current class + $('.img' + imageId + '.ic-crop').click(function() { + $('.img' + imageId + '.ic-crop').removeClass('current'); + $(this).addClass('current'); + }); + + $('.img' + imageId + '.ic-crop').each(function(i) { + $(this).click(function() { + + var json = eval('(' + $('#' + jsonId).val() + ')'); + var btn = this; + var index = this.rel; + var crop = json.crops[index]; + + // keep aspect? + API.setOptions({ + aspectRatio: (crop.config.keepAspect ? crop.config.targetWidth / crop.config.targetHeight : 0) + }); + + // only change presets + API.setOptions({ allowSelect: false }); + + // events + API.setOptions({ + + onChange: function(c) { + $('.img' + imageId + '.ic-width').html(c.w); + $('.img' + imageId + '.ic-height').html('x' + c.h); + }, + + onSelect: function(c) { + + crop.value.x = c.x; + crop.value.y = c.y; + crop.value.x2 = c.x2; + crop.value.y2 = c.y2; + json.current = index; + + $('#' + jsonId).val(JSON.stringify(json)); + + var raw = ''; + for (var i = 0; i < json.crops.length; i++) { + raw += json.crops[i].value.x + ',' + + json.crops[i].value.y + ',' + + json.crops[i].value.x2 + ',' + + json.crops[i].value.y2; + if (i < json.crops.length - 1) raw += ';'; + } + $('#' + rawId).val(raw); + icValidateHealth(imageId, jsonId); + } + }); + + var x = crop.value.x; + var y = crop.value.y; + var x2 = crop.value.x2; + var y2 = crop.value.y2; + + if (x2 == 0) x2 = x + crop.config.targetWidth; + if (y2 == 0) y2 = y + crop.config.targetHeight; + + API.setSelect([x, y, x2, y2]); + + return false; + + }); + }); + + $(".img" + imageId + ".ic-crop[rel='" + json.current + "']").click(); + + } + } +} + +function icValidateHealth(imageId, jsonId) { + var json = eval('(' + $('#' + jsonId).val() + ')'); + + $('.img' + imageId + '.ic-crop').each(function(i) { + + var crop = json.crops[i]; + + if (crop.value.x2 - crop.value.x < crop.config.targetWidth || + crop.value.y2 - crop.value.y < crop.config.targetHeight) { + $(this).removeClass('healthy'); + $(this).addClass('unhealthy'); + } else { + $(this).removeClass('unhealthy'); + $(this).addClass('healthy'); + } + + }); +} + + + <style type="text/css"> +/* Fixes issue here http://code.google.com/p/jcrop/issues/detail?id=1 */ +.jcrop-holder { text-align: left; } + +.jcrop-vline, .jcrop-hline +{ + font-size: 0; + position: absolute; + background: white url('/umbraco_client/imagecropper/Jcrop.gif') top left repeat; +} +.jcrop-vline { height: 100%; width: 1px !important; } +.jcrop-hline { width: 100%; height: 1px !important; } +.jcrop-handle { + font-size: 1px; + width: 7px !important; + height: 7px !important; + border: 1px #eee solid; + background-color: #333; + *width: 9px; + *height: 9px; +} + +.jcrop-tracker { width: 100%; height: 100%; } + +.custom .jcrop-vline, +.custom .jcrop-hline +{ + background: yellow; +} +.custom .jcrop-handle +{ + border-color: black; + background-color: #C7BB00; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; +} + +.ic-crop +{ + display: block; + float: left; + text-decoration: none; + padding: 10px 5px; + background: #ddd; + border: 4px solid #fff; + + margin: 5px 5px 0 0; +} + +.ic-crop:hover { text-decoration: none; } + +.ic-crop.current +{ +/* text-decoration: underline; */ + + +} + +.ic-crop.healthy +{ + background: #E6EFC2; + color: #264409; +} + +.ic-crop.current.healthy +{ + border-color: #C6D880; +} + +.ic-crop.unhealthy +{ + background: #FBE3E4; + color: #8A1F11; +} + +.ic-crop.current.unhealthy +{ + border-color: #FBC2C4; +} +</style> + + + /** + * jquery.Jcrop.js v0.9.8 + * jQuery Image Cropping Plugin + * @author Kelly Hallman <khallman@gmail.com> + * Copyright (c) 2008-2009 Kelly Hallman - released under MIT License {{{ + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + + * }}} + */ + +(function($) { + +$.Jcrop = function(obj,opt) +{ + // Initialization {{{ + + // Sanitize some options {{{ + var obj = obj, opt = opt; + + if (typeof(obj) !== 'object') obj = $(obj)[0]; + if (typeof(opt) !== 'object') opt = { }; + + // Some on-the-fly fixes for MSIE...sigh + if (!('trackDocument' in opt)) + { + opt.trackDocument = $.browser.msie ? false : true; + if ($.browser.msie && $.browser.version.split('.')[0] == '8') + opt.trackDocument = true; + } + + if (!('keySupport' in opt)) + opt.keySupport = $.browser.msie ? false : true; + + // }}} + // Extend the default options {{{ + var defaults = { + + // Basic Settings + trackDocument: false, + baseClass: 'jcrop', + addClass: null, + + // Styling Options + bgColor: 'black', + bgOpacity: .6, + borderOpacity: .4, + handleOpacity: .5, + + handlePad: 5, + handleSize: 9, + handleOffset: 5, + edgeMargin: 14, + + aspectRatio: 0, + keySupport: true, + cornerHandles: true, + sideHandles: true, + drawBorders: true, + dragEdges: true, + + boxWidth: 0, + boxHeight: 0, + + boundary: 8, + animationDelay: 20, + swingSpeed: 3, + + allowSelect: true, + allowMove: true, + allowResize: true, + + minSelect: [ 0, 0 ], + maxSize: [ 0, 0 ], + minSize: [ 0, 0 ], + + // Callbacks / Event Handlers + onChange: function() { }, + onSelect: function() { } + + }; + var options = defaults; + setOptions(opt); + + // }}} + // Initialize some jQuery objects {{{ + + var $origimg = $(obj); + var $img = $origimg.clone().removeAttr('id').css({ position: 'absolute' }); + + $img.width($origimg.width()); + $img.height($origimg.height()); + $origimg.after($img).hide(); + + presize($img,options.boxWidth,options.boxHeight); + + var boundx = $img.width(), + boundy = $img.height(), + + $div = $('<div />') + .width(boundx).height(boundy) + .addClass(cssClass('holder')) + .css({ + position: 'relative', + backgroundColor: options.bgColor + }).insertAfter($origimg).append($img); + ; + + if (options.addClass) $div.addClass(options.addClass); + //$img.wrap($div); + + var $img2 = $('<img />')/*{{{*/ + .attr('src',$img.attr('src')) + .css('position','absolute') + .width(boundx).height(boundy) + ;/*}}}*/ + var $img_holder = $('<div />')/*{{{*/ + .width(pct(100)).height(pct(100)) + .css({ + zIndex: 310, + position: 'absolute', + overflow: 'hidden' + }) + .append($img2) + ;/*}}}*/ + var $hdl_holder = $('<div />')/*{{{*/ + .width(pct(100)).height(pct(100)) + .css('zIndex',320); + /*}}}*/ + var $sel = $('<div />')/*{{{*/ + .css({ + position: 'absolute', + zIndex: 300 + }) + .insertBefore($img) + .append($img_holder,$hdl_holder) + ;/*}}}*/ + + var bound = options.boundary; + var $trk = newTracker().width(boundx+(bound*2)).height(boundy+(bound*2)) + .css({ position: 'absolute', top: px(-bound), left: px(-bound), zIndex: 290 }) + .mousedown(newSelection); + + /* }}} */ + // Set more variables {{{ + + var xlimit, ylimit, xmin, ymin; + var xscale, yscale, enabled = true; + var docOffset = getPos($img), + // Internal states + btndown, lastcurs, dimmed, animating, + shift_down; + + // }}} + + + // }}} + // Internal Modules {{{ + + var Coords = function()/*{{{*/ + { + var x1 = 0, y1 = 0, x2 = 0, y2 = 0, ox, oy; + + function setPressed(pos)/*{{{*/ + { + var pos = rebound(pos); + x2 = x1 = pos[0]; + y2 = y1 = pos[1]; + }; + /*}}}*/ + function setCurrent(pos)/*{{{*/ + { + var pos = rebound(pos); + ox = pos[0] - x2; + oy = pos[1] - y2; + x2 = pos[0]; + y2 = pos[1]; + }; + /*}}}*/ + function getOffset()/*{{{*/ + { + return [ ox, oy ]; + }; + /*}}}*/ + function moveOffset(offset)/*{{{*/ + { + var ox = offset[0], oy = offset[1]; + + if (0 > x1 + ox) ox -= ox + x1; + if (0 > y1 + oy) oy -= oy + y1; + + if (boundy < y2 + oy) oy += boundy - (y2 + oy); + if (boundx < x2 + ox) ox += boundx - (x2 + ox); + + x1 += ox; + x2 += ox; + y1 += oy; + y2 += oy; + }; + /*}}}*/ + function getCorner(ord)/*{{{*/ + { + var c = getFixed(); + switch(ord) + { + case 'ne': return [ c.x2, c.y ]; + case 'nw': return [ c.x, c.y ]; + case 'se': return [ c.x2, c.y2 ]; + case 'sw': return [ c.x, c.y2 ]; + } + }; + /*}}}*/ + function getFixed()/*{{{*/ + { + if (!options.aspectRatio) return getRect(); + // This function could use some optimization I think... + var aspect = options.aspectRatio, + min_x = options.minSize[0]/xscale, + min_y = options.minSize[1]/yscale, + max_x = options.maxSize[0]/xscale, + max_y = options.maxSize[1]/yscale, + rw = x2 - x1, + rh = y2 - y1, + rwa = Math.abs(rw), + rha = Math.abs(rh), + real_ratio = rwa / rha, + xx, yy + ; + if (max_x == 0) { max_x = boundx * 10 } + if (max_y == 0) { max_y = boundy * 10 } + if (real_ratio < aspect) + { + yy = y2; + w = rha * aspect; + xx = rw < 0 ? x1 - w : w + x1; + + if (xx < 0) + { + xx = 0; + h = Math.abs((xx - x1) / aspect); + yy = rh < 0 ? y1 - h: h + y1; + } + else if (xx > boundx) + { + xx = boundx; + h = Math.abs((xx - x1) / aspect); + yy = rh < 0 ? y1 - h : h + y1; + } + } + else + { + xx = x2; + h = rwa / aspect; + yy = rh < 0 ? y1 - h : y1 + h; + if (yy < 0) + { + yy = 0; + w = Math.abs((yy - y1) * aspect); + xx = rw < 0 ? x1 - w : w + x1; + } + else if (yy > boundy) + { + yy = boundy; + w = Math.abs(yy - y1) * aspect; + xx = rw < 0 ? x1 - w : w + x1; + } + } + + // Magic %-) + if(xx > x1) { // right side + if(xx - x1 < min_x) { + xx = x1 + min_x; + } else if (xx - x1 > max_x) { + xx = x1 + max_x; + } + if(yy > y1) { + yy = y1 + (xx - x1)/aspect; + } else { + yy = y1 - (xx - x1)/aspect; + } + } else if (xx < x1) { // left side + if(x1 - xx < min_x) { + xx = x1 - min_x + } else if (x1 - xx > max_x) { + xx = x1 - max_x; + } + if(yy > y1) { + yy = y1 + (x1 - xx)/aspect; + } else { + yy = y1 - (x1 - xx)/aspect; + } + } + + if(xx < 0) { + x1 -= xx; + xx = 0; + } else if (xx > boundx) { + x1 -= xx - boundx; + xx = boundx; + } + + if(yy < 0) { + y1 -= yy; + yy = 0; + } else if (yy > boundy) { + y1 -= yy - boundy; + yy = boundy; + } + + return last = makeObj(flipCoords(x1,y1,xx,yy)); + }; + /*}}}*/ + function rebound(p)/*{{{*/ + { + if (p[0] < 0) p[0] = 0; + if (p[1] < 0) p[1] = 0; + + if (p[0] > boundx) p[0] = boundx; + if (p[1] > boundy) p[1] = boundy; + + return [ p[0], p[1] ]; + }; + /*}}}*/ + function flipCoords(x1,y1,x2,y2)/*{{{*/ + { + var xa = x1, xb = x2, ya = y1, yb = y2; + if (x2 < x1) + { + xa = x2; + xb = x1; + } + if (y2 < y1) + { + ya = y2; + yb = y1; + } + return [ Math.round(xa), Math.round(ya), Math.round(xb), Math.round(yb) ]; + }; + /*}}}*/ + function getRect()/*{{{*/ + { + var xsize = x2 - x1; + var ysize = y2 - y1; + + if (xlimit && (Math.abs(xsize) > xlimit)) + x2 = (xsize > 0) ? (x1 + xlimit) : (x1 - xlimit); + if (ylimit && (Math.abs(ysize) > ylimit)) + y2 = (ysize > 0) ? (y1 + ylimit) : (y1 - ylimit); + + if (ymin && (Math.abs(ysize) < ymin)) + y2 = (ysize > 0) ? (y1 + ymin) : (y1 - ymin); + if (xmin && (Math.abs(xsize) < xmin)) + x2 = (xsize > 0) ? (x1 + xmin) : (x1 - xmin); + + if (x1 < 0) { x2 -= x1; x1 -= x1; } + if (y1 < 0) { y2 -= y1; y1 -= y1; } + if (x2 < 0) { x1 -= x2; x2 -= x2; } + if (y2 < 0) { y1 -= y2; y2 -= y2; } + if (x2 > boundx) { var delta = x2 - boundx; x1 -= delta; x2 -= delta; } + if (y2 > boundy) { var delta = y2 - boundy; y1 -= delta; y2 -= delta; } + if (x1 > boundx) { var delta = x1 - boundy; y2 -= delta; y1 -= delta; } + if (y1 > boundy) { var delta = y1 - boundy; y2 -= delta; y1 -= delta; } + + return makeObj(flipCoords(x1,y1,x2,y2)); + }; + /*}}}*/ + function makeObj(a)/*{{{*/ + { + return { x: a[0], y: a[1], x2: a[2], y2: a[3], + w: a[2] - a[0], h: a[3] - a[1] }; + }; + /*}}}*/ + + return { + flipCoords: flipCoords, + setPressed: setPressed, + setCurrent: setCurrent, + getOffset: getOffset, + moveOffset: moveOffset, + getCorner: getCorner, + getFixed: getFixed + }; + }(); + + /*}}}*/ + var Selection = function()/*{{{*/ + { + var start, end, dragmode, awake, hdep = 370; + var borders = { }; + var handle = { }; + var seehandles = false; + var hhs = options.handleOffset; + + /* Insert draggable elements {{{*/ + + // Insert border divs for outline + if (options.drawBorders) { + borders = { + top: insertBorder('hline') + .css('top',$.browser.msie?px(-1):px(0)), + bottom: insertBorder('hline'), + left: insertBorder('vline'), + right: insertBorder('vline') + }; + } + + // Insert handles on edges + if (options.dragEdges) { + handle.t = insertDragbar('n'); + handle.b = insertDragbar('s'); + handle.r = insertDragbar('e'); + handle.l = insertDragbar('w'); + } + + // Insert side handles + options.sideHandles && + createHandles(['n','s','e','w']); + + // Insert corner handles + options.cornerHandles && + createHandles(['sw','nw','ne','se']); + + /*}}}*/ + // Private Methods + function insertBorder(type)/*{{{*/ + { + var jq = $('<div />') + .css({position: 'absolute', opacity: options.borderOpacity }) + .addClass(cssClass(type)); + $img_holder.append(jq); + return jq; + }; + /*}}}*/ + function dragDiv(ord,zi)/*{{{*/ + { + var jq = $('<div />') + .mousedown(createDragger(ord)) + .css({ + cursor: ord+'-resize', + position: 'absolute', + zIndex: zi + }) + ; + $hdl_holder.append(jq); + return jq; + }; + /*}}}*/ + function insertHandle(ord)/*{{{*/ + { + return dragDiv(ord,hdep++) + .css({ top: px(-hhs+1), left: px(-hhs+1), opacity: options.handleOpacity }) + .addClass(cssClass('handle')); + }; + /*}}}*/ + function insertDragbar(ord)/*{{{*/ + { + var s = options.handleSize, + o = hhs, + h = s, w = s, + t = o, l = o; + + switch(ord) + { + case 'n': case 's': w = pct(100); break; + case 'e': case 'w': h = pct(100); break; + } + + return dragDiv(ord,hdep++).width(w).height(h) + .css({ top: px(-t+1), left: px(-l+1)}); + }; + /*}}}*/ + function createHandles(li)/*{{{*/ + { + for(i in li) handle[li[i]] = insertHandle(li[i]); + }; + /*}}}*/ + function moveHandles(c)/*{{{*/ + { + var midvert = Math.round((c.h / 2) - hhs), + midhoriz = Math.round((c.w / 2) - hhs), + north = west = -hhs+1, + east = c.w - hhs, + south = c.h - hhs, + x, y; + + 'e' in handle && + handle.e.css({ top: px(midvert), left: px(east) }) && + handle.w.css({ top: px(midvert) }) && + handle.s.css({ top: px(south), left: px(midhoriz) }) && + handle.n.css({ left: px(midhoriz) }); + + 'ne' in handle && + handle.ne.css({ left: px(east) }) && + handle.se.css({ top: px(south), left: px(east) }) && + handle.sw.css({ top: px(south) }); + + 'b' in handle && + handle.b.css({ top: px(south) }) && + handle.r.css({ left: px(east) }); + }; + /*}}}*/ + function moveto(x,y)/*{{{*/ + { + $img2.css({ top: px(-y), left: px(-x) }); + $sel.css({ top: px(y), left: px(x) }); + }; + /*}}}*/ + function resize(w,h)/*{{{*/ + { + $sel.width(w).height(h); + }; + /*}}}*/ + function refresh()/*{{{*/ + { + var c = Coords.getFixed(); + + Coords.setPressed([c.x,c.y]); + Coords.setCurrent([c.x2,c.y2]); + + updateVisible(); + }; + /*}}}*/ + + // Internal Methods + function updateVisible()/*{{{*/ + { if (awake) return update(); }; + /*}}}*/ + function update()/*{{{*/ + { + var c = Coords.getFixed(); + + resize(c.w,c.h); + moveto(c.x,c.y); + + options.drawBorders && + borders['right'].css({ left: px(c.w-1) }) && + borders['bottom'].css({ top: px(c.h-1) }); + + seehandles && moveHandles(c); + awake || show(); + + options.onChange(unscale(c)); + }; + /*}}}*/ + function show()/*{{{*/ + { + $sel.show(); + $img.css('opacity',options.bgOpacity); + awake = true; + }; + /*}}}*/ + function release()/*{{{*/ + { + disableHandles(); + $sel.hide(); + $img.css('opacity',1); + awake = false; + }; + /*}}}*/ + function showHandles()//{{{ + { + if (seehandles) + { + moveHandles(Coords.getFixed()); + $hdl_holder.show(); + } + }; + //}}} + function enableHandles()/*{{{*/ + { + seehandles = true; + if (options.allowResize) + { + moveHandles(Coords.getFixed()); + $hdl_holder.show(); + return true; + } + }; + /*}}}*/ + function disableHandles()/*{{{*/ + { + seehandles = false; + $hdl_holder.hide(); + }; + /*}}}*/ + function animMode(v)/*{{{*/ + { + (animating = v) ? disableHandles(): enableHandles(); + }; + /*}}}*/ + function done()/*{{{*/ + { + animMode(false); + refresh(); + }; + /*}}}*/ + + var $track = newTracker().mousedown(createDragger('move')) + .css({ cursor: 'move', position: 'absolute', zIndex: 360 }) + + $img_holder.append($track); + disableHandles(); + + return { + updateVisible: updateVisible, + update: update, + release: release, + refresh: refresh, + setCursor: function (cursor) { $track.css('cursor',cursor); }, + enableHandles: enableHandles, + enableOnly: function() { seehandles = true; }, + showHandles: showHandles, + disableHandles: disableHandles, + animMode: animMode, + done: done + }; + }(); + /*}}}*/ + var Tracker = function()/*{{{*/ + { + var onMove = function() { }, + onDone = function() { }, + trackDoc = options.trackDocument; + + if (!trackDoc) + { + $trk + .mousemove(trackMove) + .mouseup(trackUp) + .mouseout(trackUp) + ; + } + + function toFront()/*{{{*/ + { + $trk.css({zIndex:450}); + if (trackDoc) + { + $(document) + .mousemove(trackMove) + .mouseup(trackUp) + ; + } + } + /*}}}*/ + function toBack()/*{{{*/ + { + $trk.css({zIndex:290}); + if (trackDoc) + { + $(document) + .unbind('mousemove',trackMove) + .unbind('mouseup',trackUp) + ; + } + } + /*}}}*/ + function trackMove(e)/*{{{*/ + { + onMove(mouseAbs(e)); + }; + /*}}}*/ + function trackUp(e)/*{{{*/ + { + e.preventDefault(); + e.stopPropagation(); + + if (btndown) + { + btndown = false; + + onDone(mouseAbs(e)); + options.onSelect(unscale(Coords.getFixed())); + toBack(); + onMove = function() { }; + onDone = function() { }; + } + + return false; + }; + /*}}}*/ + + function activateHandlers(move,done)/* {{{ */ + { + btndown = true; + onMove = move; + onDone = done; + toFront(); + return false; + }; + /* }}} */ + + function setCursor(t) { $trk.css('cursor',t); }; + + $img.before($trk); + return { + activateHandlers: activateHandlers, + setCursor: setCursor + }; + }(); + /*}}}*/ + var KeyManager = function()/*{{{*/ + { + var $keymgr = $('<input type="radio" />') + .css({ position: 'absolute', left: '-30px' }) + .keypress(parseKey) + .blur(onBlur), + + $keywrap = $('<div />') + .css({ + position: 'absolute', + overflow: 'hidden' + }) + .append($keymgr) + ; + + function watchKeys()/*{{{*/ + { + if (options.keySupport) + { + $keymgr.show(); + $keymgr.focus(); + } + }; + /*}}}*/ + function onBlur(e)/*{{{*/ + { + $keymgr.hide(); + }; + /*}}}*/ + function doNudge(e,x,y)/*{{{*/ + { + if (options.allowMove) { + Coords.moveOffset([x,y]); + Selection.updateVisible(); + }; + e.preventDefault(); + e.stopPropagation(); + }; + /*}}}*/ + function parseKey(e)/*{{{*/ + { + if (e.ctrlKey) return true; + shift_down = e.shiftKey ? true : false; + var nudge = shift_down ? 10 : 1; + switch(e.keyCode) + { + case 37: doNudge(e,-nudge,0); break; + case 39: doNudge(e,nudge,0); break; + case 38: doNudge(e,0,-nudge); break; + case 40: doNudge(e,0,nudge); break; + + case 27: Selection.release(); break; + + case 9: return true; + } + + return nothing(e); + }; + /*}}}*/ + + if (options.keySupport) $keywrap.insertBefore($img); + return { + watchKeys: watchKeys + }; + }(); + /*}}}*/ + + // }}} + // Internal Methods {{{ + + function px(n) { return '' + parseInt(n) + 'px'; }; + function pct(n) { return '' + parseInt(n) + '%'; }; + function cssClass(cl) { return options.baseClass + '-' + cl; }; + function getPos(obj)/*{{{*/ + { + // Updated in v0.9.4 to use built-in dimensions plugin + var pos = $(obj).offset(); + return [ pos.left, pos.top ]; + }; + /*}}}*/ + function mouseAbs(e)/*{{{*/ + { + return [ (e.pageX - docOffset[0]), (e.pageY - docOffset[1]) ]; + }; + /*}}}*/ + function myCursor(type)/*{{{*/ + { + if (type != lastcurs) + { + Tracker.setCursor(type); + //Handles.xsetCursor(type); + lastcurs = type; + } + }; + /*}}}*/ + function startDragMode(mode,pos)/*{{{*/ + { + docOffset = getPos($img); + Tracker.setCursor(mode=='move'?mode:mode+'-resize'); + + if (mode == 'move') + return Tracker.activateHandlers(createMover(pos), doneSelect); + + var fc = Coords.getFixed(); + var opp = oppLockCorner(mode); + var opc = Coords.getCorner(oppLockCorner(opp)); + + Coords.setPressed(Coords.getCorner(opp)); + Coords.setCurrent(opc); + + Tracker.activateHandlers(dragmodeHandler(mode,fc),doneSelect); + }; + /*}}}*/ + function dragmodeHandler(mode,f)/*{{{*/ + { + return function(pos) { + if (!options.aspectRatio) switch(mode) + { + case 'e': pos[1] = f.y2; break; + case 'w': pos[1] = f.y2; break; + case 'n': pos[0] = f.x2; break; + case 's': pos[0] = f.x2; break; + } + else switch(mode) + { + case 'e': pos[1] = f.y+1; break; + case 'w': pos[1] = f.y+1; break; + case 'n': pos[0] = f.x+1; break; + case 's': pos[0] = f.x+1; break; + } + Coords.setCurrent(pos); + Selection.update(); + }; + }; + /*}}}*/ + function createMover(pos)/*{{{*/ + { + var lloc = pos; + KeyManager.watchKeys(); + + return function(pos) + { + Coords.moveOffset([pos[0] - lloc[0], pos[1] - lloc[1]]); + lloc = pos; + + Selection.update(); + }; + }; + /*}}}*/ + function oppLockCorner(ord)/*{{{*/ + { + switch(ord) + { + case 'n': return 'sw'; + case 's': return 'nw'; + case 'e': return 'nw'; + case 'w': return 'ne'; + case 'ne': return 'sw'; + case 'nw': return 'se'; + case 'se': return 'nw'; + case 'sw': return 'ne'; + }; + }; + /*}}}*/ + function createDragger(ord)/*{{{*/ + { + return function(e) { + if (options.disabled) return false; + if ((ord == 'move') && !options.allowMove) return false; + btndown = true; + startDragMode(ord,mouseAbs(e)); + e.stopPropagation(); + e.preventDefault(); + return false; + }; + }; + /*}}}*/ + function presize($obj,w,h)/*{{{*/ + { + var nw = $obj.width(), nh = $obj.height(); + if ((nw > w) && w > 0) + { + nw = w; + nh = (w/$obj.width()) * $obj.height(); + } + if ((nh > h) && h > 0) + { + nh = h; + nw = (h/$obj.height()) * $obj.width(); + } + xscale = $obj.width() / nw; + yscale = $obj.height() / nh; + $obj.width(nw).height(nh); + }; + /*}}}*/ + function unscale(c)/*{{{*/ + { + return { + x: parseInt(c.x * xscale), y: parseInt(c.y * yscale), + x2: parseInt(c.x2 * xscale), y2: parseInt(c.y2 * yscale), + w: parseInt(c.w * xscale), h: parseInt(c.h * yscale) + }; + }; + /*}}}*/ + function doneSelect(pos)/*{{{*/ + { + var c = Coords.getFixed(); + if (c.w > options.minSelect[0] && c.h > options.minSelect[1]) + { + Selection.enableHandles(); + Selection.done(); + } + else + { + Selection.release(); + } + Tracker.setCursor( options.allowSelect?'crosshair':'default' ); + }; + /*}}}*/ + function newSelection(e)/*{{{*/ + { + if (options.disabled) return false; + if (!options.allowSelect) return false; + btndown = true; + docOffset = getPos($img); + Selection.disableHandles(); + myCursor('crosshair'); + var pos = mouseAbs(e); + Coords.setPressed(pos); + Tracker.activateHandlers(selectDrag,doneSelect); + KeyManager.watchKeys(); + Selection.update(); + + e.stopPropagation(); + e.preventDefault(); + return false; + }; + /*}}}*/ + function selectDrag(pos)/*{{{*/ + { + Coords.setCurrent(pos); + Selection.update(); + }; + /*}}}*/ + function newTracker() + { + var trk = $('<div></div>').addClass(cssClass('tracker')); + $.browser.msie && trk.css({ opacity: 0, backgroundColor: 'white' }); + return trk; + }; + + // }}} + // API methods {{{ + + function animateTo(a)/*{{{*/ + { + var x1 = a[0] / xscale, + y1 = a[1] / yscale, + x2 = a[2] / xscale, + y2 = a[3] / yscale; + + if (animating) return; + + var animto = Coords.flipCoords(x1,y1,x2,y2); + var c = Coords.getFixed(); + var animat = initcr = [ c.x, c.y, c.x2, c.y2 ]; + var interv = options.animationDelay; + + var x = animat[0]; + var y = animat[1]; + var x2 = animat[2]; + var y2 = animat[3]; + var ix1 = animto[0] - initcr[0]; + var iy1 = animto[1] - initcr[1]; + var ix2 = animto[2] - initcr[2]; + var iy2 = animto[3] - initcr[3]; + var pcent = 0; + var velocity = options.swingSpeed; + + Selection.animMode(true); + + var animator = function() + { + return function() + { + pcent += (100 - pcent) / velocity; + + animat[0] = x + ((pcent / 100) * ix1); + animat[1] = y + ((pcent / 100) * iy1); + animat[2] = x2 + ((pcent / 100) * ix2); + animat[3] = y2 + ((pcent / 100) * iy2); + + if (pcent < 100) animateStart(); + else Selection.done(); + + if (pcent >= 99.8) pcent = 100; + + setSelectRaw(animat); + }; + }(); + + function animateStart() + { window.setTimeout(animator,interv); }; + + animateStart(); + }; + /*}}}*/ + function setSelect(rect)//{{{ + { + setSelectRaw([rect[0]/xscale,rect[1]/yscale,rect[2]/xscale,rect[3]/yscale]); + }; + //}}} + function setSelectRaw(l) /*{{{*/ + { + Coords.setPressed([l[0],l[1]]); + Coords.setCurrent([l[2],l[3]]); + Selection.update(); + }; + /*}}}*/ + function setOptions(opt)/*{{{*/ + { + if (typeof(opt) != 'object') opt = { }; + options = $.extend(options,opt); + + if (typeof(options.onChange)!=='function') + options.onChange = function() { }; + + if (typeof(options.onSelect)!=='function') + options.onSelect = function() { }; + + }; + /*}}}*/ + function tellSelect()/*{{{*/ + { + return unscale(Coords.getFixed()); + }; + /*}}}*/ + function tellScaled()/*{{{*/ + { + return Coords.getFixed(); + }; + /*}}}*/ + function setOptionsNew(opt)/*{{{*/ + { + setOptions(opt); + interfaceUpdate(); + }; + /*}}}*/ + function disableCrop()//{{{ + { + options.disabled = true; + Selection.disableHandles(); + Selection.setCursor('default'); + Tracker.setCursor('default'); + }; + //}}} + function enableCrop()//{{{ + { + options.disabled = false; + interfaceUpdate(); + }; + //}}} + function cancelCrop()//{{{ + { + Selection.done(); + Tracker.activateHandlers(null,null); + }; + //}}} + function destroy()//{{{ + { + $div.remove(); + $origimg.show(); + }; + //}}} + + function interfaceUpdate(alt)//{{{ + // This method tweaks the interface based on options object. + // Called when options are changed and at end of initialization. + { + options.allowResize ? + alt?Selection.enableOnly():Selection.enableHandles(): + Selection.disableHandles(); + + Tracker.setCursor( options.allowSelect? 'crosshair': 'default' ); + Selection.setCursor( options.allowMove? 'move': 'default' ); + + $div.css('backgroundColor',options.bgColor); + + if ('setSelect' in options) { + setSelect(opt.setSelect); + Selection.done(); + delete(options.setSelect); + } + + if ('trueSize' in options) { + xscale = options.trueSize[0] / boundx; + yscale = options.trueSize[1] / boundy; + } + + xlimit = options.maxSize[0] || 0; + ylimit = options.maxSize[1] || 0; + xmin = options.minSize[0] || 0; + ymin = options.minSize[1] || 0; + + if ('outerImage' in options) + { + $img.attr('src',options.outerImage); + delete(options.outerImage); + } + + Selection.refresh(); + }; + //}}} + + // }}} + + $hdl_holder.hide(); + interfaceUpdate(true); + + var api = { + animateTo: animateTo, + setSelect: setSelect, + setOptions: setOptionsNew, + tellSelect: tellSelect, + tellScaled: tellScaled, + + disable: disableCrop, + enable: enableCrop, + cancel: cancelCrop, + + focus: KeyManager.watchKeys, + + getBounds: function() { return [ boundx * xscale, boundy * yscale ]; }, + getWidgetSize: function() { return [ boundx, boundy ]; }, + + release: Selection.release, + destroy: destroy + + }; + + $origimg.data('Jcrop',api); + return api; +}; + +$.fn.Jcrop = function(options)/*{{{*/ +{ + function attachWhenDone(from)/*{{{*/ + { + var loadsrc = options.useImg || from.src; + var img = new Image(); + img.onload = function() { $.Jcrop(from,options); }; + img.src = loadsrc; + }; + /*}}}*/ + if (typeof(options) !== 'object') options = { }; + + // Iterate over each object, attach Jcrop + this.each(function() + { + // If we've already attached to this object + if ($(this).data('Jcrop')) + { + // The API can be requested this way (undocumented) + if (options == 'api') return $(this).data('Jcrop'); + // Otherwise, we just reset the options... + else $(this).data('Jcrop').setOptions(options); + } + // If we haven't been attached, preload and attach + else attachWhenDone(this); + }); + + // Return "this" so we're chainable a la jQuery plugin-style! + return this; +}; +/*}}}*/ + +})(jQuery); + + + /* + http://www.JSON.org/json2.js + 2008-11-19 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or '&nbsp;'), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the object holding the key. + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. +*/ + +/*jslint evil: true */ + +/*global JSON */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (!this.JSON) { + JSON = {}; +} +(function () { + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : + gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/. +test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +})(); + + \ No newline at end of file diff --git a/components/editorControls/imagecropper/SaveData.cs b/components/editorControls/imagecropper/SaveData.cs new file mode 100644 index 0000000000..2f047770c9 --- /dev/null +++ b/components/editorControls/imagecropper/SaveData.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections; +using System.Xml; + +namespace umbraco.editorControls.imagecropper +{ + public class SaveData + { + public ArrayList data { get; set; } + + public string Xml(Config config, ImageInfo imageInfo) + { + XmlDocument doc = createBaseXmlDocument(); + XmlNode root = doc.DocumentElement; + + if (root == null) return null; + + XmlNode dateStampNode = doc.CreateNode(XmlNodeType.Attribute, "date", null); + dateStampNode.Value = imageInfo.DateStamp.ToString(); + root.Attributes.SetNamedItem(dateStampNode); + + for (int i = 0; i < data.Count; i++) + { + Crop crop = (Crop) data[i]; + Preset preset = (Preset) config.presets[i]; + + XmlNode newNode = doc.CreateElement("crop"); + + XmlNode nameNode = doc.CreateNode(XmlNodeType.Attribute, "name", null); + nameNode.Value = preset.Name; + newNode.Attributes.SetNamedItem(nameNode); + + XmlNode xNode = doc.CreateNode(XmlNodeType.Attribute, "x", null); + xNode.Value = crop.X.ToString(); + newNode.Attributes.SetNamedItem(xNode); + + XmlNode yNode = doc.CreateNode(XmlNodeType.Attribute, "y", null); + yNode.Value = crop.Y.ToString(); + newNode.Attributes.SetNamedItem(yNode); + + XmlNode x2Node = doc.CreateNode(XmlNodeType.Attribute, "x2", null); + x2Node.Value = crop.X2.ToString(); + newNode.Attributes.SetNamedItem(x2Node); + + XmlNode y2Node = doc.CreateNode(XmlNodeType.Attribute, "y2", null); + y2Node.Value = crop.Y2.ToString(); + newNode.Attributes.SetNamedItem(y2Node); + + if (config.GenerateImages) + { + XmlNode urlNode = doc.CreateNode(XmlNodeType.Attribute, "url", null); + urlNode.Value = String.Format("{0}/{1}_{2}.jpg", + imageInfo.RelativePath.Substring(0, + imageInfo.RelativePath.LastIndexOf( + '/')), + imageInfo.Name, + preset.Name); + newNode.Attributes.SetNamedItem(urlNode); + } + + root.AppendChild(newNode); + } + + return doc.InnerXml; + } + + public SaveData() + { + data = new ArrayList(); + } + + public SaveData(string raw) + { + data = new ArrayList(); + + string[] crops = raw.Split(';'); + + foreach (string crop in crops) + { + var val = crop.Split(','); + + data.Add( + new Crop( + Convert.ToInt32(val[0]), + Convert.ToInt32(val[1]), + Convert.ToInt32(val[2]), + Convert.ToInt32(val[3]) + ) + ); + } + + } + + private static XmlDocument createBaseXmlDocument() + { + XmlDocument doc = new XmlDocument(); + XmlNode root = doc.CreateElement("crops"); + doc.AppendChild(root); + return doc; + } + + + } +} \ No newline at end of file diff --git a/components/editorControls/imagecropper/SmartListBox.cs b/components/editorControls/imagecropper/SmartListBox.cs new file mode 100644 index 0000000000..5556e78777 --- /dev/null +++ b/components/editorControls/imagecropper/SmartListBox.cs @@ -0,0 +1,47 @@ +using System.Web.UI.WebControls; + +namespace umbraco.editorControls.imagecropper +{ + public class SmartListBox : ListBox + { + //Moves the selected items up one level + public void MoveUp() + { + + for (int i = 0; i < Items.Count; i++) + { + if (Items[i].Selected)//identify the selected item + { + //swap with the top item(move up) + if (i > 0 && !Items[i - 1].Selected) + { + ListItem bottom = Items[i]; + Items.Remove(bottom); + Items.Insert(i - 1, bottom); + Items[i - 1].Selected = true; + } + } + } + } + //Moves the selected items one level down + public void MoveDown() + { + int startindex = Items.Count - 1; + for (int i = startindex; i > -1; i--) + { + if (Items[i].Selected)//identify the selected item + { + //swap with the lower item(move down) + if (i < startindex && !Items[i + 1].Selected) + { + ListItem bottom = Items[i]; + Items.Remove(bottom); + Items.Insert(i + 1, bottom); + Items[i + 1].Selected = true; + } + + } + } + } + } +} \ No newline at end of file diff --git a/components/editorControls/umbraco.editorControls.csproj b/components/editorControls/umbraco.editorControls.csproj index 04b36a507d..cf9464c281 100644 --- a/components/editorControls/umbraco.editorControls.csproj +++ b/components/editorControls/umbraco.editorControls.csproj @@ -211,6 +211,18 @@ Code + + + + + + + + + + + + Code @@ -343,6 +355,7 @@ Code + textFieldDataEditor.cs Designer diff --git a/umbraco/presentation/umbraco.presentation.csproj b/umbraco/presentation/umbraco.presentation.csproj index 3d2f70c7f6..c43fe710bb 100644 --- a/umbraco/presentation/umbraco.presentation.csproj +++ b/umbraco/presentation/umbraco.presentation.csproj @@ -1,6 +1,6 @@  - 9.0.21022 + 9.0.30729 2.0 {651E1350-91B6-44B7-BD60-7207006D7003} {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} @@ -1539,6 +1539,7 @@ + diff --git a/umbraco/presentation/umbraco_client/imagecropper/Jcrop.gif b/umbraco/presentation/umbraco_client/imagecropper/Jcrop.gif new file mode 100644 index 0000000000..72ea7ccb53 Binary files /dev/null and b/umbraco/presentation/umbraco_client/imagecropper/Jcrop.gif differ