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        // If the extension of the generated binary is empty, it's fetched from the mimetype
403        String extension = FilenameUtils.getExtension(viewBlob.getFilename());
404        if (StringUtils.isEmpty(extension)) {
405            MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class);
406            List<String> extensions = mimetypeRegistry.getExtensionsFromMimetypeName(viewBlob.getMimeType());
407            if (extensions != null && !extensions.isEmpty()) {
408                extension = extensions.get(0);
409            }
410        }
411
412        String viewFilename = String.format("%s_%s.%s", title, FilenameUtils.getBaseName(blob.getFilename()),
413                extension);
414        viewBlob.setFilename(viewFilename);
415        pictureViewMap.put(PictureView.FIELD_FILENAME, viewFilename);
416        pictureViewMap.put(PictureView.FIELD_CONTENT, (Serializable) viewBlob);
417        pictureViewMap.put(PictureView.FIELD_INFO, getImageInfo(viewBlob));
418
419        return new PictureViewImpl(pictureViewMap);
420    }
421
422    protected Blob callPictureConversionChain(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
423            ImageInfo imageInfo, Point size, String conversionFormat) {
424        String chainId = pictureConversion.getChainId();
425
426        // if the chainId is null just use the same blob (wrapped)
427        if (StringUtils.isBlank(chainId)) {
428            return wrapBlob(blob);
429        }
430
431        Properties parameters = new Properties();
432        parameters.put(OPTION_RESIZE_WIDTH, String.valueOf(size.x));
433        parameters.put(OPTION_RESIZE_HEIGHT, String.valueOf(size.y));
434        parameters.put(OPTION_RESIZE_DEPTH, String.valueOf(imageInfo.getDepth()));
435        parameters.put(CONVERSION_FORMAT, conversionFormat);
436
437        Map<String, Object> chainParameters = new HashMap<>();
438        chainParameters.put("parameters", parameters);
439
440        boolean txWasActive = false;
441        try (OperationContext context = new OperationContext()) {
442            if (doc != null) {
443                DocumentModel pictureDocument = doc.getCoreSession().getDocument(doc.getRef());
444                pictureDocument.detach(true);
445                context.put("pictureDocument", pictureDocument);
446            }
447            context.setInput(blob);
448
449            if (TransactionHelper.isTransactionActive()) {
450                txWasActive = true;
451                TransactionHelper.commitOrRollbackTransaction();
452            }
453
454            Blob viewBlob = (Blob) Framework.getService(AutomationService.class).run(context, chainId, chainParameters);
455            if (viewBlob == null) {
456                viewBlob = wrapBlob(blob);
457            }
458            return viewBlob;
459        } catch (OperationException e) {
460            throw new NuxeoException(e);
461        } finally {
462            if (txWasActive && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
463                TransactionHelper.startTransaction();
464            }
465        }
466    }
467
468    @Override
469    public List<PictureView> computeViewsFor(DocumentModel doc, Blob blob, ImageInfo imageInfo, boolean convert)
470            throws IOException {
471        List<PictureConversion> pictureConversions = getPictureConversions();
472        List<PictureView> pictureViews = new ArrayList<>(pictureConversions.size());
473
474        for (PictureConversion pictureConversion : pictureConversions) {
475            if (canApplyPictureConversion(pictureConversion, doc)) {
476                PictureView pictureView = computeView(doc, blob, pictureConversion, imageInfo, convert);
477                pictureViews.add(pictureView);
478            }
479        }
480
481        return pictureViews;
482    }
483
484    protected boolean canApplyPictureConversion(PictureConversion pictureConversion, DocumentModel doc) {
485        ActionManager actionService = Framework.getService(ActionManager.class);
486        return actionService.checkFilters(pictureConversion.getFilterIds(), createActionContext(doc));
487    }
488
489    protected ActionContext createActionContext(DocumentModel doc) {
490        ActionContext actionContext = new ELActionContext();
491        actionContext.setCurrentDocument(doc);
492        return actionContext;
493    }
494
495    protected PictureView computeViewWithoutConversion(Blob blob, PictureConversion pictureConversion,
496            ImageInfo imageInfo) {
497        PictureView view = new PictureViewImpl();
498        view.setBlob(blob);
499        view.setWidth(imageInfo.getWidth());
500        view.setHeight(imageInfo.getHeight());
501        view.setFilename(blob.getFilename());
502        view.setTitle(pictureConversion.getId());
503        view.setDescription(pictureConversion.getDescription());
504        view.setTag(pictureConversion.getTag());
505        view.setImageInfo(imageInfo);
506        return view;
507    }
508
509    protected static Point getSize(Point current, int max) {
510        int x = current.x;
511        int y = current.y;
512        int newX;
513        int newY;
514        if (x > y) { // landscape
515            newY = (y * max) / x;
516            newX = max;
517        } else { // portrait
518            newX = (x * max) / y;
519            newY = max;
520        }
521        if (newX > x || newY > y) {
522            return current;
523        }
524        return new Point(newX, newY);
525    }
526
527    @Override
528    public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions,
529            boolean convert) throws IOException {
530        return computeViewsFor(blobs, pictureConversions, null, convert);
531    }
532
533    @Override
534    public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions,
535            ImageInfo imageInfo, boolean convert) throws IOException {
536        List<List<PictureView>> allViews = new ArrayList<List<PictureView>>();
537        for (Blob blob : blobs) {
538            allViews.add(computeViewsFor(blob, pictureConversions, imageInfo, convert));
539        }
540        return allViews;
541    }
542
543    @Override
544    public void activate(ComponentContext context) {
545        pictureMigrationHandler.install();
546    }
547
548    @Override
549    public void deactivate(ComponentContext context) {
550        pictureMigrationHandler.uninstall();
551    }
552}