Goal
AEM Cloud Version : 2021.3.5087.20210322T071003Z-210225 (March 22, 2021)
Add a button Smart Crop Download in Asset Details action bar to open/download smart crops for images and encodes for videos. Please stress test the logic for large video encode downloads....
Demo | Package Install | Github
Image Smart Crop Download
Video Encode Download
Solution
1) Add a service user eaem-service-user in repo init script ui.config\src\main\content\jcr_root\apps\eaem-cs-smart-crop-open\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:all on /conf end " ]
2) Provide the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-cs-smart-crop-open\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-smart-crop-open.core:eaem-service-user=[eaem-service-user]]"/>
3) Add a proxy servlet apps.experienceaem.assets.core.servlets.DynamicRenditionProxy to download the video encodes
package apps.experienceaem.assets.core.servlets; import apps.experienceaem.assets.core.services.EAEMDMService; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.fluent.Request; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.entity.ContentType; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.osgi.services.HttpClientBuilderFactory; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.api.servlets.SlingAllMethodsServlet; import org.apache.sling.commons.mime.MimeTypeService; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.Servlet; import javax.servlet.ServletException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @Component( name = "Experience AEM Dynamic Rendition Proxy Servlet", immediate = true, service = Servlet.class, property = { "sling.servlet.methods=GET", "sling.servlet.paths=/bin/eaem/proxy" }) public class DynamicRenditionProxy extends SlingAllMethodsServlet { private static final Logger log = LoggerFactory.getLogger(DynamicRenditionProxy.class); @Reference private transient HttpClientBuilderFactory httpClientBuilderFactory; private transient CloseableHttpClient httpClient; @Reference private transient EAEMDMService dmcService; @Reference private transient ResourceResolverFactory factory; @Reference private transient MimeTypeService mimeTypeService; protected void activate(final ComponentContext ctx) { final HttpClientBuilder builder = httpClientBuilderFactory.newBuilder(); final RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(30000).setSocketTimeout(30000) .build(); builder.setDefaultRequestConfig(requestConfig); httpClient = builder.build(); } @Override protected final void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response) throws ServletException, IOException { try { final String drUrl = request.getParameter("dr"); if (StringUtils.isEmpty(drUrl)) { response.getWriter().print(getAEMIPAddress()); return; } downloadImage(response, drUrl); } catch (final Exception e) { log.error("Could not get response", e); response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } private String getAEMIPAddress() throws Exception { return Request.Get("https://ifconfig.me/ip").execute().returnContent().asString(); } private void downloadImage(final SlingHttpServletResponse response, final String url) throws Exception { String fileName = url.substring(url.lastIndexOf("/") + 1); final String finalUrl = url.substring(0, url.lastIndexOf("/")) + "/" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()); fileName = fileName.replaceAll(":", "-"); log.info("Encoded URL: {}", finalUrl); final HttpGet get = new HttpGet(finalUrl); final CloseableHttpResponse s7Response = httpClient.execute(get); final String contentType = ContentType.get(s7Response.getEntity()).getMimeType(); fileName = fileName + "." + mimeTypeService.getExtension(contentType); response.setContentType("application/octet-stream"); response.setHeader("Content-disposition", "attachment; filename=" + fileName); final InputStream in = s7Response.getEntity().getContent(); final OutputStream out = response.getOutputStream(); IOUtils.copy(in, out); out.close(); in.close(); } private void streamImage(final SlingHttpServletResponse response, final String url) throws Exception { response.setContentType("image/jpeg"); final byte[] image = Request.Get(url).execute().returnContent().asBytes(); final InputStream in = new ByteArrayInputStream(image); final OutputStream out = response.getOutputStream(); IOUtils.copy(in, out); out.close(); in.close(); } }
4) Add a service implementation apps.experienceaem.assets.core.services.impl.EAEMDMServiceImpl for executing the S7 API and get preview server / test context url...
package apps.experienceaem.assets.core.services.impl; import apps.experienceaem.assets.core.services.EAEMDMService; import com.day.cq.dam.scene7.api.*; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; 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.Node; import org.w3c.dom.NodeList; import javax.xml.xpath.*; import java.util.HashMap; import java.util.Map; @Component(service = EAEMDMService.class) @Designate(ocd = EAEMDMServiceImpl.DMServiceConfiguration .class) public class EAEMDMServiceImpl implements EAEMDMService { private static final Logger log = LoggerFactory.getLogger(EAEMDMServiceImpl.class); private static String APPLICATION_TEST_SERVER_CONTEXT = "application_test_server_context"; private static final String EAEM_SERVICE_USER = "eaem-service-user"; private String dmcTestContext; @Reference private ResourceResolverFactory resourceResolverFactory; @Reference private Scene7Service scene7Service; @Reference private S7ConfigResolver s7ConfigResolver; @Reference private Scene7APIClient scene7APIClient; @Activate @Modified protected void activate(final DMServiceConfiguration config) { dmcTestContext = config.dmc_test_context(); if (StringUtils.isNotEmpty(dmcTestContext)) { dmcTestContext = dmcTestContext.trim(); if (!dmcTestContext.endsWith("/")) { dmcTestContext = dmcTestContext + "/"; } } log.debug("DMC(S7) test context set in configuration - " + dmcTestContext); } @Override public String getS7TestContext(final String assetPath) { if (StringUtils.isNotEmpty(dmcTestContext)) { log.info("DMC(S7) test context - " + dmcTestContext); return dmcTestContext; } String testContext = ""; try { final ResourceResolver s7ConfigResourceResolver = getServiceResourceResolver(); if (s7ConfigResourceResolver == null) { return testContext; } S7Config s7Config = s7ConfigResolver.getS7ConfigForAssetPath(s7ConfigResourceResolver, assetPath); if (s7Config == null) { s7Config = s7ConfigResolver.getDefaultS7Config(s7ConfigResourceResolver); } final String appSettingsTypeHandle = scene7Service.getApplicationPropertyHandle(s7Config); final Document document = scene7APIClient.getPropertySets(appSettingsTypeHandle, s7Config); testContext = getPropertyValue(document, APPLICATION_TEST_SERVER_CONTEXT); if(StringUtils.isEmpty(testContext)){ testContext = "https://preview1.assetsadobe.com/"; } if (!testContext.endsWith("/")) { testContext = testContext + "/"; } log.info("DMC(S7) test context read using api - " + testContext); dmcTestContext = testContext; } catch (final XPathExpressionException e) { log.error("Error getting S7 test context ", e); } return testContext; } public String getS7TestContextUrl(final String assetPath, final String deliveryUrl) { String testContextUrl = ""; if (StringUtils.isEmpty(deliveryUrl)) { return testContextUrl; } String imageServerPath = ""; imageServerPath = deliveryUrl.substring(deliveryUrl.indexOf("/is/image") + 1); testContextUrl = getS7TestContext(assetPath) + imageServerPath; testContextUrl = testContextUrl.replace("http://", "https://"); log.debug("Rendition test context url - " + testContextUrl); return testContextUrl; } private String getPropertyValue(final Document document, final String name) throws XPathExpressionException { final XPath xpath = XPathFactory.newInstance().newXPath(); String value = ""; final String expression = getLocalName("getPropertySetsReturn") + getLocalName("setArray") + getLocalName("items") + getLocalName("propertyArray") + getLocalName("items"); final XPathExpression xpathExpr = xpath.compile(expression); final NodeList nodeList = (NodeList) xpathExpr.evaluate(document, XPathConstants.NODESET); Node nameNode, valueNode; for (int i = 0; i < nodeList.getLength(); i++) { nameNode = nodeList.item(i).getFirstChild(); if (!nameNode.getTextContent().equals(name)) { continue; } valueNode = nodeList.item(i).getLastChild(); value = valueNode.getTextContent(); break; } return value; } private String getLocalName(final String name) { return "/*[local-name()='" + name + "']"; } public ResourceResolver getServiceResourceResolver() { Map<String, Object> subServiceUser = new HashMap<>(); subServiceUser.put(ResourceResolverFactory.SUBSERVICE, EAEM_SERVICE_USER); try { return resourceResolverFactory.getServiceResourceResolver(subServiceUser); } catch (Exception ex) { log.error("Could not login as SubService user {}, exiting SearchService service.", "eaem-service-user", ex); return null; } } @ObjectClassDefinition(name = "Experience AEM Dynamic Media Configuration") public @interface DMServiceConfiguration { @AttributeDefinition( name = "DMC (S7) test context", description = "Set DMC (S7) test context (and not read it using API)", type = AttributeType.STRING) String dmc_test_context(); } }
5) To get the image smart crops as JSON, add script /apps/eaem-cs-smart-crop-open/extensions/image-smart-crops/image-smart-crops.jsp
<%@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.Config, com.adobe.granite.ui.components.Tag"%> <%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %> <%@ page import="com.adobe.granite.ui.components.ds.DataSource" %> <%@ page import="org.apache.sling.commons.json.JSONArray" %> <%@ page import="apps.experienceaem.assets.core.services.EAEMDMService" %> <% Config cfg = cmp.getConfig(); ValueMap dynVM = null; JSONObject dynRenditions = new JSONObject(); Resource dynResource = null; EAEMDMService dmcService = sling.getService(EAEMDMService.class); response.setContentType("application/json"); String name = "Original"; JSONObject dynRendition = new JSONObject(); dynRendition.put("type", "IMAGE"); dynRendition.put("name", name); dynRenditions.put(name, dynRendition); 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();) { dynRendition = new JSONObject(); dynResource = items.next(); dynVM = dynResource.getValueMap(); name = String.valueOf(dynVM.get("breakpoint-name")); String testContextUrl = dmcService.getS7TestContextUrl(dynResource.getPath(), (String)dynVM.get("copyurl")); dynRendition.put("type", "IMAGE"); dynRendition.put("name", name); dynRendition.put("s7Url", testContextUrl); 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; } %>
6) Set the datasource for Image Smart Crops /apps/eaem-cs-smart-crop-open/extensions/image-smart-crops/renditions/datasource@sling:resourceType = dam/gui/components/s7dam/smartcrop/datasource
7) To get the video encodes as JSON, add script /apps/eaem-cs-smart-crop-open/extensions/video-encodes/video-encodes.jsp
<%@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.Config, com.adobe.granite.ui.components.Tag"%> <%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %> <%@ page import="org.apache.sling.api.SlingHttpServletRequest" %> <%@ page import="com.day.cq.dam.api.Asset" %> <%@ page import="com.day.cq.dam.api.renditions.DynamicMediaRenditionProvider" %> <%@ page import="com.day.cq.dam.api.Rendition" %> <%@ page import="java.util.HashMap" %> <%@ page import="java.util.List" %> <%@ page import="apps.experienceaem.assets.core.services.EAEMDMService" %> <% response.setContentType("application/json"); SlingHttpServletRequest eaemSlingRequest = slingRequest; String assetPath = eaemSlingRequest.getRequestPathInfo().getSuffix(); Resource currentResource = eaemSlingRequest.getResourceResolver().getResource(assetPath); Asset asset = (currentResource != null ? currentResource.adaptTo(Asset.class) : null); EAEMDMService dmcService = sling.getService(EAEMDMService.class); String s7Domain = dmcService.getS7TestContext(asset.getPath()); s7Domain = s7Domain.replace("http://", "https://"); JSONObject dynRenditions = new JSONObject(); if( (asset == null) || !(asset.getMimeType().startsWith("video/"))) { dynRenditions.write(response.getWriter()); return; } DynamicMediaRenditionProvider dmRendProvider = sling.getService(DynamicMediaRenditionProvider.class); HashMap<String, Object> rules = new HashMap<>(); rules.put("remote", true); rules.put("video", true); JSONObject dynRendition = new JSONObject(); String image = null; String s7EncodeUrl = null; List<Rendition> dmRenditions = dmRendProvider.getRenditions(asset, rules); for (Rendition dmRendition : dmRenditions) { dynRendition = new JSONObject(); image = dmRendition.getPath(); image = image.substring(0, image.lastIndexOf(".")); s7EncodeUrl = getPreviewUrl(s7Domain, dmRendition.getPath()); dynRendition.put("type", "VIDEO"); dynRendition.put("name", dmRendition.getName()); dynRendition.put("image", getRendThumbnail(s7Domain, image)); dynRendition.put("s7Url", s7EncodeUrl); dynRenditions.put(dmRendition.getName(), dynRendition); } dynRenditions.write(response.getWriter()); %> <%! private static String getScene7Url(String s7Domain, String rendPath){ return s7Domain + "/s7viewers/html5/VideoViewer.html?asset=" + rendPath; } private static String getPreviewUrl(String s7Domain, String rendPath){ if(rendPath.contains(".")){ rendPath = rendPath.substring(0, rendPath.lastIndexOf(".")); } return s7Domain + "is/content/" + rendPath; } private static String getRendThumbnail(String s7Domain, String rendPath){ return s7Domain + "is/image/" + rendPath + "?fit=constrain,1&wid=200&hei=200"; } %>
8) Set the datasource for video encodes /apps/eaem-cs-smart-crop-open/extensions/video-encodes/renditions/datasource@sling:resourceType = dam/gui/components/s7dam/smartcrop/datasource
9) Add the action bar button Smart Crop Download configuration in /apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/content/smart-crop-url-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="link" target=".cq-damadmin-admin-childpages" text="Smart Crop Download" variant="actionBar"/>
10) Add a client library /apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/clientlib with categories=dam.gui.actions.coral and the following logic in /apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/clientlib/get-smart-crop-link.js to add the button to action bar, read crops and show them in a modal window...
(function ($, $document) { "use strict"; var ASSET_DETAILS_PAGE = "/assetdetails.html", initialized = false, RENDITION_ACTIVE = ".rendition-active", IMAGE_SMART_CROPS_URL = "/apps/eaem-cs-smart-crop-open/extensions/image-smart-crops/renditions.html", VIDEO_ENCODES_URL = "/apps/eaem-cs-smart-crop-open/extensions/video-encodes/renditions.html", BESIDE_ACTIVATOR = "cq-damadmin-admin-actions-download-activator", PROXY_SERLVET = "/bin/eaem/proxy?dr=", SMART_CROP_BUTTON_URL = "/apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/content/smart-crop-url-but.html"; if (!isAssetDetailsPage()) { return; } $document.on("foundation-contentloaded", addActionBarButtons); function addActionBarButtons(){ if (initialized) { return; } initialized = true; $.ajax(SMART_CROP_BUTTON_URL).done(addSmartCropUrlButton); } function addSmartCropUrlButton(html) { var $eActivator = $("." + BESIDE_ACTIVATOR); if ($eActivator.length == 0) { return; } var $smartCropBUt = $(html).insertAfter($eActivator); $smartCropBUt.find("coral-button-label").css("padding-left", "7px"); $smartCropBUt.click(showSmartCropUrl); } function showSmartCropUrl() { var $activeRendition = $(RENDITION_ACTIVE); if (_.isEmpty($activeRendition)) { showAlert("Rendition not selected...", "Error"); return; } var title = $activeRendition.attr("title"), assetUrl = window.location.pathname.substring(ASSET_DETAILS_PAGE.length), assetMimeType = $(RENDITION_ACTIVE).attr("data-type"), url = IMAGE_SMART_CROPS_URL; if (assetMimeType && assetMimeType.toLowerCase().startsWith("video")) { url = VIDEO_ENCODES_URL; } else { title = $activeRendition.find(".name").last().html(); } return $.ajax({url: url + assetUrl}).done(function (data) { var drUrl = data[title]; if (!drUrl) { showAlert("Dynamic rendition url not available", "Error"); return; } var fui = $(window).adaptTo("foundation-ui"), options = [{ id: "DOWNLOAD", text: "Download" }, { id: "OPEN_TAB", text: "Open" }, { id: "ok", text: "Ok", primary: true }]; fui.prompt("Rendition Url", drUrl["s7Url"], "default", options, function (actionId) { if (actionId === "OPEN_TAB") { window.open(drUrl["s7Url"], '_blank'); }else if (actionId === "DOWNLOAD") { var downloadUrl = PROXY_SERLVET + drUrl["s7Url"]; window.open(downloadUrl, '_blank'); } }); }); } 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