AEM Cloud Service SDK - Content Fragment with Styled RTE Text to RTF to InDesign to PDF


Adobe Experience Manager 2024.10.18311.20241017T104455Z-240900

When creating an InDesign doc (and later a pdf) out of AEM Content Fragments, InDesign uses Character Styles for Styling Text, so any Html tags in Content Fragments are printed as-is into a Textframe (with no styles like "bold" applied). This post attempts to solve the problem by converting the content fragments into RTF (Rich Text Format) and placing the RTF on InDesign doc page (using InDesign Server) and later exporting it to a PDF (preserving the Html styling). 

For converting the Html Tags to InDesign Character Styles check this post

Thank you Edouard Dorval for the code to convert Content Fragment to RTF...

Demo | Package Install | Github


Content Fragment with Styling


Run the Workflow


Generated RTF and PDF


PDF with Styled Text


InDesign Connector Configuration


1) Create a workflow process step apps.experienceaem.core.workflows.EAEMContentFragmentToRTF to initiate the conversion from Content Fragment to RTF. The created RTF is stored in same folder..

package apps.experienceaem.core.workflows;

import apps.experienceaem.core.cf2rtf.CFToRTF;
import apps.experienceaem.core.cf2rtf.CFToRTFAssetHelper;
import apps.experienceaem.core.cf2rtf.CFToRTFFragmentHelper;
import com.adobe.cq.dam.cfm.ContentFragment;
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.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import com.day.cq.dam.api.AssetManager;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import apps.experienceaem.core.cf2rtf.CFToRTFConfig;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@Component(
service = WorkflowProcess.class,
property = {
Constants.SERVICE_DESCRIPTION + "=Write Content Fragments to Repository as RTF Files",
EAEMContentFragmentToRTF.COMPONENT_LABEL + "=Experience AEM : Content Fragments to RTF Files"
}
)
public class EAEMContentFragmentToRTF implements WorkflowProcess{
public static final String COMPONENT_LABEL = "process.label";
private static final String HTML_MIME_TYPE = "text/html";

private static final Logger LOG = LoggerFactory.getLogger(EAEMContentFragmentToRTF.class);
private static final String LOG_PREFIX = "CFToRTF Workflow:";

public static final String CF_RTF_LOCATION = "rtfLocation";

private static final boolean EXPAND_GROUP_MEMBERS = false;

@Override
public void execute(WorkItem workItem,WorkflowSession workflowSession,MetaDataMap processArguments)
throws WorkflowException {
String payloadPath = getPayloadPath(workItem);
ResourceResolver resourceResolver = workflowSession.adaptTo(ResourceResolver.class);

if (resourceResolver == null) {
throw new WorkflowException("Could not adapt `WorkflowSession` to `ResourceResolver`!");
}

AssetManager assetManager = resourceResolver.adaptTo(AssetManager.class);

if (assetManager == null) {
throw new WorkflowException("Could not adapt `ResourceResolver` to `AssetManager`!");
}

Resource fragmentResource = resourceResolver.getResource(payloadPath);
String configurationSource = getConfigurationSource(processArguments);
CFToRTFConfig config = CFToRTFConfig.parse(configurationSource);

if (fragmentResource != null) {
ContentFragment cf = fragmentResource.adaptTo(ContentFragment.class);

if (cf != null){
StringBuilder html = CFToRTFFragmentHelper.getFragmentHtmlContents(cf);

LOG.debug("{} Extracted HTML from CF `{}`: `{}`", LOG_PREFIX, payloadPath, configurationSource);

String rtf = CFToRTF.transpile(html.toString(), config);
String rtfLocation = CFToRTFAssetHelper.createRTFAsset(rtf, fragmentResource, assetManager, config);

workItem.getWorkflowData().getMetaDataMap().put(CF_RTF_LOCATION, rtfLocation);

LOG.info("{} Successfully created RTF file `{}` from CF `{}`", LOG_PREFIX, rtfLocation, payloadPath);
}else{
LOG.error("{} The resource {} could not converted to a `com.adobe.cq.dam.cfm.ContentFragment` instance!",LOG_PREFIX,payloadPath);
}
}else{
LOG.error("{} The content fragment resource `{}` was not found!", LOG_PREFIX, payloadPath);
}

completeWorkflow(workItem, workflowSession);
}

private String getPayloadPath(WorkItem workItem){
return workItem.getWorkflowData().getPayload().toString();
}

private String getConfigurationSource(MetaDataMap processArguments) {
return String.valueOf(processArguments.get("PROCESS_ARGS", "string"));
}

private void completeWorkflow(WorkItem workItem, WorkflowSession workflowSession)
throws WorkflowException {
workflowSession.complete(workItem, workflowSession.getRoutes(workItem, EXPAND_GROUP_MEMBERS).get(0));
}
}


2) Code for converting the Html Styled text to RTF is available in https://github.com/schoudry/eaem-extensions/tree/master/eaem-cloud-service/eaem-cf-html-rtf-indesign/core/src/main/java/apps/experienceaem/core/cf2rtf

