Downloading a custom export of an asset in a different form than its original one is a common request from our users. Using a bit of Nuxeo Studio and the converter extension point, you'll see that it's pretty easy to achieve.

As an example let's say we want to watermark a picture asset with some text. When you add a new button to Nuxeo, you have to define when it will be displayed and what it will do.

First let's see about the when. Using Nuxeo Studio, you need to create a new User Action through the Automation > User Actions menu. Here you can select a category. It's where your button will be displayed. If you select "DAM Asset view actions" it will display the button on the DAM tab along with the actions available on the single asset view on the right.

New User Action

Now as the goal is to watermark pictures, we don't want to display the button when something other than a picture is selected. To do that you simply select the Picture type in "Action Enablement > Current document has one of the types" menu.
Action Filter
Then, if you hit the save button, Studio will warn you that there is no automation chain associated to the current button. So click on create, pick a name and Studio will automatically send you to the new automation chain screen. Here we can define what we want to do with our Picture asset.

What we'll need now is an operation that can actually do the watermarking. This is not available by default, but we can add it easily with Nuxeo IDE. Use the new operation wizard to start a brand new operation. Let's call it GenericConverter. It will take as parameter a converter name and a map of parameters. Take a look at the code:

package org.nuxeo.presales.toolkit.images;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.automation.core.Constants;
import org.nuxeo.ecm.automation.core.annotations.Context;
import org.nuxeo.ecm.automation.core.annotations.Operation;
import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
import org.nuxeo.ecm.automation.core.annotations.Param;
import org.nuxeo.ecm.automation.core.collectors.BlobCollector;
import org.nuxeo.ecm.automation.core.util.Properties;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolder;
import org.nuxeo.ecm.core.convert.api.ConversionService;
import org.nuxeo.runtime.api.Framework;

/**

  • @author fvadon and aescaffre
    */
    @Operation(id = GenericConverter.ID, category = Constants.CAT_CONVERSION, label = "GenericConverter", description = "")
    public class GenericConverter {

    public static final Log log = LogFactory.getLog(GenericConverter.class);

    @Param(name = "converterName")
    protected String converterName;

    @Param(name = "parameters", required = false)
    protected Properties parameters;

    public static final String ID = "GenericConverter";

    @OperationMethod(collector = BlobCollector.class)
    public Blob run(Blob input) {

      try {
          ConversionService conversionService = Framework.getService(ConversionService.class);
          BlobHolder source = new SimpleBlobHolder(input);
          Map<String, Serializable> serializableParameters = new HashMap<String, Serializable>();
          if (parameters !=null) {
              Set<String> parameterNames = parameters.keySet();
              for (String parameterName : parameterNames) {
                  serializableParameters.put(parameterName, (Serializable) parameters.get(parameterName));
              }
          }
          log.debug("Converter Being called:"+converterName);
          BlobHolder result = conversionService.convert(converterName, source, serializableParameters);
          return result.getBlob();
      } catch (Exception e) {
          log.error("Error during conversion",e);
      }
      return null;
    

    }

}


As this operation can take any converter as parameter, we can create a new one that will do the watermarking. To achieve this, we can simply add some text as overlay on the existing image.

Adding a converter is easy. Most of them are usually based on a commandLine executor. So what we've done is create a generic converter called GenericImageMagickConverter that takes as a parameter the name of the commandLine executor to use. Here's the code to create the generic converter:

package org.nuxeo.presales.toolkit.images;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
import org.nuxeo.ecm.core.convert.api.ConversionException;
import org.nuxeo.ecm.platform.convert.plugins.PDF2ImageConverter;

public class GenericImageMagickConverter extends PDF2ImageConverter {
public static final Log log = LogFactory.getLog(GenericImageMagickConverter.class);
@Override
protected Map<String, String> getCmdStringParameters(BlobHolder blobHolder,
Map<String, Serializable> parameters) throws ConversionException {
Map<String, String> cmdStringParameters= super.getCmdStringParameters(blobHolder, parameters);
Map<String, String> stringParameters = new HashMap<String, String>();
Set<String> parameterNames = parameters.keySet();
for (String parameterName : parameterNames) {
//targetFilePath is computed by the method of the superType
if (!parameterName.equals("targetFilePath"))
stringParameters.put(parameterName, (String) parameters.get(parameterName));
}
cmdStringParameters.putAll(stringParameters);
return cmdStringParameters;
}

}


Most of the time when you add Java code in a Nuxeo bundle, it's tied to an extension point. Here, our generic converter is of course linked to the converter extension point:

<component name="org.nuxeo.presales.toolkit.images.conversions">
<extension target="org.nuxeo.ecm.core.convert.service.ConversionServiceImpl"
point="converter">
<converter name="overlaying" class="org.nuxeo.presales.toolkit.images.GenericImageMagickConverter">
<parameters>
<parameter name="CommandLineName">imageOverlay</parameter>
</parameters>
</converter>
</extension>
</component>

And the imageOverlay commandLine executor defined in the converter has to be defined in the right extension point too:

    <extension target="org.nuxeo.ecm.platform.commandline.executor.service.CommandLineExecutorComponent"
point="command">
<command name="imageOverlay" enabled="true">
<commandLine>convert</commandLine>
<parameterString>#{sourceFilePath} -gravity #{gravity} -fill #{textColor} -stroke '#070536' -strokewidth 2
-pointsize #{textSize} -annotate #{textRotation}x#{textRotation}+#{xOffset}+#{yOffset}

          #{textValue} "#{targetFilePath}"&lt;/parameterString&gt;
      &lt;installationDirective&gt;You need to install ImageMagick.&lt;/installationDirective&gt;
  &lt;/command&gt;
&lt;/extension&gt;


As you can see it's an ImageMagick command that will overlay some text on the given image. Okay, maybe you can't see it just by looking at the command line :) We're not all ImageMagick gurus...

Now that we have a converter, we can deploy this code using the Nuxeo IDE. We can also send the new operation definition to Nuxeo Studio using the export button available on the IDE. If you don't know how to do this I strongly recommend you read the quick start dev guide.

Once this part is done we can go back to our operation. What we're going to do first is set a context variable that will hold the text we want to overlay. Then we get the main file of the document and assign to the filename variable the filename of the main file. We're going to use it in the operation parameter.

The next step is to resize the current image to 800x600. Controlling the size of the image is important as our overlay operation will have predefined parameters like the size of the text and its position. We could probably do something proportional to the size of the image being converted but let's keep things simple.

The next step is to call the actual overlay operation. You can see in the given parameters that we are using the context variables we previously defined. If Bill created the image and downloads the export April 11, 2014, the resulting image will have a text overlay like Copyright: Bill-2014.

The final step is to use the Download file operation that will actually send the converted file back to the user.

Fetch > Context Document(s)
Execution Context > Set Context Variable
name:
textValue
value:
Copyright: @{Document["dc:creator"]}-@{CurrentDate.format("yyyy")}
Files > Get Document File
xpath:
file:content
Execution Context > Set Context Variable
name:
fileName
value:
@{This.filename}
Conversion > Resize a picture
maxHeight:
600
maxWidth:
800
Conversion > GenericConverter
converterName:
overlaying
parameters:
targetFilePath=@{fileName}
textColor=Yellow
textValue=@{textValue}
gravity=SouthWest
textRotation=0
xOffset=0
yOffset=0
textSize=42
User Interface > Download file

Now you are all set to try this watermarking operation. Keep in mind that using these converters you can add pretty much any kind of export you want. And if you want to make them more configurable, you can always make the parameters of the operation configurable through a workflow (which might be the subject of another blog post in the near future).