/*
* (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Thibaud Arguillere
*/
package org.nuxeo.labs.images;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.thumbnail.ThumbnailService;
import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters;
import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters.ParameterValue;
import org.nuxeo.ecm.platform.commandline.executor.api.CommandLineExecutorService;
import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
import org.nuxeo.ecm.platform.commandline.executor.api.ExecResult;
import org.nuxeo.ecm.platform.picture.api.PictureView;
import org.nuxeo.ecm.platform.picture.api.adapters.MultiviewPicture;
import org.nuxeo.runtime.api.Framework;
import com.google.common.io.Files;
/**
* Use ImageMagick <code>montage</code> to build a sheet from the images, with misc. parameters.
* <p>
* please refer to ImageMagick documentation for parameters meaning and formats.
* <p>
* The following may require more explanation:
* <ul>
* <li>label: A pattern. %f for the file name, %w for the width, ... For example, to draw the title and the size below:
* "%f\n#wx#h"<br/>
* Default value is "%f" (see "useDocTitle" below)</li>
* <li>define: ImageMagick keeps all images in cache. if the list has a lot of big images, it will fails. This parameter
* allows to reduce the size of the image in memory.<br>
* Default value is "jpeg:size=150x150"</li>
* <li>geometry: The size of each thumb, expressed as a String, regular ImageMagic geometry parameter. Default value is
* "150x150+20+20" which means a thumb is a rectangle of 150 pixel, and there is a margin of 20 pixels</li>
* <li></li>
* </ul>
* <p>
* The <code>view</code> is the <Code>PictureView</code> t use for building the final thumbnail. We use "Medium" by
* default, but if the bview is not found (removed after configuration for example), the plug-in uses the original
* "file:content" binary.
* <p>
* it is also possible to contribute your own command-line and use this class to run it. You can even declare/pass more
* parameters. There are 2 parameters that are required by the plug-in and must be used exactly as the following:
* <ul>
* <li>They must be the last two parameters</li>
* <li>They must exactly be: <code> @#{listFilePath} #{targetFilePath}</code> (plesae notice the @ sign)</li>
* </ul>
*
* @since 8.2
*/
public class ImagesSheetBuilder {
private static final Log log = LogFactory.getLog(ImagesSheetBuilder.class);
public static final String DEFAULT_COMMAND = "IM-montage";
public static final String DEFAULT_LABEL = "%f";
public static final String NO_LABEL = "NO_LABEL";
// Format is "geometry". nbPerRowsxnbPerColumns
// To just have 4 per rows, pass "4"
public static final String DEFAULT_TILE = "0";
public static final String DEFAULT_FONT = "Helvetica";
public static final int DEFAULT_FONT_SIZE = 12;
public static final String DEFAULT_BACKGROUND = "white";
public static final String DEFAULT_FILL = "black";
public static final String DEFAULT_DEFINE = "jpeg:size=150x150>";
public static final String DEFAULT_GEOMETRY = "150x150>+20+20";
public static final String RESULT_FILE_NAME = "thumbnails-sheet.jpg";
public static final String RESULT_MIMETYPE = "image/jpeg";
public static final String DEFAULT_VIEW = "Medium";
public static final boolean DEFAULT_USE_DOC_TITLE = false;
protected String label = DEFAULT_LABEL;
protected String font = DEFAULT_FONT;
protected int fontSize = DEFAULT_FONT_SIZE;
protected String background = DEFAULT_BACKGROUND;
protected String fill = DEFAULT_FILL;
protected String define = DEFAULT_DEFINE;
protected String geometry = DEFAULT_GEOMETRY;
protected String view = DEFAULT_VIEW;
protected boolean useDocTitle = DEFAULT_USE_DOC_TITLE;
protected String tile = DEFAULT_TILE;
protected DocumentModelList docs;
protected String command = DEFAULT_COMMAND;
protected static ThumbnailService thumbnailService = null;
public ImagesSheetBuilder(DocumentModelList inDocs) {
docs = inDocs;
// Not 100% threadsafe, but it's ok in this context
if (thumbnailService == null) {
thumbnailService = Framework.getService(ThumbnailService.class);
}
}
public Blob build() throws IOException, CommandNotAvailable, NuxeoException {
return build(null);
}
public Blob build(CmdParameters moreParameters) throws IOException, CommandNotAvailable, NuxeoException {
Blob result = null;
if (docs.size() < 1) {
return null;
}
// Duplicate the images in a temp folder
// and build a list of path, ordered as the doc list
File tempDir = Files.createTempDir();
File f;
Blob blob;
String fileList = "";
boolean useView = StringUtils.isNotBlank(view);
for (DocumentModel doc : docs) {
blob = null;
if (doc.hasFacet("Picture")) {
// Get the blob of the view or the whole content
if (useView) {
MultiviewPicture mvp = doc.getAdapter(MultiviewPicture.class);
if (mvp != null) {
PictureView pv = mvp.getView(view);
if (pv != null) {
blob = pv.getBlob();
}
}
}
if (blob == null) {
blob = (Blob) doc.getPropertyValue("file:content");
}
}
if (blob == null) {
blob = thumbnailService.getThumbnail(doc, doc.getCoreSession());
}
// Duplicate
if (blob != null) {
if (useDocTitle) {
f = new File(tempDir, doc.getTitle());
} else {
f = new File(tempDir, blob.getFilename());
}
fileList += "\"" + f.getAbsolutePath() + "\"\n";
blob.transferTo(f);
}
}
// Create the file to be used by ImageMagic to get the files
File listOfFiles = new File(tempDir, "list.txt");
FileUtils.writeStringToFile(listOfFiles, fileList);
// Call the command line
// * Create a temp blob, handled by Nuxeo
result = Blobs.createBlobWithExtension(".jpg");
String outputFilePath = result.getFile().getAbsolutePath();
// * Setup up the command line parameters (see "IM-montage" in conversions.xlml)
CmdParameters params = new CmdParameters();
if (label != null && label.equals(NO_LABEL)) {
label = "";
}
params.addNamedParameter("label", label);
params.addNamedParameter("font", font);
params.addNamedParameter("fontSize", "" + fontSize);
params.addNamedParameter("background", background);
params.addNamedParameter("fill", fill);
params.addNamedParameter("define", define);
params.addNamedParameter("geometry", geometry);
params.addNamedParameter("tile", tile);
params.addNamedParameter("listFilePath", listOfFiles);
params.addNamedParameter("targetFilePath", outputFilePath);
// * Add optional parameters (when used with another commandline)
// We accept only String as ParameterValue (not File or String List)
if (moreParameters != null) {
Map<String, ParameterValue> more = moreParameters.getParameters();
for (Entry<String, ParameterValue> entry : more.entrySet()) {
params.addNamedParameter(entry.getKey(), entry.getValue().getValue());
}
}
// * Run
CommandLineExecutorService cles = Framework.getService(CommandLineExecutorService.class);
ExecResult clResult = cles.execCommand(command, params);
// * Handle errors
// It may happen ImageMagick returns 1 while the operation went well. We can't rely on clResult
// So we test if we do have a result and handle an error if we don't
if (!result.getFile().exists() || result.getFile().length() == 0) {
result = null;
log.error("Failed to build the Images Sheet: \n" + "Command Line: " + clResult.getCommandLine() + "\n"
+ "Result code: " + clResult.getReturnCode() + "\n" + "Error: " + clResult.getError());
throw new NuxeoException("Failed to build the Images Sheet");
} else {
result.setMimeType(RESULT_MIMETYPE);
result.setFilename(RESULT_FILE_NAME);
}
// Cleanup the temp folder now
FileUtils.deleteDirectory(tempDir);
return result;
}
public String getCommand() {
return command;
}
/**
* Set the contributed command line to use. If empty or null, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setCommand(String value) {
command = StringUtils.isBlank(value) ? DEFAULT_COMMAND : value;
return this;
}
public String getLabel() {
return label;
}
/**
* Set the label to use. If empty or null, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setLabel(String value) {
label = StringUtils.isBlank(value) ? DEFAULT_LABEL : value;
return this;
}
public String getFont() {
return font;
}
/**
* Set the font to use. If empty or null, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setFont(String value) {
font = StringUtils.isBlank(value) ? DEFAULT_FONT : value;
return this;
}
public int getFontSize() {
return fontSize;
}
/**
* Set the font size to use. If null or < 1, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setFontSize(int value) {
fontSize = value <= 0 ? DEFAULT_FONT_SIZE : value;
return this;
}
/**
* Set the font size to use. If null or < 1, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setFontSize(Long value) {
fontSize = value == null || value.intValue() <= 0 ? DEFAULT_FONT_SIZE : value.intValue();
return this;
}
public String getBackground() {
return background;
}
/**
* Set the background color to use. If empty or null, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setBackground(String value) {
background = StringUtils.isBlank(value) ? DEFAULT_BACKGROUND : value;
return this;
}
public String getFill() {
return fill;
}
/**
* Set the fill color to use. If empty or null, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setFill(String value) {
fill = StringUtils.isBlank(value) ? DEFAULT_FILL : value;
return this;
}
public String getDefine() {
return define;
}
/**
* Set the "define" to use. If empty or null, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setDefine(String value) {
define = StringUtils.isBlank(value) ? DEFAULT_DEFINE : value;
return this;
}
public String getGeometry() {
return geometry;
}
/**
* Set the geometry of each thumb. If empty or null, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setGeometry(String value) {
geometry = StringUtils.isBlank(value) ? DEFAULT_GEOMETRY : value;
return this;
}
public String getTile() {
return tile;
}
/**
* Set the tile (nb colums) to use. If empty or null, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setTile(String value) {
tile = StringUtils.isBlank(value) ? DEFAULT_TILE : value;
return this;
}
public String getView() {
return view;
}
/**
* Set the picture view to use. If empty or null, use the default value.
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setView(String value) {
view = StringUtils.isBlank(value) ? DEFAULT_VIEW : value;
return this;
}
public boolean useDocTitle() {
return this.useDocTitle;
}
/**
* Tell the builder to use the Document titles instead of the file names
*
* @param value
* @return the <code>this</object>
* @since 8.2
*/
public ImagesSheetBuilder setUseDocTitle(boolean value) {
useDocTitle = value;
return this;
}
}