001/*
002 * (C) Copyright 2007-2018 Nuxeo (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.OPTION_RESIZE_DEPTH;
025import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_HEIGHT;
026import static org.nuxeo.ecm.platform.picture.api.ImagingConvertConstants.OPTION_RESIZE_WIDTH;
027
028import java.awt.Point;
029import java.io.File;
030import java.io.IOException;
031import java.io.Serializable;
032import java.util.ArrayList;
033import java.util.Collections;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037
038import org.apache.commons.io.FilenameUtils;
039import org.apache.commons.lang3.StringUtils;
040import org.apache.logging.log4j.LogManager;
041import org.apache.logging.log4j.Logger;
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.CoreSession;
050import org.nuxeo.ecm.core.api.DocumentModel;
051import org.nuxeo.ecm.core.api.NuxeoException;
052import org.nuxeo.ecm.core.api.impl.blob.BlobWrapper;
053import org.nuxeo.ecm.platform.actions.ActionContext;
054import org.nuxeo.ecm.platform.actions.ELActionContext;
055import org.nuxeo.ecm.platform.actions.ejb.ActionManager;
056import org.nuxeo.ecm.platform.commandline.executor.api.CommandException;
057import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
058import org.nuxeo.ecm.platform.mimetype.MimetypeDetectionException;
059import org.nuxeo.ecm.platform.mimetype.MimetypeNotFoundException;
060import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
061import org.nuxeo.ecm.platform.picture.api.ImageInfo;
062import org.nuxeo.ecm.platform.picture.api.ImagingConfigurationDescriptor;
063import org.nuxeo.ecm.platform.picture.api.ImagingService;
064import org.nuxeo.ecm.platform.picture.api.PictureConversion;
065import org.nuxeo.ecm.platform.picture.api.PictureView;
066import org.nuxeo.ecm.platform.picture.api.PictureViewImpl;
067import org.nuxeo.ecm.platform.picture.core.libraryselector.LibrarySelector;
068import org.nuxeo.ecm.platform.picture.magick.utils.ImageIdentifier;
069import org.nuxeo.ecm.platform.picture.magick.utils.ImageResizer;
070import org.nuxeo.runtime.api.Framework;
071import org.nuxeo.runtime.model.ComponentContext;
072import org.nuxeo.runtime.model.ComponentInstance;
073import org.nuxeo.runtime.model.DefaultComponent;
074import org.nuxeo.runtime.transaction.TransactionHelper;
075
076public class ImagingComponent extends DefaultComponent implements ImagingService {
077
078    private static final Logger log = LogManager.getLogger(ImagingComponent.class);
079
080    public static final String CONFIGURATION_PARAMETERS_EP = "configuration";
081
082    public static final String PICTURE_CONVERSIONS_EP = "pictureConversions";
083
084    protected Map<String, String> configurationParameters = new HashMap<>();
085
086    protected PictureConversionRegistry pictureConversionRegistry = new PictureConversionRegistry();
087
088    protected final PictureMigrationHandler pictureMigrationHandler = new PictureMigrationHandler();
089
090    @Override
091    public List<PictureConversion> getPictureConversions() {
092        return pictureConversionRegistry.getPictureConversions();
093    }
094
095    @Override
096    public PictureConversion getPictureConversion(String id) {
097        return pictureConversionRegistry.getPictureConversion(id);
098    }
099
100    @Override
101    public Blob crop(Blob blob, int x, int y, int width, int height) {
102        return getLibrarySelectorService().getImageUtils().crop(blob, x, y, width, height);
103    }
104
105    @Override
106    public Blob resize(Blob blob, String finalFormat, int width, int height, int depth) {
107        return getLibrarySelectorService().getImageUtils().resize(blob, finalFormat, width, height, depth);
108    }
109
110    @Override
111    public Blob rotate(Blob blob, int angle) {
112        return getLibrarySelectorService().getImageUtils().rotate(blob, angle);
113    }
114
115    @Override
116    public Blob convertToPDF(Blob blob) {
117        return getLibrarySelectorService().getImageUtils().convertToPDF(blob);
118    }
119
120    @Override
121    public Map<String, Object> getImageMetadata(Blob blob) {
122        log.warn("org.nuxeo.ecm.platform.picture.ImagingComponent.getImageMetadata is deprecated. Please use "
123                + "org.nuxeo.binary.metadata.api.BinaryMetadataService#readMetadata(org.nuxeo.ecm.core.api.Blob)");
124        return Collections.emptyMap();
125    }
126
127    @Override
128    public String getImageMimeType(File file) {
129        try {
130            MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class);
131            return mimetypeRegistry.getMimetypeFromFilenameAndBlobWithDefault(file.getName(), Blobs.createBlob(file),
132                    "image/jpeg");
133        } catch (MimetypeNotFoundException | MimetypeDetectionException | IOException e) {
134            log.error("Unable to retrieve mime type", e);
135        }
136        return null;
137    }
138
139    @Override
140    public String getImageMimeType(Blob blob) {
141        try {
142            MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class);
143            if (blob.getFilename() != null) {
144                return mimetypeRegistry.getMimetypeFromFilenameAndBlobWithDefault(blob.getFilename(), blob,
145                        "image/jpeg");
146            } else {
147                return mimetypeRegistry.getMimetypeFromBlob(blob);
148            }
149        } catch (MimetypeNotFoundException | MimetypeDetectionException e) {
150            log.error("Unable to retrieve mime type", e);
151        }
152        return null;
153    }
154
155    private LibrarySelector getLibrarySelectorService() {
156        LibrarySelector librarySelector = Framework.getService(LibrarySelector.class);
157        if (librarySelector == null) {
158            log.error("Unable to get LibrarySelector runtime service");
159            throw new NuxeoException("Unable to get LibrarySelector runtime service");
160        }
161        return librarySelector;
162    }
163
164    @Override
165    public ImageInfo getImageInfo(Blob blob) {
166        ImageInfo imageInfo = null;
167        try {
168            String ext = blob.getFilename() == null ? ".tmp" : "." + FilenameUtils.getExtension(blob.getFilename());
169            try (CloseableFile cf = blob.getCloseableFile(ext)) {
170                imageInfo = ImageIdentifier.getInfo(cf.getFile().getAbsolutePath());
171            }
172        } catch (CommandNotAvailable | CommandException | IOException e) {
173            log.error("Failed to get ImageInfo for file {}", blob.getFilename(), e);
174        }
175        return imageInfo;
176    }
177
178    @Override
179    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
180        if (CONFIGURATION_PARAMETERS_EP.equals(extensionPoint)) {
181            ImagingConfigurationDescriptor desc = (ImagingConfigurationDescriptor) contribution;
182            configurationParameters.putAll(desc.getParameters());
183        } else if (PICTURE_CONVERSIONS_EP.equals(extensionPoint)) {
184            pictureConversionRegistry.addContribution((PictureConversion) contribution);
185        }
186    }
187
188    @Override
189    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
190        if (CONFIGURATION_PARAMETERS_EP.equals(extensionPoint)) {
191            ImagingConfigurationDescriptor desc = (ImagingConfigurationDescriptor) contribution;
192            for (String configuration : desc.getParameters().keySet()) {
193                configurationParameters.remove(configuration);
194            }
195        } else if (PICTURE_CONVERSIONS_EP.equals(extensionPoint)) {
196            pictureConversionRegistry.removeContribution((PictureConversion) contribution);
197        }
198    }
199
200    @Override
201    public String getConfigurationValue(String configurationName) {
202        return configurationParameters.get(configurationName);
203    }
204
205    @Override
206    public String getConfigurationValue(String configurationName, String defaultValue) {
207        return configurationParameters.getOrDefault(configurationName, defaultValue);
208    }
209
210    @Override
211    public void setConfigurationValue(String configurationName, String configurationValue) {
212        configurationParameters.put(configurationName, configurationValue);
213    }
214
215    @Override
216    public PictureView computeViewFor(Blob blob, PictureConversion pictureConversion, boolean convert) {
217        return computeViewFor(blob, pictureConversion, null, convert);
218    }
219
220    @Override
221    public PictureView computeViewFor(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo,
222            boolean convert) {
223        String mimeType = blob.getMimeType();
224        if (mimeType == null) {
225            blob.setMimeType(getImageMimeType(blob));
226        }
227
228        if (imageInfo == null) {
229            imageInfo = getImageInfo(blob);
230        }
231        return computeView(blob, pictureConversion, imageInfo, convert);
232    }
233
234    @Override
235    public List<PictureView> computeViewsFor(Blob blob, List<PictureConversion> pictureConversions, boolean convert) {
236        return computeViewsFor(blob, pictureConversions, null, convert);
237    }
238
239    @Override
240    public List<PictureView> computeViewsFor(Blob blob, List<PictureConversion> pictureConversions, ImageInfo imageInfo,
241            boolean convert) {
242        String mimeType = blob.getMimeType();
243        if (mimeType == null) {
244            blob.setMimeType(getImageMimeType(blob));
245        }
246
247        if (imageInfo == null) {
248            imageInfo = getImageInfo(blob);
249        }
250        List<PictureView> views = new ArrayList<>();
251        for (PictureConversion pictureConversion : pictureConversions) {
252            try {
253                views.add(computeView(blob, pictureConversion, imageInfo, convert));
254            } catch (NuxeoException e) {
255                log.warn("Unable to compute view: {} exception message: {}", pictureConversion::getId, e::getMessage);
256                log.debug(e, e);
257            }
258        }
259        return views;
260    }
261
262    protected PictureView computeView(Blob blob, PictureConversion pictureConversion, ImageInfo imageInfo,
263            boolean convert) {
264        return computeView(null, blob, pictureConversion, imageInfo, convert);
265    }
266
267    protected PictureView computeView(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
268            ImageInfo imageInfo, boolean convert) {
269        if (convert) {
270            return computeView(doc, blob, pictureConversion, imageInfo);
271        } else {
272            return computeViewWithoutConversion(blob, pictureConversion, imageInfo);
273        }
274    }
275
276    protected Blob wrapBlob(Blob blob) {
277        return new BlobWrapper(blob);
278    }
279
280    protected PictureView computeView(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
281            ImageInfo imageInfo) {
282
283        String title = pictureConversion.getId();
284
285        Map<String, Serializable> pictureViewMap = new HashMap<>();
286        pictureViewMap.put(PictureView.FIELD_TITLE, title);
287        pictureViewMap.put(PictureView.FIELD_DESCRIPTION, pictureConversion.getDescription());
288        pictureViewMap.put(PictureView.FIELD_TAG, pictureConversion.getTag());
289
290        Point size = new Point(imageInfo.getWidth(), imageInfo.getHeight());
291
292        /*
293         * If the picture template have a max size then use it for the new size computation, else take the current size
294         * will be used.
295         */
296        if (pictureConversion.getMaxSize() != null) {
297            size = ImageResizer.scaleToMax(size.x, size.y, pictureConversion.getMaxSize());
298        }
299
300        // Use the registered conversion format
301        String conversionFormat = getConfigurationValue(CONVERSION_FORMAT, JPEG_CONVERSATION_FORMAT);
302
303        Blob viewBlob = callPictureConversionChain(doc, blob, pictureConversion, imageInfo, size, conversionFormat);
304
305        // If the extension of the generated binary is empty, it's fetched from the mimetype
306        String extension = FilenameUtils.getExtension(viewBlob.getFilename());
307        if (StringUtils.isEmpty(extension)) {
308            MimetypeRegistry mimetypeRegistry = Framework.getService(MimetypeRegistry.class);
309            List<String> extensions = mimetypeRegistry.getExtensionsFromMimetypeName(viewBlob.getMimeType());
310            if (extensions != null && !extensions.isEmpty()) {
311                extension = extensions.get(0);
312            }
313        }
314
315        String viewFilename = String.format("%s_%s.%s", title, FilenameUtils.getBaseName(blob.getFilename()),
316                extension);
317        viewBlob.setFilename(viewFilename);
318        ImageInfo viewBlobImageInfo = getImageInfo(viewBlob);
319        pictureViewMap.put(PictureView.FIELD_FILENAME, viewFilename);
320        pictureViewMap.put(PictureView.FIELD_CONTENT, (Serializable) viewBlob);
321        pictureViewMap.put(PictureView.FIELD_INFO, viewBlobImageInfo);
322        if (viewBlobImageInfo != null){
323            pictureViewMap.put(PictureView.FIELD_WIDTH, viewBlobImageInfo.getWidth());
324            pictureViewMap.put(PictureView.FIELD_HEIGHT, viewBlobImageInfo.getHeight());
325        }
326        return new PictureViewImpl(pictureViewMap);
327    }
328
329    protected Blob callPictureConversionChain(DocumentModel doc, Blob blob, PictureConversion pictureConversion,
330            ImageInfo imageInfo, Point size, String conversionFormat) {
331        String chainId = pictureConversion.getChainId();
332
333        // if the chainId is null just use the same blob (wrapped)
334        if (StringUtils.isBlank(chainId)) {
335            return wrapBlob(blob);
336        }
337
338        Properties parameters = new Properties();
339        parameters.put(OPTION_RESIZE_WIDTH, String.valueOf(size.x));
340        parameters.put(OPTION_RESIZE_HEIGHT, String.valueOf(size.y));
341        parameters.put(OPTION_RESIZE_DEPTH, String.valueOf(imageInfo.getDepth()));
342        parameters.put(CONVERSION_FORMAT, conversionFormat);
343
344        Map<String, Object> chainParameters = new HashMap<>();
345        chainParameters.put("parameters", parameters);
346
347        boolean txWasActive = false;
348        try (OperationContext context = new OperationContext()) {
349            if (doc != null) {
350                DocumentModel pictureDocument = doc.getCoreSession().getDocument(doc.getRef());
351                pictureDocument.detach(true);
352                context.put("pictureDocument", pictureDocument);
353            }
354            context.setInput(blob);
355
356            if (TransactionHelper.isTransactionActive()) {
357                txWasActive = true;
358                TransactionHelper.commitOrRollbackTransaction();
359            }
360
361            Blob viewBlob = (Blob) Framework.getService(AutomationService.class).run(context, chainId, chainParameters);
362            if (viewBlob == null) {
363                viewBlob = wrapBlob(blob);
364            }
365            return viewBlob;
366        } catch (OperationException e) {
367            throw new NuxeoException(e);
368        } finally {
369            if (txWasActive && !TransactionHelper.isTransactionActiveOrMarkedRollback()) {
370                TransactionHelper.startTransaction();
371            }
372        }
373    }
374
375    @Override
376    public List<PictureView> computeViewsFor(DocumentModel doc, Blob blob, ImageInfo imageInfo, boolean convert) {
377        List<PictureConversion> pictureConversions = getPictureConversions();
378        List<PictureView> pictureViews = new ArrayList<>(pictureConversions.size());
379
380        for (PictureConversion pictureConversion : pictureConversions) {
381            if (canApplyPictureConversion(pictureConversion, doc)) {
382                try {
383                    PictureView pictureView = computeView(doc, blob, pictureConversion, imageInfo, convert);
384                    pictureViews.add(pictureView);
385                } catch (NuxeoException e) {
386                    log.warn("Unable to compute view: {} for document: {} exception message: {}",
387                            pictureConversion::getId, doc::toString, e::getMessage);
388                    log.debug(e, e);
389                }
390            }
391        }
392
393        return pictureViews;
394    }
395
396    protected boolean canApplyPictureConversion(PictureConversion pictureConversion, DocumentModel doc) {
397        ActionManager actionService = Framework.getService(ActionManager.class);
398        return actionService.checkFilters(pictureConversion.getFilterIds(), createActionContext(doc));
399    }
400
401    protected ActionContext createActionContext(DocumentModel doc) {
402        ActionContext actionContext = new ELActionContext();
403        actionContext.setCurrentDocument(doc);
404        CoreSession coreSession = doc.getCoreSession();
405        actionContext.setDocumentManager(coreSession);
406        if (coreSession != null) {
407            actionContext.setCurrentPrincipal(coreSession.getPrincipal());
408        }
409        return actionContext;
410    }
411
412    protected PictureView computeViewWithoutConversion(Blob blob, PictureConversion pictureConversion,
413            ImageInfo imageInfo) {
414        PictureView view = new PictureViewImpl();
415        view.setBlob(blob);
416        view.setWidth(imageInfo.getWidth());
417        view.setHeight(imageInfo.getHeight());
418        view.setFilename(blob.getFilename());
419        view.setTitle(pictureConversion.getId());
420        view.setDescription(pictureConversion.getDescription());
421        view.setTag(pictureConversion.getTag());
422        view.setImageInfo(imageInfo);
423        return view;
424    }
425
426    /**
427     * @deprecated since 11.1 use {@link ImageResizer#scaleToMax(int, int, int)} instead.
428     */
429    @Deprecated
430    protected static Point getSize(Point current, int max) {
431        int x = current.x;
432        int y = current.y;
433        int newX;
434        int newY;
435        if (x > y) { // landscape
436            newY = (y * max) / x;
437            newX = max;
438        } else { // portrait
439            newX = (x * max) / y;
440            newY = max;
441        }
442        if (newX > x || newY > y) {
443            return current;
444        }
445        return new Point(newX, newY);
446    }
447
448    @Override
449    public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions,
450            boolean convert) {
451        return computeViewsFor(blobs, pictureConversions, null, convert);
452    }
453
454    @Override
455    public List<List<PictureView>> computeViewsFor(List<Blob> blobs, List<PictureConversion> pictureConversions,
456            ImageInfo imageInfo, boolean convert) {
457        List<List<PictureView>> allViews = new ArrayList<>();
458        for (Blob blob : blobs) {
459            allViews.add(computeViewsFor(blob, pictureConversions, imageInfo, convert));
460        }
461        return allViews;
462    }
463
464    @Override
465    public void activate(ComponentContext context) {
466        pictureMigrationHandler.install();
467    }
468
469    @Override
470    public void deactivate(ComponentContext context) {
471        pictureMigrationHandler.uninstall();
472    }
473}