HTML 5 Smart Image Component Custom Aspect Ratios

Goal


Create a html 5 smart image component that supports custom aspect ratios, image cropping. An author can create images of different aspect ratios using the same image component. Before you proceed, the process explained here is not Adobe suggested approach, it's just a thought; Package install, Source code and Video demonstration are available for download..





Prerequisites


If you are new to CQ pls visit this blog post; it explains the page component basics and setting up your IDE

Create the Component


1) In your CRXDE Lite http://localhost:4502/crx/de, create the below folder and save changes

                      /apps/imagecustomar

2) Copy the component /libs/foundation/components/logo and paste it in path /apps/imagecustomar

3) Rename /apps/imagecustomar/logo to /apps/imagecustomar/image

4) Rename /apps/imagecustomar/image/logo.jsp to /apps/imagecustomar/image/image.jsp

5) Change the package statement of /apps/imagecustomar/image/img.GET.java from libs.foundation.components.logo to apps.imagecustomar.image

6) Change the following properties of /apps/imagecustomar/image

                     componentGroup - My Components
                     jcr:title - Image with Custom Aspect Ratios

7) Rename /apps/imagecustomar/image/design_dialog to /apps/imagecustomar/image/dialog

8) Delete /apps/imagecustomar/image/dialog/items/basic

9) Replace the code in /apps/imagecustomar/image/image.jsp with the following...

<%@include file="/libs/foundation/global.jsp" %>
<%@ page import="com.day.cq.commons.Doctype,
                 com.day.cq.wcm.foundation.Image,
                 java.io.PrintWriter" %>
<%
    try {
        Resource res = null;

        if (currentNode.hasProperty("imageReference")) {
            res = resource;
        }
%>

<%
        if (res == null) {
%>
            Configure Image
<%
        } else {
            Image img = new Image(res);
            img.setItemName(Image.NN_FILE, "image");
            img.setItemName(Image.PN_REFERENCE, "imageReference");
            img.setSelector("img");
            img.setDoctype(Doctype.fromRequest(request));
            img.setAlt("Home");
            img.draw(out);
        }
    } catch (Exception e) {
        e.printStackTrace(new PrintWriter(out));
    }
%>


10) Add this Image component on a page and select an image. The image dialog with cropping features enabled looks like below. Here, Free crop has default aspect ratio (0,0)




10) We now have a basic image component with ootb features ( crop, rotate etc ). Let us modify this component to add custom aspect ratios; in the process we also create and register a new ExtJS xtype

Create and Register xtype


1) Create the node /apps/imagecustomar/image/clientlib of type cq:ClientLibraryFolder and add the following properties

             categories - String[] - mycomponent.imagear
             dependencies - String[] - cq.widgets

2) Create file (type nt:file) /apps/imagecustomar/image/clientlib/js.txt and add the following

              imagear.js

3) Create file /apps/imagecustomar/image/clientlib/imagear.js. For now, add the following code

              alert("hi");

4) Modify /apps/imagecustomar/image/image.jsp to include the clientlib created above

               <cq:includeClientLib categories="mycomponent.imagear"/>

5) Save changes and access the component dialog; an alert "hi" should popup, confirming the js file load.

6) Add the following JS code to imagear.js. This logic adds the custom aspect ratios UI changes to crop tool

var MyClientLib = MyClientLib || {};

