Sunday, February 2, 2014

Implement Jcrop Image Cropping in Sitecore Admin UI

The purpose of this exercise is to add an image cropping tool to any image field in the Sitecore Admin UI.  It comprises of multiple steps such as setting up the UI to have a button on image fields to open up this tool and then developing the actual tool.  To make things simpler, we will create the tool as a standalone ASPX page that will reside in the same Website project.

Part 1: Set Up Admin UI


The goal here is to make sure we add the "Crop Image" button to the top of all image fields.  To do this we have to go to the Core database in content editor.  Open the node:

/sitecore/system/Field types/Simple Types/Image/Menu/

Here, you will see all the subitems as buttons that reflect the screenshot above.  You can insert a new item in this folder but it is best to just duplicate an existing item.  Give the new item a name and then open it for more options.  Give the message an appropriate value.  We will use:

contentimage:crop(id=$Target)

for this example.

Since the built-in Image field class has already been defined, we would have to create a derived object that mirrors the functionalities of the Image field and append more features to it.  Use this as an example:

using System.Web.UI;
using System;
using Sitecore.Web.UI.Sheer;
using Sitecore;
using Sitecore.Text;
using Sitecore.Web.UI.Framework.Scripts;
using Sitecore.Globalization;
using Sitecore.Resources;

namespace MyCoolAssembly.Fields
{
    public class Image : Sitecore.Shell.Applications.ContentEditor.Image
    {
        public override void HandleMessage(Message message)
        {
            if (!ShouldHandleMessage(message))
            {
                return;
            }

            if (IsCropClick(message))
            {
                OpenApp();
                return;
            }

            base.HandleMessage(message);
        }

        private bool ShouldHandleMessage(Message message)
        {
            return IsCurrentControl(message)
                    && !string.IsNullOrWhiteSpace(message.Name);
        }

        private bool IsCurrentControl(Message message)
        {
            return AreEqualIgnoreCase(message["id"], ID);
        }

        private static bool IsCropClick(Message message)
        {
            return AreEqualIgnoreCase(message.Name, "contentimage:crop");
        }

        private void OpenApp()
        {
            UrlString urlString = new UrlString("/croptool.aspx");
            SheerResponse.Eval(new ShowEditorTab()
            {
                Header = "Image Cropper",
                Icon = Images.GetThemedImageSource("Business/32x32/data_replace.png"),
                Url = urlString.ToString(),
                Id = "CropTool",
                Closeable = true,
                Activate = true
            }.ToString());
        }

        private static bool AreEqualIgnoreCase(string one, string two)
        {
            return string.Equals(one, two, StringComparison.CurrentCultureIgnoreCase);
        }
    }
}

Notice that in the IsCropClick method, we check the message name to make sure this value matches the message defined earlier for this new button. 

Now, since we have overridden the default Image field and made a custom derived object, we need to make sure that Sitecore Admin knows about it.  Let's go to:

/sitecore/system/Field types/Simple Types/Image/

In the control field, modify it to point to the class that you just created.  In this example, it would be:

mca:Image

But what is mca?  We have to make sure that Sitecore knows mca is the prefix for the MyCoolAssembly library.  We can do this in the web.config:

<system.web>
    <pages>
      <controls>
        ...
        <add tagPrefix="mca" namespace="MyCoolAssembly" assembly="MyCoolAssembly"/>
      ...
      </controls>
   </pages>
</system.web>

Save all your work, rebuild you code and open up content editor.  Open an item with image fields.  If all is well, you should see the new button on top of each image field and clicking the button should open a new tab with the ASPX page inside.


Part 2: Set Up ASPX Page

This page will be a regular web form page with a reference to the item that was referenced by the image field that opened this page.  You would have to decide if you want to pass that value as a property when calling this tab in the OpenApp method above or to append the item id as part of the url parameter.

As for the front end, I suggest using Jcrop.  You can read more and download the tool here:

http://deepliquid.com/content/Jcrop.html

Jcrop will provide all the UI elements and data capturing you will need.  To work with Jcrop, you simply use an <img> tag to reference the image of this media library content item.  To call Jcrop on any <img> tag, all you have to do is call:


$(function(){ $('#jcrop_target').Jcrop(); });


If you need additonal parameters, you would have to consult the Jcrop manual and API for more details.  Also, Jcrop provides the GUI for cropping and capturing the cropped image coordinates and new dimensions. It does not perform the actual image manipulation and processing.  This would have to be done in the codebehind.  Jcrop provides some good examples of capturing the coordinates and dimensions.  This is a good example:

 $(function(){
    $('#jcrop_target').Jcrop({
        onChange: showCoords,
        onSelect: showCoords
     });
});

function showCoords(c)
{
    $('#x').val(c.x);
    $('#y').val(c.y);
    $('#x2').val(c.x2);
    $('#y2').val(c.y2);
    $('#w').val(c.w);
    $('#h').val(c.h);
};

Of course, it is best if the 6 values are stored using hidden input controls.  These values can be passed back to the codebehind upon form submission. 

In the codebehind, you need a method similar to this:

