Goal
AEM Cloud Version : 2021.2.4944.20210221T230729Z-210225 (Feb 21, 2021)
When new smart crop settings are added in Image Profile, assets have to be reprocessed to see the new crops (otherwise they show unprocessed). However reprocessing results in generating all crops again, so any manual adjustments made to the previous crops by creatives get overwritten. This post explains how to preserve the manually adjusted smart crops, and generate new unprocessed ones...for scene7 api check this documentation
Demo | Package Install | Github
Process New Smart Crops
Unprocessed crops
Smart Crop Update Workflow
Solution
1) Add a datasource to read the existing smart crops /apps/eaem-cs-process-new-smart-crops/extensions/smart-crop-renditions
<%@include file="/libs/granite/ui/global.jsp"%> <%@page session="false" import="java.util.Iterator, org.apache.sling.commons.json.JSONObject, com.adobe.granite.ui.components.Tag"%> <%@ page import="com.adobe.granite.ui.components.ds.DataSource" %> <%@ page import="org.apache.sling.commons.json.JSONArray" %> <%@ page import="apps.experienceaem.assets.core.services.DMCService" %> <% ValueMap dynVM = null; JSONObject dynRenditions = new JSONObject(); Resource dynResource = null; DMCService dmcService = sling.getService(DMCService.class); response.setContentType("application/json"); DataSource rendsDS = null; try{ rendsDS = cmp.getItemDataSource(); }catch(Exception e){ //could be pixel crop, ignore... } if(rendsDS == null){ dynRenditions.write(response.getWriter()); return; } for (Iterator<Resource> items = rendsDS.iterator(); items.hasNext();) { JSONObject dynRendition = new JSONObject(); dynResource = items.next(); dynVM = dynResource.getValueMap(); String name = String.valueOf(dynVM.get("breakpoint-name")); dynRendition.put("name", name); dynRendition.put("cropdata", getCropData(dynVM)); dynRenditions.put(name, dynRendition); } dynRenditions.write(response.getWriter()); %> <%! private static JSONArray getCropData(ValueMap dynVM) throws Exception{ JSONArray cropArray = new JSONArray(); JSONObject cropData = new JSONObject(); cropData.put("name", String.valueOf(dynVM.get("breakpoint-name"))); cropData.put("id", dynVM.get("id")); cropData.put("topN", dynVM.get("topN")); cropData.put("bottomN", dynVM.get("bottomN")); cropData.put("leftN", dynVM.get("leftN")); cropData.put("rightN", dynVM.get("rightN")); cropArray.put(cropData); return cropArray; } %>
2) To get the smart crops as json send the asset path as resource suffix to data source eg. https://author-pxxxxx-exxxxx.adobeaemcloud.com/apps/eaem-cs-process-new-smart-crops/extensions/smart-crop-renditions/renditions.html/content/dam/experience-aem/am-i-doing-this-right.png
3) Code a servlet apps.experienceaem.assets.core.servlets.UpdateSmartCropSettings to read the existing smart crops and kickoff the smart crop update workflow (added in next steps), when the button Process New Smart Crops is clicked in Asset Details console...
package apps.experienceaem.assets.core.servlets; import apps.experienceaem.assets.core.services.DMCService; import com.day.cq.workflow.WorkflowService; import com.day.cq.workflow.WorkflowSession; import com.day.cq.workflow.exec.WorkflowData; import com.day.cq.workflow.model.WorkflowModel; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.servlets.SlingAllMethodsServlet; import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper; import org.apache.sling.commons.json.JSONArray; import org.apache.sling.commons.json.JSONObject; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Session; import javax.servlet.RequestDispatcher; import javax.servlet.Servlet; import javax.servlet.ServletException; import java.io.CharArrayWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.Iterator; @Component( name = "Experience AEM Update Smart Crop settings", immediate = true, service = Servlet.class, property = { "sling.servlet.methods=POST", "sling.servlet.paths=/bin/eaem/update-smart-crops" } ) public class UpdateSmartCropSettings extends SlingAllMethodsServlet { private static final Logger log = LoggerFactory.getLogger(UpdateSmartCropSettings.class); private static String SMART_CROPS_RES = "/apps/eaem-cs-process-new-smart-crops/extensions/smart-crop-renditions/renditions.html"; private static String CROP_DATA = "cropdata"; private static final String UPDATE_SMART_CROPS_WF_PATH = "/var/workflow/models/experience-aem-update-smart-crops"; @Reference private DMCService dmcService; @Reference private WorkflowService workflowService; @Override protected final void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { try { String path = request.getParameter("path"); if(StringUtils.isEmpty(path)){ return; } RequestDispatcher dp = request.getRequestDispatcher(SMART_CROPS_RES + path); SlingHttpServletResponse wrapperResponse = new DefaultSlingModelResponseWrapper(response); dp.include(request, wrapperResponse); String smartCropsStr = wrapperResponse.toString(); if(StringUtils.isEmpty(smartCropsStr)){ return; } JSONObject smartCrops = new JSONObject(smartCropsStr); JSONArray smartCropsToUpdate = new JSONArray(); Iterator smartCropKeys = smartCrops.keys(); JSONObject smartCrop = null; while(smartCropKeys.hasNext()){ smartCrop = (JSONObject)smartCrops.get(String.valueOf(smartCropKeys.next())); if(!smartCrop.has(CROP_DATA)){ continue; } JSONObject currentCrop = (JSONObject)(((JSONArray)smartCrop.get(CROP_DATA)).get(0)); if(StringUtils.isEmpty((String)currentCrop.get("id"))){ continue; } smartCropsToUpdate.put(currentCrop); } ResourceResolver resolver = request.getResourceResolver(); WorkflowSession wfSession = workflowService.getWorkflowSession(resolver.adaptTo(Session.class)); WorkflowModel wfModel = wfSession.getModel(UPDATE_SMART_CROPS_WF_PATH); WorkflowData wfData = wfSession.newWorkflowData("JCR_PATH", path); wfData.getMetaDataMap().put(DMCService.SMART_CROPS_JSON, smartCropsToUpdate.toString()); wfSession.startWorkflow(wfModel, wfData); smartCropsToUpdate.write(response.getWriter()); } catch (Exception e) { log.error("Could not get the smart crop settings", e); response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } private class DefaultSlingModelResponseWrapper extends SlingHttpServletResponseWrapper { private CharArrayWriter writer; public DefaultSlingModelResponseWrapper (final SlingHttpServletResponse response) { super(response); writer = new CharArrayWriter(); } public PrintWriter getWriter() throws IOException { return new PrintWriter(writer); } public String toString() { return writer.toString(); } } }
4) Add the workflow process step apps.experienceaem.assets.core.services.UpdateSmartCropsProcess to update smart crops (restore to previous crop rect). The first step in workflow Scene7:Reprocess Assets generates all crops afresh (including the new unprocessed) and this step updates existing ones to their previous crop settings (assuming they have been manually adjusted....)
package apps.experienceaem.assets.core.services; import com.adobe.granite.workflow.WorkflowException; import com.adobe.granite.workflow.WorkflowSession; import com.adobe.granite.workflow.exec.WorkItem; import com.adobe.granite.workflow.exec.WorkflowData; import com.adobe.granite.workflow.exec.WorkflowProcess; import com.adobe.granite.workflow.metadata.MetaDataMap; import org.apache.commons.lang3.StringUtils; import org.apache.sling.commons.json.JSONArray; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component( service = WorkflowProcess.class, property = { "process.label=Update Smart Crops Workflow Process Step" }) public class UpdateSmartCropsProcess implements WorkflowProcess { private static final Logger log = LoggerFactory.getLogger(UpdateSmartCropsProcess.class); @Reference private DMCService dmcService; public void execute(final WorkItem workItem, final WorkflowSession workflowSession, final MetaDataMap args) throws WorkflowException { String assetPath = getPayloadPath(workItem.getWorkflowData()); try{ MetaDataMap wfData = workItem.getWorkflow().getMetaDataMap(); log.info("Updating smart crops for asset : " + assetPath); String smartCropsToUpdateStr = wfData.get(DMCService.SMART_CROPS_JSON, ""); if(StringUtils.isEmpty(smartCropsToUpdateStr)){ return; } log.info("Smart crops to update " + smartCropsToUpdateStr); dmcService.updateSmartCropsInS7(assetPath, new JSONArray(smartCropsToUpdateStr)); }catch(Exception e){ log.error("Error occured while updating crops for payload - " + assetPath, e); } } private String getPayloadPath(WorkflowData wfData) { String payloadPath = null; if (wfData.getPayloadType().equals("JCR_PATH")) { payloadPath = (String)wfData.getPayload(); } return payloadPath; } }
5) Add a helper service class apps.experienceaem.assets.core.services.impl.DMCServiceImpl for the Scene7 API...
package apps.experienceaem.assets.core.services.impl; import apps.experienceaem.assets.core.services.DMCService; import com.adobe.granite.crypto.CryptoSupport; import com.adobe.granite.license.ProductInfo; import com.adobe.granite.license.ProductInfoService; import com.day.cq.dam.api.Asset; import com.day.cq.dam.scene7.api.*; import com.scene7.ipsapi.*; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.config.SocketConfig; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.sling.api.resource.*; import org.apache.sling.commons.json.JSONArray; import org.apache.sling.commons.json.JSONObject; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Modified; import org.osgi.service.component.annotations.Reference; import org.osgi.service.metatype.annotations.AttributeDefinition; import org.osgi.service.metatype.annotations.AttributeType; import org.osgi.service.metatype.annotations.Designate; import org.osgi.service.metatype.annotations.ObjectClassDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.*; import java.io.ByteArrayInputStream; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; @Component(service = DMCService.class) public class DMCServiceImpl implements DMCService { private static final Logger log = LoggerFactory.getLogger(DMCServiceImpl.class); private static final String EAEM_SERVICE_USER = "eaem-service-user"; @Reference private ResourceResolverFactory resourceResolverFactory; @Reference private Scene7Service scene7Service; @Reference private S7ConfigResolver s7ConfigResolver; @Reference private Scene7APIClient scene7APIClient; @Reference private ProductInfoService productInfoService; @Reference private CryptoSupport cryptoSupport; @Reference private Scene7EndpointsManager scene7EndpointsManager; public Map<String, String> getSmartCropsSubAssetHandles(S7Config s7Config, String assetHandle){ Map<String, String> subAssetHandles = new HashMap<String, String>(); try{ GetAssociatedAssetsParam getAssociatedAssetsParam = new GetAssociatedAssetsParam(); getAssociatedAssetsParam.setCompanyHandle(s7Config.getCompanyHandle()); getAssociatedAssetsParam.setAssetHandle(assetHandle); String responseBody = makeS7Request(s7Config, getAssociatedAssetsParam); subAssetHandles = parseResponseForSubAssetHandles(responseBody.getBytes()); }catch(Exception e){ log.error("Error getting smart crop handles for : " + assetHandle, e); } return subAssetHandles; } public static ResourceResolver getServiceResourceResolver(ResourceResolverFactory resourceResolverFactory) { Map<String, Object> subServiceUser = new HashMap<>(); subServiceUser.put(ResourceResolverFactory.SUBSERVICE, EAEM_SERVICE_USER); try { return resourceResolverFactory.getServiceResourceResolver(subServiceUser); } catch (LoginException ex) { log.error("Could not login as SubService user {}, exiting SearchService service.", "eaem-service-user", ex);
return null; } } public void updateSmartCropsInS7(String assetPath, JSONArray cropsToUpdate){ final ResourceResolver s7ConfigResourceResolver = getServiceResourceResolver(resourceResolverFactory); S7Config s7Config = s7ConfigResolver.getS7ConfigForAssetPath(s7ConfigResourceResolver, assetPath); if (s7Config == null) { s7Config = s7ConfigResolver.getDefaultS7Config(s7ConfigResourceResolver); } if((cropsToUpdate == null) || (cropsToUpdate.length() == 0)){ log.info("No crops to update for asset : " + assetPath); return; } try{ JSONObject smartCrop = cropsToUpdate.getJSONObject(0); String id = null, ownerHandle, subAssetHandle; id = smartCrop.getString("id"); ownerHandle = id.substring(0, id.lastIndexOf("__")).replace("_", "|"); Map<String, String> subAssetHandles = getSmartCropsSubAssetHandles(s7Config, ownerHandle); log.debug("subAssetHandles - " + subAssetHandles); UpdateSmartCropsParam updateSmartCropsParam = new UpdateSmartCropsParam(); updateSmartCropsParam.setCompanyHandle(s7Config.getCompanyHandle()); SmartCropUpdateArray updateArray = new SmartCropUpdateArray(); SmartCropUpdate smartCropUpdate = null; NormalizedCropRect cropRect = null; double leftN, topN; for(int i = 0; i < cropsToUpdate.length(); i++){ smartCrop = cropsToUpdate.getJSONObject(i); smartCropUpdate = new SmartCropUpdate(); id = smartCrop.getString("id"); ownerHandle = id.substring(0, id.lastIndexOf("__")).replace("_", "|"); subAssetHandle = subAssetHandles.get(smartCrop.getString("name")); log.debug("subAssetHandle - " + subAssetHandle + ", for name : " + smartCrop.getString("name")); if(StringUtils.isEmpty(subAssetHandle)){ continue; } smartCropUpdate.setOwnerHandle(ownerHandle); smartCropUpdate.setSubAssetHandle(subAssetHandle); cropRect = new NormalizedCropRect(); leftN = Double.parseDouble(smartCrop.getString("leftN")) / 100; topN = Double.parseDouble(smartCrop.getString("topN")) / 100; cropRect.setLeftN(leftN); cropRect.setTopN(topN); cropRect.setWidthN(1 - (Double.parseDouble(smartCrop.getString("rightN")) / 100) - leftN); cropRect.setHeightN(1 - (Double.parseDouble(smartCrop.getString("bottomN")) / 100) - topN); smartCropUpdate.setCropRect(cropRect); updateArray.getItems().add(smartCropUpdate); } updateSmartCropsParam.setUpdateArray(updateArray); makeS7Request(s7Config, updateSmartCropsParam); }catch(Exception e){ log.error("Error updating smart crops for : " + assetPath, e); } } private String makeS7Request(S7Config s7Config, Object param) throws Exception{ AuthHeader authHeader = getS7AuthHeader(s7Config); Marshaller marshaller = getMarshaller(AuthHeader.class); StringWriter sw = new StringWriter(); marshaller.marshal(authHeader, sw); String authHeaderStr = sw.toString(); marshaller = getMarshaller(param.getClass()); sw = new StringWriter(); marshaller.marshal(param, sw); String apiMethod = sw.toString(); StringBuilder requestBody = new StringBuilder("<Request xmlns=\"http://www.scene7.com/IpsApi/xsd/2017-10-29-beta\">"); requestBody.append(authHeaderStr).append(apiMethod).append("</Request>"); String uri = scene7EndpointsManager.getAPIServer(s7Config.getRegion()).toString() + "/scene7/api/IpsApiService"; CloseableHttpClient client = null; String responseBody = ""; SocketConfig sc = SocketConfig.custom().setSoTimeout(180000).build(); client = HttpClients.custom().setDefaultSocketConfig(sc).build(); HttpPost post = new HttpPost(uri); StringEntity entity = new StringEntity(requestBody.toString(), "UTF-8"); post.addHeader("Content-Type", "text/xml"); post.setEntity(entity); HttpResponse response = client.execute(post); HttpEntity responseEntity = response.getEntity(); responseBody = IOUtils.toString(responseEntity.getContent(), "UTF-8"); log.info("Scene7 response - " + responseBody); return responseBody; } private static Map<String, String> parseResponseForSubAssetHandles(byte[] responseBody) throws Exception{ Map<String, String> subAssetHandles = new HashMap<String, String>(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); ByteArrayInputStream input = new ByteArrayInputStream(responseBody); Document doc = builder.parse(input); XPath xPath = XPathFactory.newInstance().newXPath(); String expression = "/getAssociatedAssetsReturn/subAssetArray/items"; NodeList itemList = (NodeList) xPath.compile(expression).evaluate(doc, XPathConstants.NODESET); String subAssetHandle = null, name; for (int i = 0; i < itemList.getLength(); i++) { Node item = itemList.item(i); if(item.getNodeType() == Node.ELEMENT_NODE) { Element eElement = (Element) item; subAssetHandle = eElement.getElementsByTagName("subAssetHandle").item(0).getTextContent(); name = eElement.getElementsByTagName("name").item(0).getTextContent(); subAssetHandles.put(name, subAssetHandle); } } return subAssetHandles; } private AuthHeader getS7AuthHeader(S7Config s7Config) throws Exception{ ProductInfo[] prodInfo = productInfoService.getInfos(); String password = cryptoSupport.unprotect(s7Config.getPassword()); AuthHeader authHeader = new AuthHeader(); authHeader.setUser(s7Config.getEmail()); authHeader.setPassword(password); authHeader.setAppName(prodInfo[0].getName()); authHeader.setAppVersion(prodInfo[0].getVersion().toString()); authHeader.setFaultHttpStatusCode(200); return authHeader; } private Marshaller getMarshaller(Class apiMethodClass) throws JAXBException { Marshaller marshaller = JAXBContext.newInstance(new Class[]{apiMethodClass}).createMarshaller(); marshaller.setProperty("jaxb.formatted.output", Boolean.valueOf(true)); marshaller.setProperty("jaxb.fragment", Boolean.valueOf(true)); return marshaller; } }
6) Create the workflow /conf/global/settings/workflow/models/experience-aem-update-smart-crops
7) Add a service user eaem-service-user in repo init script ui.config\src\main\content\jcr_root\apps\eaem-cs-process-new-smart-crops\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config
scripts=[ " create service user eaem-service-user with path system/cq:services/experience-aem set principal ACL for eaem-service-user allow jcr:read on /apps allow jcr:read on /conf allow jcr:read on /content/dam end " ]
8) Provide the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-cs-process-new-smart-crops\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-ea.xml
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="sling:OsgiConfig" user.mapping="[eaem-cs-process-new-smart-crops.core:eaem-service-user=[eaem-service-user]]"/>
9) For adding the button Process New Smart Crops in Asset details action bar, create a clientlib /apps/eaem-cs-process-new-smart-crops/clientlibs/update-smart-crops/clientlib with categories dam.gui.actions.coral
10) Create the button configuration /apps/eaem-cs-process-new-smart-crops/clientlibs/update-smart-crops/content/process-smart-crops-but
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/collection/action" icon="refresh" target=".cq-damadmin-admin-childpages" text="Process New Smart Crops" variant="actionBar"/>
11) Add the button JS logic in /apps/eaem-cs-process-new-smart-crops/clientlibs/update-smart-crops/clientlib/process-smart-crops.js
(function ($, $document) { "use strict"; var ASSET_DETAILS_PAGE = "/assetdetails.html", initialized = false, REPROCESS_ACTIVATOR = "dam-asset-reprocessassets-action-activator", BESIDE_ACTIVATOR = "cq-damadmin-admin-actions-download-activator", PROCESS_NEW_SMART_CROPS_ACT_URL = "/bin/eaem/update-smart-crops?path=", PROCESS_NEW_SMART_CROPS_BUT_URL = "/apps/eaem-cs-process-new-smart-crops/clientlibs/update-smart-crops/content/process-smart-crops-but.html"; if (!isAssetDetailsPage()) { return; } $document.on("foundation-contentloaded", addActionBarButtons); function addActionBarButtons(){ if (initialized) { return; } initialized = true; if(!getAssetMimeType().startsWith("image/")){ return; } $.ajax(PROCESS_NEW_SMART_CROPS_BUT_URL).done(addProcessNewSmartCropsButton); } function getAssetMimeType(){ return $("#image-preview").data("assetMimetype") || ""; } function addProcessNewSmartCropsButton(html) { var $eActivator = $("." + REPROCESS_ACTIVATOR); if ($eActivator.length == 0) { $eActivator = $("." + BESIDE_ACTIVATOR); } var $smartCropProcessBut = $(html).insertAfter($eActivator); $smartCropProcessBut.find("coral-button-label").css("padding-left", "7px"); $smartCropProcessBut.click(updateSmartCrops); } function updateSmartCrops() { var assetUrl = window.location.pathname.substring(ASSET_DETAILS_PAGE.length); $.ajax({url: PROCESS_NEW_SMART_CROPS_ACT_URL + assetUrl}); showAlert("Processing new smart crops...", "Smart Crop", "Default"); } function showAlert(message, title, type, callback) { var fui = $(window).adaptTo("foundation-ui"), options = [{ id: "ok", text: "Ok", primary: true }]; message = message || "Unknown Error"; title = title || "Error"; type = type || "warning"; fui.prompt(title, message, type, options, callback); } function isAssetDetailsPage() { return (window.location.pathname.indexOf(ASSET_DETAILS_PAGE) >= 0); } }(jQuery, jQuery(document)));
No comments:
Post a Comment