MyClientLib.Html5SmartImage = CQ.Ext.extend(CQ.html5.form.SmartImage, {
    crops: {},

    constructor: function (config) {
        config = config || {};

        var aRatios = {
            "freeCrop": {
                "value": "0,0",
                "text": CQ.I18n.getMessage("Free crop")
            }
        };

        var tObj = this;

        $.each(config, function (key, value) {
            if (key.endsWith("AspectRatio")) {
                var text = config[key + "Text"];

                if (!text) {
                    text = key;
                }

                if (!value) {
                    value = "0,0";
                }

                aRatios[key] = {
                    "value": value,
                    "text": text
                };

                tObj.crops[key] = { text: text, cords : ''};
            }
        });

        var defaults = { "cropConfig": { "aspectRatios": aRatios } };
        config = CQ.Util.applyDefaults(config, defaults);

        MyClientLib.Html5SmartImage.superclass.constructor.call(this, config);
    },

    initComponent: function () {
        MyClientLib.Html5SmartImage.superclass.initComponent.call(this);

        var imgTools = this.imageToolDefs;
        var cropTool;

        if(imgTools){
            for(var x = 0; x < imgTools.length; x++){
                if(imgTools[x].toolId == 'smartimageCrop'){
                    cropTool = imgTools[x];
                    break;
                }
            }
        }

        if(!cropTool){
            return;
        }

        for(var x in this.crops){
            if(this.crops.hasOwnProperty(x)){
                var field = new CQ.Ext.form.Hidden({
                    id: x,
                    name: "./" + x
                });

                this.add(field);

                field = new CQ.Ext.form.Hidden({
                    name: "./" + x + "Text",
                    value: this.crops[x].text
                });

                this.add(field);
            }
        }

        var userInterface = cropTool.userInterface;

        this.on("loadimage", function(){
            var aRatios = userInterface.aspectRatioMenu.findByType("menucheckitem");

            if(!aRatios){
                return;
            }

            for(var x = 0; x < aRatios.length; x++){
                if(aRatios[x].text !== "Free crop"){
                    aRatios[x].on('click', function(radio){
                        var key = this.getCropKey(radio.text);

                        if(!key){
                            return;
                        }

                        if(this.crops[key].cords){
                            this.setCoords(cropTool, this.crops[key].cords);
                        }else{
                            var field = CQ.Ext.getCmp(key);
                            this.crops[key].cords = this.getRect(radio, userInterface);
                            field.setValue(this.crops[key].cords);
                        }
                    },this);
                }

                var key = this.getCropKey(aRatios[x].text);

                if(key && this.dataRecord && this.dataRecord.data[key]){
                    this.crops[key].cords = this.dataRecord.data[key];

                    var field = CQ.Ext.getCmp(key);
                    field.setValue(this.crops[key].cords);
                }
            }
        });

        cropTool.workingArea.on("contentchange", function(changeDef){
            var aRatios = userInterface.aspectRatioMenu.findByType("menucheckitem");
            var aRatioChecked;

            if(aRatios){
                for(var x = 0; x < aRatios.length; x++){
                    if(aRatios[x].checked === true){
                        aRatioChecked = aRatios[x];
                        break;
                    }
                }
            }

            if(!aRatioChecked){
                return;
            }

            var key = this.getCropKey(aRatioChecked.text);
            var field = CQ.Ext.getCmp(key);

            this.crops[key].cords = this.getRect(aRatioChecked, userInterface);
            field.setValue(this.crops[key].cords);
        }, this);
    },

    getCropKey: function(text){
        for(var x in this.crops){
            if(this.crops.hasOwnProperty(x)){
                if(this.crops[x].text == text){
                    return x;
                }
            }
        }

        return null;
    },

    getRect: function (radio, ui) {
        var ratioStr = "";
        var aspectRatio = radio.value;

        if ((aspectRatio != null) && (aspectRatio != "0,0")) {
            ratioStr = "/" + aspectRatio;
        }

        if (ui.cropRect == null) {
            return ratioStr;
        }

        return ui.cropRect.x + "," + ui.cropRect.y + "," + (ui.cropRect.x + ui.cropRect.width) + ","
            + (ui.cropRect.y + ui.cropRect.height) + ratioStr;
    },

    setCoords: function (cropTool, cords) {
        cropTool.initialValue = cords;
        cropTool.onActivation();
    }
});

CQ.Ext.reg("myhtml5smartimage", MyClientLib.Html5SmartImage);


7) To create the following image source paths...

/content/firstapp-demo-site/test/_jcr_content/par/image.img.png/2To1AspectRatio.jpg
/content/firstapp-demo-site/test/_jcr_content/par/image.img.png/9To1AspectRatio.jpg
/content/firstapp-demo-site/test/_jcr_content/par/image.img.png/5To1AspectRatio.jpg

Replace the code in /apps/imagecustomar/image/image.jsp with below code. This jsp renders the cropped custom aspect ratio images...

<%@include file="/libs/foundation/global.jsp" %>
<%@ page import="com.day.cq.wcm.foundation.Image,
                 java.io.PrintWriter" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>

<cq:includeClientLib js="mycomponent.imagear"/>

