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 codeand 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");
<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