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.nuxeo.ecm.automation.AutomationService;
044import org.nuxeo.ecm.automation.OperationContext;
045import org.nuxeo.ecm.automation.OperationException;
046import org.nuxeo.ecm.automation.core.util.Properties;
047import org.nuxeo.ecm.core.api.Blob;
048import org.nuxeo.ecm.core.api.Blobs;
049import org.nuxeo.ecm.core.api.CloseableFile;
050import org.nuxeo.ecm.core.api.DocumentModel;
051import org.nuxeo.ecm.core.api.NuxeoException;
052import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
053import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolder;
054import org.nuxeo.ecm.core.api.impl.blob.BlobWrapper;
055import org.nuxeo.ecm.core.convert.api.ConversionService;
056import org.nuxeo.ecm.platform.actions.ActionContext;
057import org.nuxeo.ecm.platform.actions.ELActionContext;
058import org.nuxeo.ecm.platform.actions.ejb.ActionManager;
059import org.nuxeo.ecm.platform.commandline.executor.api.CommandException;
060import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
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 Blob convertToPDF(Blob blob) {
121        return getLibrarySelectorService().getImageUtils().convertToPDF(blob);
122    }
123
124    @Override
125    public Map<String, Object> getImageMetadata(Blob blob) {
126        log.warn("org.nuxeo.ecm.platform.picture.ImagingComponent.getImageMetadata is deprecated. Please use "
127                + "org.nuxeo.binary.metadata.api.BinaryMetadataService#readMetadata(org.nuxeo.ecm.core.api.Blob)");
128        return Collections.emptyMap();
129    }
130
131    @Override
132    public String getImageMimeType(File file) {
133        try {
134            MimetypeRegistry mimetypeRegistry = Framework.getLocalService(MimetypeRegistry.class);
135            if (file.getName() != null) {
136                return mimetypeRegistry.getMimetypeFromFilenameAndBlobWithDefault(file.getName(),
137                        Blobs.createBlob(file), "image/jpeg");
138            } else {
139                return mimetypeRegistry.getMimetypeFromFile(file);
140            }
141        } catch (MimetypeNotFoundException | MimetypeDetectionException | IOException e) {
142            log.error("Unable to retrieve mime type", e);
143        }
144        return null;
145    }
146
147    @Override
148    public String getImageMimeType(Blob blob) {
149        try {
150            MimetypeRegistry mimetypeRegistry = Framework.getLocalService(MimetypeRegistry.class);
151            if (blob.getFilename() != null) {
152                return mimetypeRegistry.getMimetypeFromFilenameAndBlobWithDefault(blob.getFilename(), blob,
153                        "image/jpeg");
154            } else {
155                return mimetypeRegistry.getMimetypeFromBlob(blob);
156            }
157        } catch (MimetypeNotFoundException | MimetypeDetectionException e) {
158            log.error("Unable to retrieve mime type", e);
159        }
160        return null;
161    }
162
163    private LibrarySelector getLibrarySelectorService() {
164        if (librarySelector == null) {
165            librarySelector = Framework.getRuntime().getService(LibrarySelector.class);
166        }
167        if (librarySelector == null) {
168            log.error("Unable to get LibrarySelector runtime service");
169            throw new NuxeoException("Unable to get LibrarySelector runtime service");
170        }
171        return librarySelector;
172    }
173
174    @Override
175    public ImageInfo getImageInfo(Blob blob) {
176        ImageInfo imageInfo = null;
177        try {
178            String ext = blob.getFilename() == null ? ".tmp" : "." + FilenameUtils.getExtension(blob.getFilename());
179            try (CloseableFile cf = blob.getCloseableFile(ext)) {
180                imageInfo = ImageIdentifier.getInfo(cf.getFile().getCanonicalPath());
181            }
182        } catch (CommandNotAvailable | CommandException e) {
183            log.error("Failed to get ImageInfo for file " + blob.getFilename(), e);
184        } catch (IOException e) {
185            log.error("Failed to transfer file " + blob.getFilename(), e);
186        }
187        return imageInfo;
188    }
189
190    @Override
191    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
192        if (CONFIGURATION_PARAMETERS_EP.equals(extensionPoint)) {
193            ImagingConfigurationDescriptor desc = (ImagingConfigurationDescriptor) contribution;
194            configurationParameters.putAll(desc.getParameters());
195        } else if (PICTURE_CONVERSIONS_EP.equals(extensionPoint)) {
196            pictureConversionRegistry.addContribution((PictureConversion) contribution);
197        }
198    }
199
200    @Override
201    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
202        if (CONFIGURATION_PARAMETERS_EP.equals(extensionPoint)) {
203            ImagingConfigurationDescriptor desc = (ImagingConfigurationDescriptor) contribution;
204            for (String configuration : desc.getParameters().keySet()) {
205                configurationParameters.remove(configuration);
206            }
207        } else if (PICTURE_CONVERSIONS_EP.equals(extensionPoint)) {
208            pictureConversionRegistry.removeContribution((PictureConversion) contribution);
209        }
210    }
211
212    @Override
213    public String getConfigurationValue(String configurationName) {
214        return configurationParameters.get(configurationName);
215    }
216
217    @Override
218    public String getConfigurationValue(String configurationName, String defaultValue) {
219        return configurationParameters.containsKey(configurationName) ? configurationParameters.get(configurationName)
220                : defaultValue;
221    }
222
223    @Override
224    public void setConfigurationValue(String configurationName, String configurationValue) {
225        configurationParameters.put(configurationName, configurationValue);
226    }
227
228    @Override
229    public PictureView computeViewFor(Blob blob, PictureConversion pictureConversion, boolean convert)
230            throws IOException {
231        return computeViewFor(blob, pictureConversion, null, convert);
232    }
233
234    @Override
235    public PictureView computeViewFor(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo,
236            boolean convert) throws IOException {
237        String mimeType = blob.getMimeType();
238        if (mimeType == null) {
239            blob.setMimeType(getImageMimeType(blob));
240        }
241
242        if (imageInfo == null) {
243            imageInfo = getImageInfo(blob);
244        }
245        return computeView(blob, pictureConversion, imageInfo, convert);
246    }
247
248    @Override
249    public List<PictureView> computeViewsFor(Blob blob, List<PictureConversion> pictureConversions, boolean convert)
250            throws IOException {
251        return computeViewsFor(blob, pictureConversions, null, convert);
252    }
253
254    @Override
255    public List<PictureView> computeViewsFor(Blob blob, List<PictureConversion> pictureConversions, ImageInfo imageInfo,
256            boolean convert) throws IOException {
257        String mimeType = blob.getMimeType();
258        if (mimeType == null) {
259            blob.setMimeType(getImageMimeType(blob));
260        }
261
262        if (imageInfo == null) {
263            imageInfo = getImageInfo(blob);
264        }
265        List<PictureView> views = new ArrayList<PictureView>();
266        for (PictureConversion pictureConversion : pictureConversions) {
267            views.add(computeView(blob, pictureConversion, imageInfo, convert));
268        }
269        return views;
270    }
271
272    protected PictureView computeView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo,
273            boolean convert) throws IOException {
274        return computeView(null, blob, pictureConversion, imageInfo, convert);
275    }
276
277    protected PictureView computeView(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
278            ImageInfo imageInfo, boolean convert) throws IOException {
279        if (convert) {
280            return computeView(doc, blob, pictureConversion, imageInfo);
281        } else {
282            return computeViewWithoutConversion(blob, pictureConversion, imageInfo);
283        }
284    }
285
286    /**
287     * Use
288     * {@link ImagingComponent#computeView(org.nuxeo.ecm.core.api.DocumentModel, Blob, org.nuxeo.ecm.platform.picture.api.PictureConversion, ImageInfo)}
289     * by passing the <b>Original</b> picture template.
290     *
291     * @deprecated since 7.1
292     */
293    @Deprecated
294    protected PictureView computeOriginalView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo)
295            throws IOException {
296        String filename = blob.getFilename();
297        String title = pictureConversion.getId();
298        String viewFilename = title + "_" + filename;
299        Map<String, Serializable> map = new HashMap<String, Serializable>();
300        map.put(PictureView.FIELD_TITLE, pictureConversion.getId());
301        map.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription());
302        map.put(PictureView.FIELD_FILENAME, viewFilename);
303        map.put(PictureView.FIELD_TAG, pictureConversion.getTag());
304        map.put(PictureView.FIELD_WIDTH, imageInfo.getWidth());
305        map.put(PictureView.FIELD_HEIGHT, imageInfo.getHeight());
306
307        Blob originalViewBlob = wrapBlob(blob);
308        originalViewBlob.setFilename(viewFilename);
309        map.put(PictureView.FIELD_CONTENT, (Serializable) originalViewBlob);
310        map.put(PictureView.FIELD_INFO, imageInfo);
311        return new PictureViewImpl(map);
312    }
313
314    protected Blob wrapBlob(Blob blob) {
315        return new BlobWrapper(blob);
316    }
317
318    /**
319     * Use
320     * {@link ImagingComponent#computeView(org.nuxeo.ecm.core.api.DocumentModel, Blob, org.nuxeo.ecm.platform.picture.api.PictureConversion, ImageInfo)}
321     * by passing the <b>OriginalJpeg</b> picture template.
322     *
323     * @deprecated since 7.1
324     */
325    @Deprecated
326    protected PictureView computeOriginalJpegView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo)
327            throws IOException {
328        String filename = blob.getFilename();
329        String title = pictureConversion.getId();
330        int width = imageInfo.getWidth();
331        int height = imageInfo.getHeight();
332        Map<String, Serializable> map = new HashMap<String, Serializable>();
333        map.put(PictureView.FIELD_TITLE, pictureConversion.getId());
334        map.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription());
335        map.put(PictureView.FIELD_TAG, pictureConversion.getTag());
336        map.put(PictureView.FIELD_WIDTH, width);
337        map.put(PictureView.FIELD_HEIGHT, height);
338        Map<String, Serializable> options = new HashMap<String, Serializable>();
339        options.put(OPTION_RESIZE_WIDTH, width);
340        options.put(OPTION_RESIZE_HEIGHT, height);
341        options.put(OPTION_RESIZE_DEPTH, imageInfo.getDepth());
342        // always convert to jpeg
343        options.put(CONVERSION_FORMAT, JPEG_CONVERSATION_FORMAT);
344        BlobHolder bh = new SimpleBlobHolder(blob);
345        ConversionService conversionService = Framework.getLocalService(ConversionService.class);
346        bh = conversionService.convert(OPERATION_RESIZE, bh, options);
347
348        Blob originalJpegBlob = bh.getBlob();
349        if (originalJpegBlob == null) {
350            originalJpegBlob = wrapBlob(blob);
351        }
352        String viewFilename = String.format("%s_%s.%s", title, FilenameUtils.getBaseName(blob.getFilename()),
353                FilenameUtils.getExtension(JPEG_CONVERSATION_FORMAT));
354        map.put(PictureView.FIELD_FILENAME, viewFilename);
355        originalJpegBlob.setFilename(viewFilename);
356        map.put(PictureView.FIELD_CONTENT, (Serializable) originalJpegBlob);
357        map.put(PictureView.FIELD_INFO, getImageInfo(originalJpegBlob));
358        return new PictureViewImpl(map);
359    }
360
361    /**
362     * @deprecated since 7.1. We now use the original Blob base name + the computed Blob filename extension.
363     */
364    @Deprecated
365    protected String computeViewFilename(String filename, String format) {
366        int index = filename.lastIndexOf(".");
367        if (index == -1) {
368            return filename + "." + format;
369        } else {
370            return filename.substring(0, index + 1) + format;
371        }
372    }
373
374    protected PictureView computeView(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
375            ImageInfo imageInfo) {
376
377        String title = pictureConversion.getId();
378
379        Map<String, Serializable> pictureViewMap = new HashMap<String, Serializable>();
380        pictureViewMap.put(PictureView.FIELD_TITLE, title);
381        pictureViewMap.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription());
382        pictureViewMap.put(PictureView.FIELD_TAG, pictureConversion.getTag());
383
384        Point size = new Point(imageInfo.getWidth(), imageInfo.getHeight());
385
386        /*
387         * If the picture template have a max size then use it for the new size computation, else take the current size
388         * will be used.
389         */
390        if (pictureConversion.getMaxSize() != null) {
391            size = getSize(size, pictureConversion.getMaxSize());
392        }
393
394        pictureViewMap.put(PictureView.FIELD_WIDTH, size.x);
395        pictureViewMap.put(PictureView.FIELD_HEIGHT, size.y);
396
397        // Use the registered conversion format
398        String conversionFormat = getConfigurationValue(CONVERSION_FORMAT, JPEG_CONVERSATION_FORMAT);
399
400        Blob viewBlob = callPictureConversionChain(doc, blob, pictureConversion, imageInfo, size, conversionFormat);
401
402        String viewFilename = String.format("%s_%s.%s", title, FilenameUtils.getBaseName(blob.getFilename()),
403                FilenameUtils.getExtension(viewBlob.getFilename()));
404        viewBlob.setFilename(viewFilename);
405        pictureViewMap.put(PictureView.FIELD_FILENAME, viewFilename);
406        pictureViewMap.put(PictureView.FIELD_CONTENT, (Serializable) viewBlob);
407        pictureViewMap.put(PictureView.FIELD_INFO, getImageInfo(viewBlob));
408
409        return new PictureViewImpl(pictureViewMap);
410    }
411
412    protected Blob callPictureConversionChain(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
413            ImageInfo imageInfo, Point size, String conversionFormat) {
414        String chainId = pictureConversion.getChainId();
415
416        // if the chainId is null just use the same blob (wrapped)
417        if (StringUtils.isBlank(chainId)) {
418            return wrapBlob(blob);
419        }
420
421        Properties parameters = new Properties();
422        parameters.put(OPTION_RESIZE_WIDTH, String.valueOf(size.x));
423        parameters.put(OPTION_RESIZE_HEIGHT, String.valueOf(size.y));
424        parameters.put(OPTION_RESIZE_DEPTH, String.valueOf(imageInfo.getDepth()));
425        parameters.put(CONVERSION_FORMAT, conversionFormat);
426
427        Map<String, Object> chainParameters = new HashMap<>();
428        chainParameters.put("parameters", parameters);
429
430        OperationContext context = new OperationContext();
431        if (doc != null) {
432            DocumentModel pictureDocument = doc.getCoreSession().getDocument(doc.getRef());
433            pictureDocument.detach(true);
434            context.put("pictureDocument", pictureDocument);
435        }
436        context.setInput(blob);
437
438        boolean txWasActive = false;
439        try {
440            if (TransactionHelper.isTransactionActive()) {
441                txWasActive = true;
442                TransactionHelper.commitOrRollbackTransaction();
443            }
444
445            Blob viewBlob = (Blob) Framework.getService(AutomationService.class).run(context, chainId, chainParameters);
446            if (viewBlob == null) {
447                viewBlob = wrapBlob(blob);
448            }
449            return viewBlob;
450        } catch (OperationException e) {
451            throw new NuxeoException(e);
452        } finally {
453            if (txWasActive && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
454                TransactionHelper.startTransaction();
455            }
456        }
457    }
458
459    @Override
460    public List<PictureView> computeViewsFor(DocumentModel doc, Blob blob, ImageInfo imageInfo, boolean convert)
461            throws IOException {
462        List<PictureConversion> pictureConversions = getPictureConversions();
463        List<PictureView> pictureViews = new ArrayList<>(pictureConversions.size());
464
465        for (PictureConversion pictureConversion : pictureConversions) {
466            if (canApplyPictureConversion(pictureConversion, doc)) {
467                PictureView pictureView = computeView(doc, blob, pictureConversion, imageInfo, convert);
468                pictureViews.add(pictureView);
469            }
470        }
471
472        return pictureViews;
473    }
474
475    protected boolean canApplyPictureConversion(PictureConversion pictureConversion, DocumentModel doc) {
476        if (pictureConversion.isDefault()) {
477            return true;
478        }
479
480        ActionManager actionService = Framework.getService(ActionManager.class);
481        return actionService.checkFilters(pictureConversion.getFilterIds(), createActionContext(doc));
482    }
483
484    protected ActionContext createActionContext(DocumentModel doc) {
485        ActionContext actionContext = new ELActionContext();
486        actionContext.setCurrentDocument(doc);
487        return actionContext;
488    }
489
490    protected PictureView computeViewWithoutConversion(Blob blob, PictureConversion pictureConversion,
491            ImageInfo imageInfo) {
492        PictureView view = new PictureViewImpl();
493        view.setBlob(blob);
494        view.setWidth(imageInfo.getWidth());
495        view.setHeight(imageInfo.getHeight());
496        view.setFilename(blob.getFilename());
497        view.setTitle(pictureConversion.getId());
498        view.setDescription(pictureConversion.getDescription());
499        view.setTag(pictureConversion.getTag());
500        view.setImageInfo(imageInfo);
501        return view;
502    }
503
504    protected static Point getSize(Point current, int max) {
505        int x = current.x;
506        int y = current.y;
507        int newX;
508        int newY;
509        if (x > y) { // landscape
510            newY = (y * max) / x;
511            newX = max;
512        } else { // portrait
513            newX = (x * max) / y;
514            newY = max;
515        }
516        if (newX > x || newY > y) {
517            return current;
518        }
519        return new Point(newX, newY);
520    }
521
522    @Override
523    public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions,
524            boolean convert) throws IOException {
525        return computeViewsFor(blobs, pictureConversions, null, convert);
526    }
527
528    @Override
529    public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions,
530            ImageInfo imageInfo, boolean convert) throws IOException {
531        List<List<PictureView>> allViews = new ArrayList<List<PictureView>>();
532        for (Blob blob : blobs) {
533            allViews.add(computeViewsFor(blob, pictureConversions, imageInfo, convert));
534        }
535        return allViews;
536    }
537
538    @Override
539    public void activate(ComponentContext context) {
540        pictureMigrationHandler.install();
541    }
542
543    @Override
544    public void deactivate(ComponentContext context) {
545        pictureMigrationHandler.uninstall();
546    }
547}