<%
    try {
        Resource res = null;

        if (currentNode.hasProperty("imageReference")) {
            res = resource;
        }

        if (res == null) {
%>
            Configure Image
<%
        } else {
            PropertyIterator itr = currentNode.getProperties();
            Property prop = null; String text = "";
            Map<String, String> aMap = new HashMap<String, String>();

            while(itr.hasNext()){
                prop = itr.nextProperty();

                if(prop.getName().endsWith("AspectRatio")){
                    text = prop.getName();

                    if(currentNode.hasProperty(prop.getName() + "Text")){
                        text = currentNode.getProperty(prop.getName() + "Text").getString();
                    }

                    aMap.put(prop.getName(), text);
                }
            }

            Image img = null; String src = null;

            if(aMap.isEmpty()){
%>
                Cropped Images with custom aspect ratios not available
<%
            }else{
                for(Map.Entry entry : aMap.entrySet()){
                    img = new Image(res);
                    img.setItemName(Image.PN_REFERENCE, "imageReference");
                    img.setSuffix(entry.getKey() + ".jpg");
                    img.setSelector("img");

                    src = img.getSrc();
%>
                    <br><br><b><%=entry.getValue()%></b><br><br>
                    <img src='<%=src%>'/>
<%
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace(new PrintWriter(out));
    }
%>

8) Add the following code to /apps/imagecustomar/image/img.GET.java. This java logic reads crop co-ordinates from CRX and outputs the image bytes to browser

package apps.imagecustomar.image;

import java.awt.*;
import java.io.IOException;
import java.io.InputStream;

import javax.jcr.RepositoryException;
import javax.jcr.Property;
import javax.servlet.http.HttpServletResponse;

import com.day.cq.commons.ImageHelper;
import com.day.cq.wcm.foundation.Image;
import com.day.cq.wcm.commons.AbstractImageServlet;
import com.day.image.Layer;
import org.apache.commons.io.IOUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;

public class img_GET extends AbstractImageServlet {
    protected Layer createLayer(ImageContext c) throws RepositoryException, IOException {
        return null;
    }

    protected void writeLayer(SlingHttpServletRequest req, SlingHttpServletResponse resp, ImageContext c, Layer layer)
                                throws IOException, RepositoryException {
        Image image = new Image(c.resource);
        image.setItemName(Image.NN_FILE, "image");
        image.setItemName(Image.PN_REFERENCE, "imageReference");

        if (!image.hasContent()) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        layer = image.getLayer(false, false,false);

        String rUri = req.getRequestURI();
        String ratio = rUri.substring(rUri.lastIndexOf("/") + 1, rUri.lastIndexOf(".jpg"));
        String cords = c.properties.get(ratio, "");

        boolean modified = false;

        if(!"".equals(cords)){
            Rectangle rect = ImageHelper.getCropRect(cords, c.resource.getPath());
            layer.crop(rect);

            modified = true;
        }else{
            modified = image.crop(layer) != null;
        }

        modified |= image.resize(layer) != null;
        modified |= image.rotate(layer) != null;

        if (modified) {
            resp.setContentType(c.requestImageType);
            layer.write(c.requestImageType, 1.0, resp.getOutputStream());
        } else {
            Property data = image.getData();
            InputStream in = data.getStream();
            resp.setContentLength((int) data.getLength());
            String contentType = image.getMimeType();

            if (contentType.equals("application/octet-stream")) {
                contentType=c.requestImageType;
            }

            resp.setContentType(contentType);
            IOUtils.copy(in, resp.getOutputStream());
            in.close();
        }

        resp.flushBuffer();
    }
}

9) Finally, add the following properties in your CRX node /apps/imagecustomar/image/dialog/items/img





14 comments:

  1. Very interesting. I need to add functionality where when you upload a new image will be saved maps that were imposed on the old image. Is this possible and if so, where can I get information? I would be grateful for any informations.

    ReplyDelete
  2. Andrii, do you mean preserving the existing maps when new image is uploaded? http://experience-aem.blogspot.com/2014/03/aem-cq-56-html5smarimage-save-existing-image-maps.html

    ReplyDelete
    Replies
    1. Thank you so much! This is exactly what I was looking for!

      Delete
  3. Hi sreekanth this is a very interesting component and is just what I need, but I'm having troubles to make it work. basically the "Image crop tools" menu only shows the "Free crop" option, and because the "getCropKey" function return null for "Free crop" everything starts to fail. Do you know what could be happening that the crop menu is not being populated with the other crop options?

    ReplyDelete
    Replies
    1. hello Sebastian, have you specified the custom aspect ratios in /apps/imagecustomar/image/dialog/items/img ( check the last screenshot in blog post above)

      Delete
    2. yes! I have it just like the screenshot, I'm using CQ 5.6.1 could that be a problem? I'm debugging the JS and everything looks fine, at least in the constructor the config has all the crop options. MyClientLib.Html5SmartImage.superclass.constructor.call(this, config);

      In the 'loadimage' and 'contentchange' functions, this line var aRatios = userInterface.aspectRatioMenu.findByType("menucheckitem"); always returns an array with only the free crop option.

      Delete
  4. hi in the touch ui, we have pre define list of aspect ratio but how do we add custom aspect ratios to it

    ReplyDelete
  5. I think that you need to check here some tips on how to write essay about tragic heroes. It could be really interesting

    ReplyDelete
  6. Writing a hypothesis is a complex task since it defines the further research. As such, one should not hurry up and carefully think about the topic and its key peculiarities or get diisertation help https://bestwritingservice.com/dissertation-hypothesis-help.html.

    ReplyDelete
  7. Resizer tools play important role for blogger post image. First of all congratulations on this post. This is really awesome. Great posts that we can sink our teeth into and really go to work. Your blog post is decent and meaningful for new users. A title is very unique and content is powerful to attract the audience directly. Continue to write this type of article in the future for us. 5 Image Resizer Resources To Crop Picture

    ReplyDelete
  8. Cool, for my essay I could not find this information, it's good that I got it on your site. Now I can finish writing my work. If anyone is interested, I took most of the information for writing here papermasters.org

    ReplyDelete
  9. Cool. So useful information and I don't know how I lived before. I use information from article every day and my life became easier. And more easier study life helps me to do when I buy an argumentative essay for my university studying. Thanks for the double combo I can more relax and live my life.

    ReplyDelete
  10. Thank you so much for clear information. I’m going to use it for my superior essay I hope I’ll get the best mark in college.
    The understanding in Adobe Experience is a great skill in a modern world. The knowledge you give us is very valuable.

    ReplyDelete
  11. Oh I remember this program, I was just starting to work with images and this program was very useful and easy to use. I even remember how the teacher gave the task to write about working with images and I wrote an prime essays about html 5 smart image.

    ReplyDelete