/*
* (C) Copyright 2007-2013 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:
* Max Stepanov
* Florent Guillaume
*/
package org.nuxeo.ecm.platform.picture;
import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.CONVERSION_FORMAT;
import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.JPEG_CONVERSATION_FORMAT;
import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPERATION_RESIZE;
import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_DEPTH;
import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_HEIGHT;
import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_WIDTH;
import java.awt.Point;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.automation.AutomationService;
import org.nuxeo.ecm.automation.OperationContext;
import org.nuxeo.ecm.automation.OperationException;
import org.nuxeo.ecm.automation.core.util.Properties;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.core.api.CloseableFile;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolder;
import org.nuxeo.ecm.core.api.impl.blob.BlobWrapper;
import org.nuxeo.ecm.core.convert.api.ConversionService;
import org.nuxeo.ecm.platform.actions.ActionContext;
import org.nuxeo.ecm.platform.actions.ELActionContext;
import org.nuxeo.ecm.platform.actions.ejb.ActionManager;
import org.nuxeo.ecm.platform.commandline.executor.api.CommandException;
import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
import org.nuxeo.ecm.platform.mimetype.MimetypeDetectionException;
import org.nuxeo.ecm.platform.mimetype.MimetypeNotFoundException;
import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
import org.nuxeo.ecm.platform.picture.api.ImageInfo;
import org.nuxeo.ecm.platform.picture.api.ImagingConfigurationDescriptor;
import org.nuxeo.ecm.platform.picture.api.ImagingService;
import org.nuxeo.ecm.platform.picture.api.PictureConversion;
import org.nuxeo.ecm.platform.picture.api.PictureView;
import org.nuxeo.ecm.platform.picture.api.PictureViewImpl;
import org.nuxeo.ecm.platform.picture.core.libraryselector.LibrarySelector;
import org.nuxeo.ecm.platform.picture.magick.utils.ImageIdentifier;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.model.ComponentContext;
import org.nuxeo.runtime.model.ComponentInstance;
import org.nuxeo.runtime.model.DefaultComponent;
import org.nuxeo.runtime.transaction.TransactionHelper;
public class ImagingComponent extends DefaultComponent implements ImagingService {
private static final Log log = LogFactory.getLog(ImagingComponent.class);
public static final String CONFIGURATION_PARAMETERS_EP = "configuration";
public static final String PICTURE_CONVERSIONS_EP = "pictureConversions";
protected Map<String, String> configurationParameters = new HashMap<>();
protected PictureConversionRegistry pictureConversionRegistry = new PictureConversionRegistry();
private LibrarySelector librarySelector;
protected final PictureMigrationHandler pictureMigrationHandler = new PictureMigrationHandler();
@Override
public List<PictureConversion> getPictureConversions() {
return pictureConversionRegistry.getPictureConversions();
}
@Override
public PictureConversion getPictureConversion(String id) {
return pictureConversionRegistry.getPictureConversion(id);
}
@Override
public Blob crop(Blob blob, int x, int y, int width, int height) {
return getLibrarySelectorService().getImageUtils().crop(blob, x, y, width, height);
}
@Override
public Blob resize(Blob blob, String finalFormat, int width, int height, int depth) {
return getLibrarySelectorService().getImageUtils().resize(blob, finalFormat, width, height, depth);
}
@Override
public Blob rotate(Blob blob, int angle) {
return getLibrarySelectorService().getImageUtils().rotate(blob, angle);
}
@Override
public Blob convertToPDF(Blob blob) {
return getLibrarySelectorService().getImageUtils().convertToPDF(blob);
}
@Override
public Map<String, Object> getImageMetadata(Blob blob) {
log.warn("org.nuxeo.ecm.platform.picture.ImagingComponent.getImageMetadata is deprecated. Please use "
+ "org.nuxeo.binary.metadata.api.BinaryMetadataService#readMetadata(org.nuxeo.ecm.core.api.Blob)");
return Collections.emptyMap();
}
@Override
public String getImageMimeType(File file) {
try {
MimetypeRegistry mimetypeRegistry = Framework.getLocalService(MimetypeRegistry.class);
if (file.getName() != null) {
return mimetypeRegistry.getMimetypeFromFilenameAndBlobWithDefault(file.getName(),
Blobs.createBlob(file), "image/jpeg");
} else {
return mimetypeRegistry.getMimetypeFromFile(file);
}
} catch (MimetypeNotFoundException | MimetypeDetectionException | IOException e) {
log.error("Unable to retrieve mime type", e);
}
return null;
}
@Override
public String getImageMimeType(Blob blob) {
try {
MimetypeRegistry mimetypeRegistry = Framework.getLocalService(MimetypeRegistry.class);
if (blob.getFilename() != null) {
return mimetypeRegistry.getMimetypeFromFilenameAndBlobWithDefault(blob.getFilename(), blob,
"image/jpeg");
} else {
return mimetypeRegistry.getMimetypeFromBlob(blob);
}
} catch (MimetypeNotFoundException | MimetypeDetectionException e) {
log.error("Unable to retrieve mime type", e);
}
return null;
}
private LibrarySelector getLibrarySelectorService() {
if (librarySelector == null) {
librarySelector = Framework.getRuntime().getService(LibrarySelector.class);
}
if (librarySelector == null) {
log.error("Unable to get LibrarySelector runtime service");
throw new NuxeoException("Unable to get LibrarySelector runtime service");
}
return librarySelector;
}
@Override
public ImageInfo getImageInfo(Blob blob) {
ImageInfo imageInfo = null;
try {
String ext = blob.getFilename() == null ? ".tmp" : "." + FilenameUtils.getExtension(blob.getFilename());
try (CloseableFile cf = blob.getCloseableFile(ext)) {
imageInfo = ImageIdentifier.getInfo(cf.getFile().getCanonicalPath());
}
} catch (CommandNotAvailable | CommandException e) {
log.error("Failed to get ImageInfo for file " + blob.getFilename(), e);
} catch (IOException e) {
log.error("Failed to transfer file " + blob.getFilename(), e);
}
return imageInfo;
}
@Override
public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
if (CONFIGURATION_PARAMETERS_EP.equals(extensionPoint)) {
ImagingConfigurationDescriptor desc = (ImagingConfigurationDescriptor) contribution;
configurationParameters.putAll(desc.getParameters());
} else if (PICTURE_CONVERSIONS_EP.equals(extensionPoint)) {
pictureConversionRegistry.addContribution((PictureConversion) contribution);
}
}
@Override
public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
if (CONFIGURATION_PARAMETERS_EP.equals(extensionPoint)) {
ImagingConfigurationDescriptor desc = (ImagingConfigurationDescriptor) contribution;
for (String configuration : desc.getParameters().keySet()) {
configurationParameters.remove(configuration);
}
} else if (PICTURE_CONVERSIONS_EP.equals(extensionPoint)) {
pictureConversionRegistry.removeContribution((PictureConversion) contribution);
}
}
@Override
public String getConfigurationValue(String configurationName) {
return configurationParameters.get(configurationName);
}
@Override
public String getConfigurationValue(String configurationName, String defaultValue) {
return configurationParameters.containsKey(configurationName) ? configurationParameters.get(configurationName)
: defaultValue;
}
@Override
public void setConfigurationValue(String configurationName, String configurationValue) {
configurationParameters.put(configurationName, configurationValue);
}
@Override
public PictureView computeViewFor(Blob blob, PictureConversion pictureConversion, boolean convert)
throws IOException {
return computeViewFor(blob, pictureConversion, null, convert);
}
@Override
public PictureView computeViewFor(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo,
boolean convert) throws IOException {
String mimeType = blob.getMimeType();
if (mimeType == null) {
blob.setMimeType(getImageMimeType(blob));
}
if (imageInfo == null) {
imageInfo = getImageInfo(blob);
}
return computeView(blob, pictureConversion, imageInfo, convert);
}
@Override
public List<PictureView> computeViewsFor(Blob blob, List<PictureConversion> pictureConversions, boolean convert)
throws IOException {
return computeViewsFor(blob, pictureConversions, null, convert);
}
@Override
public List<PictureView> computeViewsFor(Blob blob, List<PictureConversion> pictureConversions, ImageInfo imageInfo,
boolean convert) throws IOException {
String mimeType = blob.getMimeType();
if (mimeType == null) {
blob.setMimeType(getImageMimeType(blob));
}
if (imageInfo == null) {
imageInfo = getImageInfo(blob);
}
List<PictureView> views = new ArrayList<PictureView>();
for (PictureConversion pictureConversion : pictureConversions) {
views.add(computeView(blob, pictureConversion, imageInfo, convert));
}
return views;
}
protected PictureView computeView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo,
boolean convert) throws IOException {
return computeView(null, blob, pictureConversion, imageInfo, convert);
}
protected PictureView computeView(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
ImageInfo imageInfo, boolean convert) throws IOException {
if (convert) {
return computeView(doc, blob, pictureConversion, imageInfo);
} else {
return computeViewWithoutConversion(blob, pictureConversion, imageInfo);
}
}
/**
* Use
* {@link ImagingComponent#computeView(org.nuxeo.ecm.core.api.DocumentModel, Blob, org.nuxeo.ecm.platform.picture.api.PictureConversion, ImageInfo)}
* by passing the <b>Original</b> picture template.
*
* @deprecated since 7.1
*/
@Deprecated
protected PictureView computeOriginalView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo)
throws IOException {
String filename = blob.getFilename();
String title = pictureConversion.getId();
String viewFilename = title + "_" + filename;
Map<String, Serializable> map = new HashMap<String, Serializable>();
map.put(PictureView.FIELD_TITLE, pictureConversion.getId());
map.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription());
map.put(PictureView.FIELD_FILENAME, viewFilename);
map.put(PictureView.FIELD_TAG, pictureConversion.getTag());
map.put(PictureView.FIELD_WIDTH, imageInfo.getWidth());
map.put(PictureView.FIELD_HEIGHT, imageInfo.getHeight());
Blob originalViewBlob = wrapBlob(blob);
originalViewBlob.setFilename(viewFilename);
map.put(PictureView.FIELD_CONTENT, (Serializable) originalViewBlob);
map.put(PictureView.FIELD_INFO, imageInfo);
return new PictureViewImpl(map);
}
protected Blob wrapBlob(Blob blob) {
return new BlobWrapper(blob);
}
/**
* Use
* {@link ImagingComponent#computeView(org.nuxeo.ecm.core.api.DocumentModel, Blob, org.nuxeo.ecm.platform.picture.api.PictureConversion, ImageInfo)}
* by passing the <b>OriginalJpeg</b> picture template.
*
* @deprecated since 7.1
*/
@Deprecated
protected PictureView computeOriginalJpegView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo)
throws IOException {
String filename = blob.getFilename();
String title = pictureConversion.getId();
int width = imageInfo.getWidth();
int height = imageInfo.getHeight();
Map<String, Serializable> map = new HashMap<String, Serializable>();
map.put(PictureView.FIELD_TITLE, pictureConversion.getId());
map.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription());
map.put(PictureView.FIELD_TAG, pictureConversion.getTag());
map.put(PictureView.FIELD_WIDTH, width);
map.put(PictureView.FIELD_HEIGHT, height);
Map<String, Serializable> options = new HashMap<String, Serializable>();
options.put(OPTION_RESIZE_WIDTH, width);
options.put(OPTION_RESIZE_HEIGHT, height);
options.put(OPTION_RESIZE_DEPTH, imageInfo.getDepth());
// always convert to jpeg
options.put(CONVERSION_FORMAT, JPEG_CONVERSATION_FORMAT);
BlobHolder bh = new SimpleBlobHolder(blob);
ConversionService conversionService = Framework.getLocalService(ConversionService.class);
bh = conversionService.convert(OPERATION_RESIZE, bh, options);
Blob originalJpegBlob = bh.getBlob();
if (originalJpegBlob == null) {
originalJpegBlob = wrapBlob(blob);
}
String viewFilename = String.format("%s_%s.%s", title, FilenameUtils.getBaseName(blob.getFilename()),
FilenameUtils.getExtension(JPEG_CONVERSATION_FORMAT));
map.put(PictureView.FIELD_FILENAME, viewFilename);
originalJpegBlob.setFilename(viewFilename);
map.put(PictureView.FIELD_CONTENT, (Serializable) originalJpegBlob);
map.put(PictureView.FIELD_INFO, getImageInfo(originalJpegBlob));
return new PictureViewImpl(map);
}
/**
* @deprecated since 7.1. We now use the original Blob base name + the computed Blob filename extension.
*/
@Deprecated
protected String computeViewFilename(String filename, String format) {
int index = filename.lastIndexOf(".");
if (index == -1) {
return filename + "." + format;
} else {
return filename.substring(0, index + 1) + format;
}
}
protected PictureView computeView(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
ImageInfo imageInfo) {
String title = pictureConversion.getId();
Map<String, Serializable> pictureViewMap = new HashMap<String, Serializable>();
pictureViewMap.put(PictureView.FIELD_TITLE, title);
pictureViewMap.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription());
pictureViewMap.put(PictureView.FIELD_TAG, pictureConversion.getTag());
Point size = new Point(imageInfo.getWidth(), imageInfo.getHeight());
/*
* If the picture template have a max size then use it for the new size computation, else take the current size
* will be used.
*/
if (pictureConversion.getMaxSize() != null) {
size = getSize(size, pictureConversion.getMaxSize());
}
pictureViewMap.put(PictureView.FIELD_WIDTH, size.x);
pictureViewMap.put(PictureView.FIELD_HEIGHT, size.y);
// Use the registered conversion format
String conversionFormat = getConfigurationValue(CONVERSION_FORMAT, JPEG_CONVERSATION_FORMAT);
Blob viewBlob = callPictureConversionChain(doc, blob, pictureConversion, imageInfo, size, conversionFormat);
String viewFilename = String.format("%s_%s.%s", title, FilenameUtils.getBaseName(blob.getFilename()),
FilenameUtils.getExtension(viewBlob.getFilename()));
viewBlob.setFilename(viewFilename);
pictureViewMap.put(PictureView.FIELD_FILENAME, viewFilename);
pictureViewMap.put(PictureView.FIELD_CONTENT, (Serializable) viewBlob);
pictureViewMap.put(PictureView.FIELD_INFO, getImageInfo(viewBlob));
return new PictureViewImpl(pictureViewMap);
}
protected Blob callPictureConversionChain(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
ImageInfo imageInfo, Point size, String conversionFormat) {
String chainId = pictureConversion.getChainId();
// if the chainId is null just use the same blob (wrapped)
if (StringUtils.isBlank(chainId)) {
return wrapBlob(blob);
}
Properties parameters = new Properties();
parameters.put(OPTION_RESIZE_WIDTH, String.valueOf(size.x));
parameters.put(OPTION_RESIZE_HEIGHT, String.valueOf(size.y));
parameters.put(OPTION_RESIZE_DEPTH, String.valueOf(imageInfo.getDepth()));
parameters.put(CONVERSION_FORMAT, conversionFormat);
Map<String, Object> chainParameters = new HashMap<>();
chainParameters.put("parameters", parameters);
boolean txWasActive = false;
try (OperationContext context = new OperationContext()) {
if (doc != null) {
DocumentModel pictureDocument = doc.getCoreSession().getDocument(doc.getRef());
pictureDocument.detach(true);
context.put("pictureDocument", pictureDocument);
}
context.setInput(blob);
if (TransactionHelper.isTransactionActive()) {
txWasActive = true;
TransactionHelper.commitOrRollbackTransaction();
}
Blob viewBlob = (Blob) Framework.getService(AutomationService.class).run(context, chainId, chainParameters);
if (viewBlob == null) {
viewBlob = wrapBlob(blob);
}
return viewBlob;
} catch (OperationException e) {
throw new NuxeoException(e);
} finally {
if (txWasActive && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
TransactionHelper.startTransaction();
}
}
}
@Override
public List<PictureView> computeViewsFor(DocumentModel doc, Blob blob, ImageInfo imageInfo, boolean convert)
throws IOException {
List<PictureConversion> pictureConversions = getPictureConversions();
List<PictureView> pictureViews = new ArrayList<>(pictureConversions.size());
for (PictureConversion pictureConversion : pictureConversions) {
if (canApplyPictureConversion(pictureConversion, doc)) {
PictureView pictureView = computeView(doc, blob, pictureConversion, imageInfo, convert);
pictureViews.add(pictureView);
}
}
return pictureViews;
}
protected boolean canApplyPictureConversion(PictureConversion pictureConversion, DocumentModel doc) {
ActionManager actionService = Framework.getService(ActionManager.class);
return actionService.checkFilters(pictureConversion.getFilterIds(), createActionContext(doc));
}
protected ActionContext createActionContext(DocumentModel doc) {
ActionContext actionContext = new ELActionContext();
actionContext.setCurrentDocument(doc);
return actionContext;
}
protected PictureView computeViewWithoutConversion(Blob blob, PictureConversion pictureConversion,
ImageInfo imageInfo) {
PictureView view = new PictureViewImpl();
view.setBlob(blob);
view.setWidth(imageInfo.getWidth());
view.setHeight(imageInfo.getHeight());
view.setFilename(blob.getFilename());
view.setTitle(pictureConversion.getId());
view.setDescription(pictureConversion.getDescription());
view.setTag(pictureConversion.getTag());
view.setImageInfo(imageInfo);
return view;
}
protected static Point getSize(Point current, int max) {
int x = current.x;
int y = current.y;
int newX;
int newY;
if (x > y) { // landscape
newY = (y * max) / x;
newX = max;
} else { // portrait
newX = (x * max) / y;
newY = max;
}
if (newX > x || newY > y) {
return current;
}
return new Point(newX, newY);
}
@Override
public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions,
boolean convert) throws IOException {
return computeViewsFor(blobs, pictureConversions, null, convert);
}
@Override
public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions,
ImageInfo imageInfo, boolean convert) throws IOException {
List<List<PictureView>> allViews = new ArrayList<List<PictureView>>();
for (Blob blob : blobs) {
allViews.add(computeViewsFor(blob, pictureConversions, imageInfo, convert));
}
return allViews;
}
@Override
public void activate(ComponentContext context) {
pictureMigrationHandler.install();
}
@Override
public void deactivate(ComponentContext context) {
pictureMigrationHandler.uninstall();
}
}