using SD = System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
...
static byte[] Crop(string Img, int Width, int Height, int X, int Y)
{
        try
        {
            using (SD.Image OriginalImage = SD.Image.FromFile(Img))
            {
                using (SD.Bitmap bmp = new SD.Bitmap(Width, Height))
                {
                    bmp.SetResolution(OriginalImage.HorizontalResolution, OriginalImage.VerticalResolution);
                    using (SD.Graphics Graphic = SD.Graphics.FromImage(bmp))
                    {
                        Graphic.SmoothingMode = SmoothingMode.AntiAlias;
                        Graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
                        Graphic.PixelOffsetMode = PixelOffsetMode.HighQuality;
                        Graphic.DrawImage(OriginalImage, new SD.Rectangle(0, 0, Width, Height), X, Y, Width, Height, SD.GraphicsUnit.Pixel);
                        MemoryStream ms = new MemoryStream();
                        bmp.Save(ms, OriginalImage.RawFormat);
                        bmp.Save(ms, OriginalImage.RawFormat);
                        return ms.GetBuffer();
                    }
                }
            }
        }
        catch (Exception Ex)
        {
            throw (Ex);
        }
}

Now save it to a temp location:

using (MemoryStream ms = new MemoryStream(CropImage, 0, CropImage.Length))
{
                ms.Write(CropImage, 0, CropImage.Length);
                using (SD.Image CroppedImage = SD.Image.FromStream(ms, true))
                {
                    if (ImageName.IndexOf("_t.jpg") != -1)
                    {
                        SaveTo = path + ImageName;
                    }
                    CroppedImage.Save(SaveTo, CroppedImage.RawFormat);
                }
}

The above code is just a sample and may need modifications to suit your needs.

Now we have to make sure we create a new media library content item with the new image and associate the original content item image field with this new media library item.  Also, we do not want to overwrite any existing images in the media library.  Follow these suggested steps:

1) Get the item name of original image, create new item in the same folder with the same base template and name it "originalimagename-currentdatetime".  This will ensure the new item does not have a duplicate name. 
2) Upload the new image and associate with this new media library content item.
3) Go back to the original content item that called this ASPX tool and change the image field value to this new media library item content id.
4) Reload content item.
 


3 comments:

  1. I'm on the fence about this, while more customization is good, I have a feeling this is a "in-progress" update, it just feels incomplete and half-way there.
    We use badge layout for apps on design approvals (visual projects), so the image being displayed is important. Old layout "feels like" it had larger images,
    maybe because the images were cropped more loosely so it's easier to tell which project it was at quick glance. Now the image is cropped closer, making it
    harder to scan thru at quick glance. I find myself needing to click into the project more often than usual. Which makes the whole user experience less
    efficient.
    I have a couple suggestions that might make it work better:
    1. Increase the height of the window the cover image is being displayed.
    2. Let us to choose which image to be displayed as "cover" (like how Pinterest handles cover images of each board, was hoping for this for a long time)
    3. Let us adjust which part of the image to show and how tight or loose the crop is (with a fixed window, let us move the image around and maybe enlarge or
    shrink it to control what shows thru the window. Pinterest does a limited form of this, which is very useful in making the cover image relevant)
    4. Allow Cover Image to be ordered in different hierarchy (currently every element can be ordered differently except the Cover Image, it seems to be stuck
    in the 2nd spot, would like the option to set it on another spot in the layout. This one seems like an easy fix, since you guys allow that for every other
    element already)

    ReplyDelete
  2. I'm on the fence about this, while more customization is good, I have a feeling this is a "in-progress" update, it just feels incomplete and half-way there.
    We use badge layout for apps on design approvals (visual projects), so the image being displayed is important. Old layout "feels like" it had larger images,
    maybe because the images were cropped more loosely so it's easier to tell which project it was at quick glance. Now the image is cropped closer, making it
    harder to scan thru at quick glance. I find myself needing to click into the project more often than usual. Which makes the whole user experience less
    efficient.
    I have a couple suggestions that might make it work better:
    1. Increase the height of the window the cover image is being displayed.
    2. Let us to choose which image to be displayed as "cover" (like how Pinterest handles cover images of each board, was hoping for this for a long time)
    3. Let us adjust which part of the image to show and how tight or loose the crop is (with a fixed window, let us move the image around and maybe enlarge or
    shrink it to control what shows thru the window. Pinterest does a limited form of this, which is very useful in making the cover image relevant)
    4. Allow Cover Image to be ordered in different hierarchy (currently every element can be ordered differently except the Cover Image, it seems to be stuck
    in the 2nd spot, would like the option to set it on another spot in the layout. This one seems like an easy fix, since you guys allow that for every other
    element already)

    ReplyDelete
  3. I'm on the fence about this, while more customization is good, I have a feeling this is a "in-progress" update, it just feels incomplete and half-way there.
    We use badge layout for apps on design approvals (visual projects), so the image being displayed is important. Old layout "feels like" it had larger images,
    maybe because the images were cropped more loosely so it's easier to tell which project it was at quick glance. Now the image is cropped closer, making it
    harder to scan thru at quick glance. I find myself needing to click into the project more often than usual. Which makes the whole user experience less
    efficient.
    I have a couple suggestions that might make it work better:
    1. Increase the height of the window the cover image is being displayed.
    2. Let us to choose which image to be displayed as "cover" (like how Pinterest handles cover images of each board, was hoping for this for a long time)
    3. Let us adjust which part of the image to show and how tight or loose the crop is (with a fixed window, let us move the image around and maybe enlarge or
    shrink it to control what shows thru the window. Pinterest does a limited form of this, which is very useful in making the cover image relevant)
    4. Allow Cover Image to be ordered in different hierarchy (currently every element can be ordered differently except the Cover Image, it seems to be stuck
    in the 2nd spot, would like the option to set it on another spot in the layout. This one seems like an easy fix, since you guys allow that for every other
    element already)

    ReplyDelete