3) Create a workflow process step apps.experienceaem.core.workflows.EAEMPostRequestToIDS to send the RTF to InDesign server

package apps.experienceaem.core.workflows;

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.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import com.google.gson.JsonObject;
import org.apache.commons.io.IOUtils;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.JobManager;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

@Component(
service = WorkflowProcess.class,
property = {
Constants.SERVICE_DESCRIPTION + "=Send the RTF File to Indesign Server",
EAEMContentFragmentToRTF.COMPONENT_LABEL + "=Experience AEM : RTF File to InDesign Server"
}
)
public class EAEMPostRequestToIDS implements WorkflowProcess {

private static final Logger LOG = LoggerFactory.getLogger(EAEMContentFragmentToRTF.class);

public static final String INDESIGN_SERVER_TOPIC = "com/eaem/ids";
public static final String CONTENT_JSON = "contentJson";

public static final String INDESIGN_SERVICE_USER = "eaem-ids-service";
public static final Map<String, Object> INDESIGN_AUTH_INFO = Collections.singletonMap("sling.service.subservice", INDESIGN_SERVICE_USER);

@Reference
private JobManager jobManager;

@Override
public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap processArguments)
throws WorkflowException {
String rtfLocation = (String)workItem.getWorkflowData().getMetaDataMap().get(EAEMContentFragmentToRTF.CF_RTF_LOCATION);
ResourceResolver resourceResolver = workflowSession.adaptTo(ResourceResolver.class);

LOG.info("RTF File path passed from previous step {}", rtfLocation);

InputStream rtfStream = (InputStream)resourceResolver.getResource(rtfLocation).getChild("jcr:content/renditions/original/jcr:content").adaptTo(ValueMap.class).get("jcr:data");
JsonObject contentMap = new JsonObject();
contentMap.addProperty("path", rtfLocation);

try{
contentMap.addProperty("rtfText", IOUtils.toString(rtfStream, StandardCharsets.UTF_8));
}catch(Exception e){
LOG.error("Error reading rtf stream {}", rtfLocation, e);
}

HashMap<String, Object> jobProps = new HashMap<String, Object>();

jobProps.put(CONTENT_JSON, contentMap.toString());
jobManager.addJob(INDESIGN_SERVER_TOPIC, jobProps);
}
}


4) Code for the simple InDesign connector is available in https://github.com/schoudry/eaem-extensions/tree/master/eaem-cloud-service/eaem-cf-html-rtf-indesign/core/src/main/java/apps/experienceaem/core/ids

5) Create a workflow model eg. /conf/global/settings/workflow/models/experience-aem-cf-2-rtf.html with the 2 process steps above...


6) Provide the following json configuration for CF 2 RTF step in its  Process > Arguments

{
"fontName": "Calibri",
"docLineHeight": 1.2,
"docFontSize": 20,
"fontScaleX": 80,
"outputPath": "./${cfNodeName}.rtf",
"borderedParagraphClasses": {
"eaem-bordered-text": ""
},
"colorClasses": {
"eaem-legal-text": "#58158A",
"eaem-plugin-text": "#0562A5",
"eaem-variable-text": "#047736",
"eaem-source-text": "#980C30"
}
}


7) Add the InDesign script in /apps/eaem-cf-html-rtf-indesign/indesign/scripts/create-pdf-from-rtf.jsx for creating the InDesign doc, place RTF content and uploading generated PDF to AEM...

