AEM CQ 56 - Crop Images in a Workflow Step

Goal


Create workflow step to crop images. The cropped image is saved as payload image rendition. Check Demo VideoSource code and Package Install



Solution


For creating workflow, we use the Dialog Participant Step and configure dialog with a CQ.html5.form.SmartImage widget. User selects crop co-ordinates; the next automated step in workflow reads crop numbers, crops the image and saves it as logo.png renditioon

Create Wokflow Step


1) Create and deploy workflow step ImageCropStep as OSGI bundle, to read crop co-ordinates and crop the image ( Read this post on how to create an OSGI component )

package apps.mysample.imagecrop;

import com.day.cq.commons.ImageHelper;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.commons.util.DamUtil;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
import com.day.image.Layer;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.jcr.resource.JcrResourceResolverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.*;
import java.awt.*;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;

@Component(metatype = true)
@Service
@Property(name = "process.label", value = "Crop Image In Inbox")
public class ImageCropStep implements WorkflowProcess {
    private static final Logger log = LoggerFactory.getLogger(ImageCropStep.class);

    @Reference(policy = ReferencePolicy.STATIC)
    private JcrResourceResolverFactory factory;


    @Override
    public void execute(WorkItem item, WorkflowSession session, MetaDataMap metaData)
                            throws WorkflowException {
        try{
            createCroppedRendition(item, session);
        }catch(Exception e){
            log.error("error generating cropped rendition", e);
            throw new WorkflowException("Crop failed", e);
        }
    }

    private void createCroppedRendition(WorkItem item, WorkflowSession session) throws Exception {
        Resource resource = getResourceFromPayload(item, session.getSession());
        Resource dataResource = resource.getChild("jcr:content/renditions/original/jcr:content");

        ValueMap map = resource.getChild("jcr:content").adaptTo(ValueMap.class);
        String cords = map.get("imageCrop", String.class);

        if(StringUtils.isEmpty(cords)){
            log.warn("crop co-ordinates missing in jcr:content for: " + resource.getPath());
            return;
        }

        Layer layer = ImageHelper.createLayer(dataResource);
        Rectangle rect = ImageHelper.getCropRect(cords, resource.getPath());

        layer.crop(rect);

        OutputStream out = null;
        InputStream in = null;
        String mimeType = "image/png";

        try {
            File file = File.createTempFile("cropped", ".tmp");
            out = FileUtils.openOutputStream(file);

            layer.write(mimeType, 1.0, out);
            IOUtils.closeQuietly(out);

            in = FileUtils.openInputStream(file);
            Asset asset = DamUtil.resolveToAsset(resource);

            asset.addRendition("logo.png", in, mimeType);
            session.getSession().save();
        } finally {
            IOUtils.closeQuietly(in);
            IOUtils.closeQuietly(out);
        }
    }

    private Resource getResourceFromPayload(WorkItem item, Session session) {
        if (!item.getWorkflowData().getPayloadType().equals("JCR_PATH")) {
            return null;
        }

        String path = item.getWorkflowData().getPayload().toString();
        return factory.getResourceResolver(session).getResource(path);
    }
}

2) Login to CRXDE Lite, create folders /apps/imagecropstep/apps/imagecropstep/install

3) Deploy bundle with ImageCropStep to /apps/imagecropstep/install

Create Dialog Image Widget


1) Create node /apps/imagecropstep/editor of type sling:Folder

2) Create node /apps/imagecropstep/editor/clientlib of type cq:ClientLibraryFolder and add property categories with String value cq.widgets

3) Create node /apps/imagecropstep/editor/clientlib/js.txt of type nt:file and add

                            SmartImage.js

4) Create node /apps/imagecropstep/editor/clientlib/SmartImage.js of type nt:file and add the following code

CQ.Ext.ns("MyClientLib");

CQ.Ext.override(CQ.form.SmartImage.ImagePanel, {
    addCanvasClass: function(clazz) {
        var imageCanvas = CQ.Ext.get(this.imageCanvas);

        if(imageCanvas){
            imageCanvas.addClass(clazz);
        }
    },

    removeCanvasClass: function(clazz) {
        var imageCanvas = CQ.Ext.get(this.imageCanvas);

        if(imageCanvas){
            imageCanvas.removeClass(clazz);
        }
    }
});

