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