(function () {
var returnObj = {}, aemHost, base64EncodedAEMCreds, cfPath, contentJson;

function createInDesignDoc() {
cfPath = contentJson.path;

var sourceFolder = getSourceFolder(),
fileName = cfPath.substring(cfPath.lastIndexOf ('/') + 1, cfPath.lastIndexOf ('.rtf')),
documentFile = new File(sourceFolder.fullName + "/" + fileName + '.indd'),
pdfOutputFile = new File(sourceFolder.fullName + "/" + fileName + '.pdf');

var document = app.documents.add();
var spread = document.spreads.lastItem();
var textFrame = createTextFrame(spread);

var rtfPath = sourceFolder + "/" + new Date().getTime() + ".rtf";
var rtfFile = new File(rtfPath);

rtfFile.open('w');
rtfFile.encoding = 'UTF8';

rtfFile.write(contentJson["rtfText"]);
rtfFile.close();

textFrame.place(File(rtfPath));

document.exportFile(ExportFormat.pdfType, pdfOutputFile);
document.save(documentFile);
//document.close();

var uploadPath = cfPath.substring(0, cfPath.lastIndexOf("/"));

app.consoleout('Uploading file - ' + pdfOutputFile);

uploadDAMFile(aemHost, base64EncodedAEMCreds, pdfOutputFile, pdfOutputFile.name, 'application/pdf', uploadPath);

returnObj.success = "completed";
}

function createTextFrame(spread) {
var textFrame = spread.textFrames.add();

var y1 = 5; // upper left Y-Coordinate
var x1 = 5; // upper left X-Coordinate
var y2 = 50; // lower right Y-Coordinate
var x2 = 40; // lower right X-Coordinate

textFrame.geometricBounds = [y1, x1, y2, x2];

return textFrame;
}

function getSourceFolder(){
var today = new Date(),
folderName = today.getFullYear() + "-" + today.getMonth() + "-" + today.getDate() + "-" + today.getHours()
+ "-" + today.getMinutes() + "-" + today.getSeconds();

var sourceFolder = new Folder(folderName);
sourceFolder.create();

return sourceFolder;
}

function setParamsFromScriptArgs(){
if (app.scriptArgs.isDefined("base64EncodedAEMCreds")) {
base64EncodedAEMCreds = app.scriptArgs.getValue("base64EncodedAEMCreds");
} else {
throw "AEM host credentials argument is missing";
}

if (app.scriptArgs.isDefined("aemHost")) {
aemHost = app.scriptArgs.getValue("aemHost");
} else {
throw "aemHost argument is missing";
}

if (app.scriptArgs.isDefined("contentJson")) {
contentJson = JSON.parse(app.scriptArgs.getValue("contentJson"));
app.consoleout('contentJson: ' + app.scriptArgs.getValue("contentJson"));
} else {
throw "contentJson argument missing";
}

app.consoleout('base64EncodedAEMCreds --- ' + base64EncodedAEMCreds);
app.consoleout('aemHost --- ' + aemHost);
}

function uploadDAMFile(host, credentials, file, fileName, contentType, aemFolderPath) {
var transformedHost = transformHost(host);
host = transformedHost.host;
var contextPath = transformedHost.contextPath

var success = false;
var statusFromServer = 0;
var retries = 0;

while(!success && retries<5){
file.open ("r");
file.encoding = "BINARY";
var boundary = '----------V2ymHFg03ehbqgZCaKO6jy';
var connection = new Socket;
if (connection.open (host, "binary")) {
connection.write ("POST "+ encodeURI(contextPath + aemFolderPath + ".createasset.html") +" HTTP/1.0");
connection.write ("\n");
connection.write ("Authorization: Basic " + credentials);
connection.write ("\n");
connection.write ("User-Agent: Jakarta Commons-HttpClient/3.1");
connection.write ("\n");
connection.write ("Content-Type: multipart/form-data; boundary="+boundary);
connection.write ("\n");
var body = getMultiPartBodyDAMUpload(boundary, file, fileName, contentType);
connection.write ("Content-Length: "+body.length);
connection.write ("\r\n\r\n");
//END of header
connection.write (body);

statusFromServer = readResponse(connection);

if(statusFromServer>=400){
app.consoleout('Seen error response '+statusFromServer+' ['+retries+']');
} else if (statusFromServer>=300) {
app.consoleout('Redirects currently not supported');
} else if (statusFromServer>=200) {
success=true;
}

if(!success){
++retries;
}
} else {
app.consoleout('Connection to host ' + host + ' could not be opened');
}
file.close();
}
}

function getMultiPartBodyDAMUpload(boundary, file, fileName, contentType) {
var endBoundary = '\r\n--' + boundary + '--\r\n';
var body;

body = body + '--'+boundary+'\r\n';
body = body + 'content-disposition: form-data; name="_charset_"';
body = body + '\r\n\r\n';
body = body + 'utf-8';
body = body + '\r\n';

body = body + '--'+boundary+'\r\n';
body = body + 'content-disposition: form-data; name="fileName"';
body = body + '\r\n\r\n';
body = body + fileName;
body = body + '\r\n';

body = body + '--'+boundary+'\r\n';
body = body + 'content-disposition: form-data; name="file"; filename="'+Base64._utf8_encode(fileName)+'"\r\n';

if (contentType) {
body = body + 'Content-Type: '+contentType+'\r\n';
}
body = body + 'Content-Transfer-Encoding: binary\r\n';
body = body + '\r\n';

var content;
while ((content = file.read ()) != '') {
body = body + content;
}

file.close();

body = body + endBoundary;
return body;
}

function setTestParams(){
aemHost = "localhost:4502";
base64EncodedAEMCreds = "YWRtaW46YWRtaW4=";
contentJson = {"rtfText":"{}","path":"/content/dam/experience-aem/bold.rtf"}
}

try{
setParamsFromScriptArgs();

//setTestParams();

createInDesignDoc();
}catch(err){
returnObj.error = err;
app.consoleout("Error processing content fragment : " + cfPath + ", error : " + err);
}finally{
}

return JSON.stringify(returnObj);
}());


8) Create the osgi config file /apps/eaem-cf-html-rtf-indesign/osgiconfig/config/org.apache.sling.jcr.repoinit.RepositoryInitializer~eaem-cf-html-rtf-indesign.cfg.json for adding necessary service user...

{
"scripts": [
"create service user eaem-ids-service with path system/experience-aem",
"set ACL for eaem-ids-service \n allow jcr:read on /apps \n allow jcr:read on /conf \n allow jcr:read on /libs \n allow jcr:all on /content \n allow jcr:read on /var \nend",
]
}




No comments:

Post a Comment