MyClientLib.InboxSmartImage = CQ.Ext.extend(CQ.html5.form.SmartImage, {
    Record:  CQ.data.SlingRecord.create([]),

    constructor: function(config) {
        config = config || {};
        config.fileReferenceParameter = "imageReference";
        config.name = "placeHolder";
        MyClientLib.InboxSmartImage.superclass.constructor.call(this,config);
    },

    clearInvalid: function() {
        if(!this.uploadPanel || !this.uploadPanel.body) {
            return;
        }

        MyClientLib.InboxSmartImage.superclass.clearInvalid.call(this);
    },

    afterRender: function() {
        MyClientLib.InboxSmartImage.superclass.afterRender.call(this);

        var dialog = this.findParentByType('dialog');
        dialog.setSize(900,550);

        var imageAdded = false;

        dialog.on('afterlayout', function(){
            if(!imageAdded){
                var rec = new this.Record({},{});

                var inbox = CQ.Ext.getCmp(CQ.workflow.Inbox.List.ID);
                var imagePath = inbox.getSelectionModel().getSelections()[0];
                imagePath = imagePath.data.payloadPath;

                rec.data["imageReference"] = imagePath;
                this.processRecord(rec);

                imageAdded = true;
            }
        },this);
    }
});

CQ.Ext.reg('inboxsmartimage', MyClientLib.InboxSmartImage);


5) In the above step we are extending html5smartimage, registering it as inboxsmartimage, with necessary changes to load image available in the workflow payload ( Line 51-53 )

6) Create node /apps/imagecropstep/editor/dialog of type cq:Dialog and add the following xml chunk

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="cq:Dialog"
    title="Image Dialog"
    xtype="dialog">
    <items jcr:primaryType="cq:WidgetCollection">
        <image
            jcr:primaryType="cq:Widget"
            cropParameter="./jcr:content/imageCrop"
            fieldLabel="Image"
            height="250"
            xtype="inboxsmartimage"/>
    </items>
</jcr:root>


Create Worflow


1) Access Workflow console (http://localhost:4502/libs/cq/workflow/content/console.html)

2) In Models tab click New and add title Inbox Crop

3) Double click Inbox Crop to open the workflow in a new tab ( http://localhost:4502/cf#/etc/workflow/models/inbox-crop.html ). Here is how it looks initially




4) Delete Step 1

5) From the SideKick -> Components tab -> Workflow, drag and drop the step Dialog Participant  Step



6) Double click to open the step and add

               Common tab -> Title: Select Crop Coordinates
               Common tab -> Description: In this step the user selects crop co-ordinates saved under jcr:content of payload image in dam
               User/Group tab -> User/Group: /home/groups/a/administrators
               Dialog tab -> Dialog path: /apps/imagecropstep/editor/dialog

7) Dialog path /apps/imagecropstep/editor/dialog is the dialog created in Step 6 of above section Create Dialog Image Widget




8) From the SideKick -> Components tab -> Workflow, drag and drop the step Process Step



9) Double click to open it and add

                             Common tab -> Title: Crop Image
                             Common tab -> Description: This automated step crops the image, reading crop coordinates from ./jcr:content/imageCrop and creates rendition /jcr:content/renditions/logo.png
                             Process tab -> Process: Select Crop Image In Inbox
                             Handler Advance: Check




10) The Process selected in Process tab above was deployed as OSGI bundle in section Create Wokflow Step above

11) Important, save the workflow




Start workflow on an Image


1) Access DAM admin console (http://localhost:4502/damadmin) and upload an image, say /Mine/Desert.jpg

2) Rightclick on Desert.jpg and click Workflow

3) Select Inbox Crop for Workflow, enter comment Complete this workflow step to crop image

4) Click Start

5) Access Inbox console (http://localhost:4502/inbox) and you should see the step Select Crop Coordinates



6) Rightclick on the step and select Complete

7) You should see the image ready for cropping, do the crop



8) Click Ok, step should complete, browse CRXDE Lite (http://localhost:4502/crx/de) /content/dam/Mine/Desert.jpg/jcr:content and you should see the crop co-ordinates with property imageCrop



9) The Crop Image in Inbox automated step should have created the cropped image as rendition /content/dam/Mine/Desert.jpg/jcr:content/renditions/logo.png



10) Access the cropped image to check if it is ok http://localhost:4502/content/dam/Mine/Desert.jpg/jcr:content/renditions/logo.png







1 comment:

  1. It is great to have the opportunity to read a good quality article with useful information on topics that plenty are interested on.

    www.imarksweb.org

    ReplyDelete