001/*
002 * (C) Copyright 2007-2013 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Max Stepanov
016 *     Florent Guillaume
017 */
018package org.nuxeo.ecm.platform.picture;
019
020import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.CONVERSION_FORMAT;
021import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.JPEG_CONVERSATION_FORMAT;
022import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPERATION_RESIZE;
023import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_DEPTH;
024import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_HEIGHT;
025import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_WIDTH;
026
027import java.awt.Point;
028import java.io.File;
029import java.io.IOException;
030import java.io.Serializable;
031import java.util.ArrayList;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036
037import org.apache.commons.io.FilenameUtils;
038import org.apache.commons.lang.StringUtils;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.jboss.el.ExpressionFactoryImpl;
042import org.nuxeo.ecm.automation.AutomationService;
043import org.nuxeo.ecm.automation.OperationContext;
044import org.nuxeo.ecm.automation.OperationException;
045import org.nuxeo.ecm.automation.core.util.Properties;
046import org.nuxeo.ecm.core.api.Blob;
047import org.nuxeo.ecm.core.api.Blobs;
048import org.nuxeo.ecm.core.api.CloseableFile;
049import org.nuxeo.ecm.core.api.DocumentModel;
050import org.nuxeo.ecm.core.api.NuxeoException;
051import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
052import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolder;
053import org.nuxeo.ecm.core.api.impl.blob.BlobWrapper;
054import org.nuxeo.ecm.core.convert.api.ConversionService;
055import org.nuxeo.ecm.platform.actions.ActionContext;
056import org.nuxeo.ecm.platform.actions.ELActionContext;
057import org.nuxeo.ecm.platform.actions.ejb.ActionManager;
058import org.nuxeo.ecm.platform.commandline.executor.api.CommandException;
059import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
060import org.nuxeo.ecm.platform.el.ExpressionContext;
061import org.nuxeo.ecm.platform.mimetype.MimetypeDetectionException;
062import org.nuxeo.ecm.platform.mimetype.MimetypeNotFoundException;
063import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
064import org.nuxeo.ecm.platform.picture.api.ImageInfo;
065import org.nuxeo.ecm.platform.picture.api.ImagingConfigurationDescriptor;
066import org.nuxeo.ecm.platform.picture.api.ImagingService;
067import org.nuxeo.ecm.platform.picture.api.PictureConversion;
068import org.nuxeo.ecm.platform.picture.api.PictureView;
069import org.nuxeo.ecm.platform.picture.api.PictureViewImpl;
070import org.nuxeo.ecm.platform.picture.core.libraryselector.LibrarySelector;
071import org.nuxeo.ecm.platform.picture.magick.utils.ImageIdentifier;
072import org.nuxeo.runtime.api.Framework;
073import org.nuxeo.runtime.model.ComponentContext;
074import org.nuxeo.runtime.model.ComponentInstance;
075import org.nuxeo.runtime.model.DefaultComponent;
076import org.nuxeo.runtime.transaction.TransactionHelper;
077
078public class ImagingComponent extends DefaultComponent implements ImagingService {
079
080    private static final Log log = LogFactory.getLog(ImagingComponent.class);
081
082    public static final String CONFIGURATION_PARAMETERS_EP = "configuration";
083
084    public static final String PICTURE_CONVERSIONS_EP = "pictureConversions";
085
086    protected Map<String, String> configurationParameters = new HashMap<>();
087
088    protected PictureConversionRegistry pictureConversionRegistry = new PictureConversionRegistry();
089
090    private LibrarySelector librarySelector;
091
092    protected final PictureMigrationHandler pictureMigrationHandler = new PictureMigrationHandler();
093
094    @Override
095    public List<PictureConversion> getPictureConversions() {
096        return pictureConversionRegistry.getPictureConversions();
097    }
098
099    @Override
100    public PictureConversion getPictureConversion(String id) {
101        return pictureConversionRegistry.getPictureConversion(id);
102    }
103
104    @Override
105    public Blob crop(Blob blob, int x, int y, int width, int height) {
106        return getLibrarySelectorService().getImageUtils().crop(blob, x, y, width, height);
107    }
108
109    @Override
110    public Blob resize(Blob blob, String finalFormat, int width, int height, int depth) {
111        return getLibrarySelectorService().getImageUtils().resize(blob, finalFormat, width, height, depth);
112    }
113
114    @Override
115    public Blob rotate(Blob blob, int angle) {
116        return getLibrarySelectorService().getImageUtils().rotate(blob, angle);
117    }
118
119    @Override
120    public Map<String, Object> getImageMetadata(Blob blob) {
121        log.warn("org.nuxeo.ecm.platform.picture.ImagingComponent.getImageMetadata is deprecated. Please use "
122                + "org.nuxeo.binary.metadata.api.BinaryMetadataService#readMetadata(org.nuxeo.ecm.core.api.Blob)");
123        return Collections.emptyMap();
124    }
125
126    @Override
127    public String getImageMimeType(File file) {
128        try {
129            MimetypeRegistry mimetypeRegistry = Framework.getLocalService(MimetypeRegistry.class);
130            if (file.getName() != null) {
131                return mimetypeRegistry.getMimetypeFromFilenameAndBlobWithDefault(file.getName(), Blobs.createBlob(file),
132                        "image/jpeg");
133            } else {
134                return mimetypeRegistry.getMimetypeFromFile(file);
135            }
136        } catch (MimetypeNotFoundException | MimetypeDetectionException | IOException e) {
137            log.error("Unable to retrieve mime type", e);
138        }
139        return null;
140    }
141
142    @Override
143    public String getImageMimeType(Blob blob) {
144        try {
145            MimetypeRegistry mimetypeRegistry = Framework.getLocalService(MimetypeRegistry.class);
146            if (blob.getFilename() != null) {
147                return mimetypeRegistry.getMimetypeFromFilenameAndBlobWithDefault(blob.getFilename(), blob,
148                        "image/jpeg");
149            } else {
150                return mimetypeRegistry.getMimetypeFromBlob(blob);
151            }
152        } catch (MimetypeNotFoundException | MimetypeDetectionException e) {
153            log.error("Unable to retrieve mime type", e);
154        }
155        return null;
156    }
157
158    private LibrarySelector getLibrarySelectorService() {
159        if (librarySelector == null) {
160            librarySelector = Framework.getRuntime().getService(LibrarySelector.class);
161        }
162        if (librarySelector == null) {
163            log.error("Unable to get LibrarySelector runtime service");
164            throw new NuxeoException("Unable to get LibrarySelector runtime service");
165        }
166        return librarySelector;
167    }
168
169    @Override
170    public ImageInfo getImageInfo(Blob blob) {
171        ImageInfo imageInfo = null;
172        try {
173            String ext = blob.getFilename() == null ? ".tmp" : "." + FilenameUtils.getExtension(blob.getFilename());
174            try (CloseableFile cf = blob.getCloseableFile(ext)) {
175                imageInfo = ImageIdentifier.getInfo(cf.getFile().getCanonicalPath());
176            }
177        } catch (CommandNotAvailable | CommandException e) {
178            log.error("Failed to get ImageInfo for file " + blob.getFilename(), e);
179        } catch (IOException e) {
180            log.error("Failed to transfer file " + blob.getFilename(), e);
181        }
182        return imageInfo;
183    }
184
185    @Override
186    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
187        if (CONFIGURATION_PARAMETERS_EP.equals(extensionPoint)) {
188            ImagingConfigurationDescriptor desc = (ImagingConfigurationDescriptor) contribution;
189            configurationParameters.putAll(desc.getParameters());
190        } else if (PICTURE_CONVERSIONS_EP.equals(extensionPoint)) {
191            pictureConversionRegistry.addContribution((PictureConversion) contribution);
192        }
193    }
194
195    @Override
196    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
197        if (CONFIGURATION_PARAMETERS_EP.equals(extensionPoint)) {
198            ImagingConfigurationDescriptor desc = (ImagingConfigurationDescriptor) contribution;
199            for (String configuration : desc.getParameters().keySet()) {
200                configurationParameters.remove(configuration);
201            }
202        } else if (PICTURE_CONVERSIONS_EP.equals(extensionPoint)) {
203            pictureConversionRegistry.removeContribution((PictureConversion) contribution);
204        }
205    }
206
207    @Override
208    public String getConfigurationValue(String configurationName) {
209        return configurationParameters.get(configurationName);
210    }
211
212    @Override
213    public String getConfigurationValue(String configurationName, String defaultValue) {
214        return configurationParameters.containsKey(configurationName) ? configurationParameters.get(configurationName)
215                : defaultValue;
216    }
217
218    @Override
219    public void setConfigurationValue(String configurationName, String configurationValue) {
220        configurationParameters.put(configurationName, configurationValue);
221    }
222
223    @Override
224    public PictureView computeViewFor(Blob blob, PictureConversion pictureConversion, boolean convert)
225            throws IOException {
226        return computeViewFor(blob, pictureConversion, null, convert);
227    }
228
229    @Override
230    public PictureView computeViewFor(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo,
231            boolean convert) throws IOException {
232        String mimeType = blob.getMimeType();
233        if (mimeType == null) {
234            blob.setMimeType(getImageMimeType(blob));
235        }
236
237        if (imageInfo == null) {
238            imageInfo = getImageInfo(blob);
239        }
240        return computeView(blob, pictureConversion, imageInfo, convert);
241    }
242
243    @Override
244    public List<PictureView> computeViewsFor(Blob blob, List<PictureConversion> pictureConversions, boolean convert)
245            throws IOException {
246        return computeViewsFor(blob, pictureConversions, null, convert);
247    }
248
249    @Override
250    public List<PictureView> computeViewsFor(Blob blob, List<PictureConversion> pictureConversions,
251            ImageInfo imageInfo, boolean convert) throws IOException {
252        String mimeType = blob.getMimeType();
253        if (mimeType == null) {
254            blob.setMimeType(getImageMimeType(blob));
255        }
256
257        if (imageInfo == null) {
258            imageInfo = getImageInfo(blob);
259        }
260        List<PictureView> views = new ArrayList<PictureView>();
261        for (PictureConversion pictureConversion : pictureConversions) {
262            views.add(computeView(blob, pictureConversion, imageInfo, convert));
263        }
264        return views;
265    }
266
267    protected PictureView computeView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo,
268            boolean convert) throws IOException {
269        return computeView(null, blob, pictureConversion, imageInfo, convert);
270    }
271
272    protected PictureView computeView(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
273            ImageInfo imageInfo, boolean convert) throws IOException {
274        if (convert) {
275            return computeView(doc, blob, pictureConversion, imageInfo);
276        } else {
277            return computeViewWithoutConversion(blob, pictureConversion, imageInfo);
278        }
279    }
280
281    /**
282     * Use
283     * {@link ImagingComponent#computeView(org.nuxeo.ecm.core.api.DocumentModel, Blob, org.nuxeo.ecm.platform.picture.api.PictureConversion, ImageInfo)}
284     * by passing the <b>Original</b> picture template.
285     *
286     * @deprecated since 7.1
287     */
288    @Deprecated
289    protected PictureView computeOriginalView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo)
290            throws IOException {
291        String filename = blob.getFilename();
292        String title = pictureConversion.getId();
293        String viewFilename = title + "_" + filename;
294        Map<String, Serializable> map = new HashMap<String, Serializable>();
295        map.put(PictureView.FIELD_TITLE, pictureConversion.getId());
296        map.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription());
297        map.put(PictureView.FIELD_FILENAME, viewFilename);
298        map.put(PictureView.FIELD_TAG, pictureConversion.getTag());
299        map.put(PictureView.FIELD_WIDTH, imageInfo.getWidth());
300        map.put(PictureView.FIELD_HEIGHT, imageInfo.getHeight());
301
302        Blob originalViewBlob = wrapBlob(blob);
303        originalViewBlob.setFilename(viewFilename);
304        map.put(PictureView.FIELD_CONTENT, (Serializable) originalViewBlob);
305        map.put(PictureView.FIELD_INFO, imageInfo);
306        return new PictureViewImpl(map);
307    }
308
309    protected Blob wrapBlob(Blob blob) {
310        return new BlobWrapper(blob);
311    }
312
313    /**
314     * Use
315     * {@link ImagingComponent#computeView(org.nuxeo.ecm.core.api.DocumentModel, Blob, org.nuxeo.ecm.platform.picture.api.PictureConversion, ImageInfo)}
316     * by passing the <b>OriginalJpeg</b> picture template.
317     *
318     * @deprecated since 7.1
319     */
320    @Deprecated
321    protected PictureView computeOriginalJpegView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo)
322            throws IOException {
323        String filename = blob.getFilename();
324        String title = pictureConversion.getId();
325        int width = imageInfo.getWidth();
326        int height = imageInfo.getHeight();
327        Map<String, Serializable> map = new HashMap<String, Serializable>();
328        map.put(PictureView.FIELD_TITLE, pictureConversion.getId());
329        map.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription());
330        map.put(PictureView.FIELD_TAG, pictureConversion.getTag());
331        map.put(PictureView.FIELD_WIDTH, width);
332        map.put(PictureView.FIELD_HEIGHT, height);
333        Map<String, Serializable> options = new HashMap<String, Serializable>();
334        options.put(OPTION_RESIZE_WIDTH, width);
335        options.put(OPTION_RESIZE_HEIGHT, height);
336        options.put(OPTION_RESIZE_DEPTH, imageInfo.getDepth());
337        // always convert to jpeg
338        options.put(CONVERSION_FORMAT, JPEG_CONVERSATION_FORMAT);
339        BlobHolder bh = new SimpleBlobHolder(blob);
340        ConversionService conversionService = Framework.getLocalService(ConversionService.class);
341        bh = conversionService.convert(OPERATION_RESIZE, bh, options);
342
343        Blob originalJpegBlob = bh.getBlob();
344        if (originalJpegBlob == null) {
345            originalJpegBlob = wrapBlob(blob);
346        }
347        String viewFilename = String.format("%s_%s.%s", title, FilenameUtils.getBaseName(blob.getFilename()),
348                FilenameUtils.getExtension(JPEG_CONVERSATION_FORMAT));
349        map.put(PictureView.FIELD_FILENAME, viewFilename);
350        originalJpegBlob.setFilename(viewFilename);
351        map.put(PictureView.FIELD_CONTENT, (Serializable) originalJpegBlob);
352        map.put(PictureView.FIELD_INFO, getImageInfo(originalJpegBlob));
353        return new PictureViewImpl(map);
354    }
355
356    /**
357     * @deprecated since 7.1. We now use the original Blob base name + the computed Blob filename extension.
358     */
359    @Deprecated
360    protected String computeViewFilename(String filename, String format) {
361        int index = filename.lastIndexOf(".");
362        if (index == -1) {
363            return filename + "." + format;
364        } else {
365            return filename.substring(0, index + 1) + format;
366        }
367    }
368
369    protected PictureView computeView(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
370            ImageInfo imageInfo) {
371
372        String title = pictureConversion.getId();
373
374        Map<String, Serializable> pictureViewMap = new HashMap<String, Serializable>();
375        pictureViewMap.put(PictureView.FIELD_TITLE, title);
376        pictureViewMap.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription());
377        pictureViewMap.put(PictureView.FIELD_TAG, pictureConversion.getTag());
378
379        Point size = new Point(imageInfo.getWidth(), imageInfo.getHeight());
380
381        /*
382         * If the picture template have a max size then use it for the new size computation, else take the current size
383         * will be used.
384         */
385        if (pictureConversion.getMaxSize() != null) {
386            size = getSize(size, pictureConversion.getMaxSize());
387        }
388
389        pictureViewMap.put(PictureView.FIELD_WIDTH, size.x);
390        pictureViewMap.put(PictureView.FIELD_HEIGHT, size.y);
391
392        // Use the registered conversion format
393        String conversionFormat = getConfigurationValue(CONVERSION_FORMAT, JPEG_CONVERSATION_FORMAT);
394
395        Blob viewBlob = callPictureConversionChain(doc, blob, pictureConversion, imageInfo, size, conversionFormat);
396
397        String viewFilename = String.format("%s_%s.%s", title, FilenameUtils.getBaseName(blob.getFilename()),
398                FilenameUtils.getExtension(viewBlob.getFilename()));
399        viewBlob.setFilename(viewFilename);
400        pictureViewMap.put(PictureView.FIELD_FILENAME, viewFilename);
401        pictureViewMap.put(PictureView.FIELD_CONTENT, (Serializable) viewBlob);
402        pictureViewMap.put(PictureView.FIELD_INFO, getImageInfo(viewBlob));
403
404        return new PictureViewImpl(pictureViewMap);
405    }
406
407    protected Blob callPictureConversionChain(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
408            ImageInfo imageInfo, Point size, String conversionFormat) {
409        String chainId = pictureConversion.getChainId();
410
411        // if the chainId is null just use the same blob (wrapped)
412        if (StringUtils.isBlank(chainId)) {
413            return wrapBlob(blob);
414        }
415
416        Properties parameters = new Properties();
417        parameters.put(OPTION_RESIZE_WIDTH, String.valueOf(size.x));
418        parameters.put(OPTION_RESIZE_HEIGHT, String.valueOf(size.y));
419        parameters.put(OPTION_RESIZE_DEPTH, String.valueOf(imageInfo.getDepth()));
420        parameters.put(CONVERSION_FORMAT, conversionFormat);
421
422        Map<String, Object> chainParameters = new HashMap<>();
423        chainParameters.put("parameters", parameters);
424
425        OperationContext context = new OperationContext();
426        if (doc != null) {
427            DocumentModel pictureDocument = doc.getCoreSession().getDocument(doc.getRef());
428            pictureDocument.detach(true);
429            context.put("pictureDocument", pictureDocument);
430        }
431        context.setInput(blob);
432
433        boolean txWasActive = false;
434        try {
435            if (TransactionHelper.isTransactionActive()) {
436                txWasActive = true;
437                TransactionHelper.commitOrRollbackTransaction();
438            }
439
440            Blob viewBlob = (Blob) Framework.getService(AutomationService.class).run(context, chainId, chainParameters);
441            if (viewBlob == null) {
442                viewBlob = wrapBlob(blob);
443            }
444            return viewBlob;
445        } catch (OperationException e) {
446            throw new NuxeoException(e);
447        } finally {
448            if (txWasActive && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
449                TransactionHelper.startTransaction();
450            }
451        }
452    }
453
454    @Override
455    public List<PictureView> computeViewsFor(DocumentModel doc, Blob blob, ImageInfo imageInfo, boolean convert)
456            throws IOException {
457        List<PictureConversion> pictureConversions = getPictureConversions();
458        List<PictureView> pictureViews = new ArrayList<>(pictureConversions.size());
459
460        for (PictureConversion pictureConversion : pictureConversions) {
461            if (canApplyPictureConversion(pictureConversion, doc)) {
462                PictureView pictureView = computeView(doc, blob, pictureConversion, imageInfo, convert);
463                pictureViews.add(pictureView);
464            }
465        }
466
467        return pictureViews;
468    }
469
470    protected boolean canApplyPictureConversion(PictureConversion pictureConversion, DocumentModel doc) {
471        if (pictureConversion.isDefault()) {
472            return true;
473        }
474
475        ActionManager actionService = Framework.getService(ActionManager.class);
476        return actionService.checkFilters(pictureConversion.getFilterIds(), createActionContext(doc));
477    }
478
479    protected ActionContext createActionContext(DocumentModel doc) {
480        ActionContext actionContext = new ELActionContext(new ExpressionContext(), new ExpressionFactoryImpl());
481        actionContext.setCurrentDocument(doc);
482        return actionContext;
483    }
484
485    protected PictureView computeViewWithoutConversion(Blob blob, PictureConversion pictureConversion,
486            ImageInfo imageInfo) {
487        PictureView view = new PictureViewImpl();
488        view.setBlob(blob);
489        view.setWidth(imageInfo.getWidth());
490        view.setHeight(imageInfo.getHeight());
491        view.setFilename(blob.getFilename());
492        view.setTitle(pictureConversion.getId());
493        view.setDescription(pictureConversion.getDescription());
494        view.setTag(pictureConversion.getTag());
495        view.setImageInfo(imageInfo);
496        return view;
497    }
498
499    protected static Point getSize(Point current, int max) {
500        int x = current.x;
501        int y = current.y;
502        int newX;
503        int newY;
504        if (x > y) { // landscape
505            newY = (y * max) / x;
506            newX = max;
507        } else { // portrait
508            newX = (x * max) / y;
509            newY = max;
510        }
511        if (newX > x || newY > y) {
512            return current;
513        }
514        return new Point(newX, newY);
515    }
516
517    @Override
518    public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions,
519            boolean convert) throws IOException {
520        return computeViewsFor(blobs, pictureConversions, null, convert);
521    }
522
523    @Override
524    public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions,
525            ImageInfo imageInfo, boolean convert) throws IOException {
526        List<List<PictureView>> allViews = new ArrayList<List<PictureView>>();
527        for (Blob blob : blobs) {
528            allViews.add(computeViewsFor(blob, pictureConversions, imageInfo, convert));
529        }
530        return allViews;
531    }
532
533    @Override
534    public void activate(ComponentContext context) {
535        pictureMigrationHandler.install();
536    }
537
538    @Override
539    public void deactivate(ComponentContext context) {
540        pictureMigrationHandler.uninstall();
541    }
542}