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