001/*
002 * (C) Copyright 2015-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 *     thibaud
018 */
019package org.nuxeo.diff.pictures;
020
021import java.io.BufferedReader;
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.InputStreamReader;
026import java.io.Serializable;
027import java.util.Calendar;
028import java.util.HashMap;
029import java.util.Map;
030import java.util.Map.Entry;
031
032import org.apache.commons.text.StringEscapeUtils;
033import org.apache.commons.lang3.StringUtils;
034
035import org.nuxeo.ecm.core.api.Blob;
036import org.nuxeo.ecm.core.api.Blobs;
037import org.nuxeo.ecm.core.api.CloseableFile;
038import org.nuxeo.ecm.core.api.DocumentModel;
039import org.nuxeo.ecm.core.api.NuxeoException;
040import org.nuxeo.ecm.platform.commandline.executor.api.CmdParameters;
041import org.nuxeo.ecm.platform.commandline.executor.api.CommandLineExecutorService;
042import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
043import org.nuxeo.ecm.platform.commandline.executor.api.ExecResult;
044import org.nuxeo.ecm.platform.commandline.executor.service.CommandLineDescriptor;
045import org.nuxeo.ecm.platform.commandline.executor.service.CommandLineExecutorComponent;
046import org.nuxeo.ecm.platform.picture.api.ImageInfo;
047import org.nuxeo.ecm.platform.picture.api.ImagingService;
048import org.nuxeo.ecm.platform.web.common.vh.VirtualHostHelper;
049import org.nuxeo.runtime.api.Framework;
050
051/**
052 * @since 7.4
053 */
054public class DiffPictures {
055
056    public static final String DEFAULT_COMMAND = "diff-pictures-default";
057
058    public static final String COMPARE_PRO_COMMAND = "diff-pictures-pro";
059
060    public static final String DEFAULT_XPATH = "file:content";
061
062    public static final String DEFAULT_FUZZ = "0";
063
064    public static final String DEFAULT_HIGHLIGHT_COLOR = "Red";
065
066    public static final String DEFAULT_LOWLIGHT_COLOR = "None";
067
068    // See nuxeo-diff-pictures-template.html
069    protected static final String TMPL_PREFIX = "{{";
070
071    protected static final String TMPL_SUFFIX = "}}";
072
073    protected static final String TMPL_CONTEXT_PATH = buildTemplateKey("CONTEXT_PATH");
074
075    protected static final String TMPL_ACTION = buildTemplateKey("ACTION");
076
077    protected static final String TMPL_LEFT_DOC_ID = buildTemplateKey("LEFT_DOC_ID");
078
079    protected static final String TMPL_LEFT_DOC_LABEL = buildTemplateKey("LEFT_DOC_LABEL");
080
081    protected static final String TMPL_RIGHT_DOC_ID = buildTemplateKey("RIGHT_DOC_ID");
082
083    protected static final String TMPL_RIGHT_DOC_LABEL = buildTemplateKey("RIGHT_DOC_LABEL");
084
085    protected static final String TMPL_XPATH = buildTemplateKey("XPATH");
086
087    protected static final String TMPL_TIME_STAMP = buildTemplateKey("TIME_STAMP");
088
089    protected static final String TMPL_HIDE_TUNING = buildTemplateKey("HIDE_TUNING");
090
091    protected static final String TMPL_FORCED_COMMAND = buildTemplateKey("FORCED_COMMAND");
092
093    protected static final String TMPL_HIDE_TOOLS_INLINE_CSS = buildTemplateKey("HIDE_TOOLS_INLINE_CSS");
094
095    protected static final String TMPL_IMG_RESULT_NB_COLUMNS = buildTemplateKey("IMG_RESULT_NB_COLUMNS");
096
097    protected static final String TMPL_IMG_RESULT_INLINE_CSS = buildTemplateKey("IMG_RESULT_INLINE_CSS");
098
099    Blob b1;
100
101    Blob b2;
102
103    String leftDocId;
104
105    String rightDocId;
106
107    String commandLine;
108
109    Map<String, Serializable> clParameters;
110
111    protected static String buildTemplateKey(String inName) {
112        return TMPL_PREFIX + inName + TMPL_SUFFIX;
113    }
114
115    public DiffPictures(Blob inB1, Blob inB2) {
116
117        this(inB1, inB2, null, null);
118
119    }
120
121    public DiffPictures(DocumentModel inLeft, DocumentModel inRight) {
122
123        this(inLeft, inRight, null);
124    }
125
126    public DiffPictures(DocumentModel inLeft, DocumentModel inRight, String inXPath) {
127
128        Blob leftB, rightB;
129
130        leftB = DiffPicturesUtils.getDocumentBlob(inLeft, inXPath);
131        rightB = DiffPicturesUtils.getDocumentBlob(inRight, inXPath);
132
133        init(leftB, rightB, inLeft.getId(), inRight.getId());
134    }
135
136    public DiffPictures(Blob inB1, Blob inB2, String inLeftDocId, String inRightDocId) {
137        init(inB1, inB2, inLeftDocId, inRightDocId);
138
139    }
140
141    private void init(Blob inB1, Blob inB2, String inLeftDocId, String inRightDocId) {
142
143        b1 = inB1;
144        b2 = inB2;
145        leftDocId = inLeftDocId;
146        rightDocId = inRightDocId;
147
148    }
149
150    public Blob compare(String inCommandLine, Map<String, Serializable> inParams) throws CommandNotAvailable,
151            IOException {
152
153        String finalName;
154
155        commandLine = StringUtils.isBlank(inCommandLine) ? DEFAULT_COMMAND : inCommandLine;
156        // Being generic, if in the future we add more command lines in the xml.
157        // We know that "compare" can't work with pictures of different format or size, so let's force another command
158        CommandLineDescriptor cld = CommandLineExecutorComponent.getCommandDescriptor(commandLine);
159        if ("compare".equals(cld.getCommand()) && !DiffPicturesUtils.sameFormatAndDimensions(b1, b2)) {
160            commandLine = COMPARE_PRO_COMMAND;
161        }
162
163        clParameters = inParams == null ? new HashMap<>() : inParams;
164
165        finalName = (String) clParameters.get("targetFileName");
166        if (StringUtils.isBlank(finalName)) {
167            finalName = "comp-" + b1.getFilename();
168        }
169
170        CloseableFile cf1 = null, cf2 = null;
171        String filePath1 = null, filePath2 = null;
172        CommandLineExecutorService cles = Framework.getService(CommandLineExecutorService.class);
173        CmdParameters params = cles.getDefaultCmdParameters();
174
175        try {
176            cf1 = b1.getCloseableFile();
177            filePath1 = cf1.getFile().getAbsolutePath();
178            params.addNamedParameter("file1", filePath1);
179
180            cf2 = b2.getCloseableFile();
181            filePath2 = cf2.getFile().getAbsolutePath();
182            params.addNamedParameter("file2", filePath2);
183
184            checkDefaultParametersValues();
185            for (Entry<String, Serializable> entry : clParameters.entrySet()) {
186                params.addNamedParameter(entry.getKey(), (String) entry.getValue());
187            }
188
189            String destFilePath;
190            String destFileExtension;
191
192            int dotIndex = finalName.lastIndexOf('.');
193            if (dotIndex < 0) {
194                destFileExtension = ".tmp";
195            } else {
196                destFileExtension = finalName.substring(dotIndex);
197            }
198
199            Blob tempBlob = Blobs.createBlobWithExtension(destFileExtension);
200            destFilePath = tempBlob.getFile().getAbsolutePath();
201
202            params.addNamedParameter("targetFilePath", destFilePath);
203
204            ExecResult execResult = cles.execCommand(commandLine, params);
205
206            // WARNING
207            // ImageMagick can return a non zero code with some of its commands,
208            // while the execution went totally OK, with no error. The problem is
209            // that the CommandLineExecutorService assumes a non-zero return code is
210            // an error => we must handle the thing by ourselves, basically just
211            // checking if we do have a comparison file created by ImageMagick
212            File tempDestFile = tempBlob.getFile();
213            if (!tempDestFile.exists() || tempDestFile.length() < 1) {
214                throw new NuxeoException("Failed to execute the command <" + commandLine + ">. Final command [ "
215                        + execResult.getCommandLine() + " ] returned with error " + execResult.getReturnCode(),
216                        execResult.getError());
217            }
218
219            tempBlob.setFilename(finalName);
220            // The output generated by the ImageMagick comparison has the MIME type of the first file
221            tempBlob.setMimeType(b1.getMimeType());
222
223            return tempBlob;
224
225        } catch (IOException e) {
226            if (filePath1 == null) {
227                throw new IOException("Could not get a valid File from left blob.", e);
228            }
229            if (filePath2 == null) {
230                throw new IOException("Could not get a valid File from right blob.", e);
231            }
232
233            throw e;
234
235        } finally {
236            if (cf1 != null) {
237                cf1.close();
238            }
239            if (cf2 != null) {
240                cf2.close();
241            }
242        }
243    }
244
245    /*
246     * Adds the default values if a parameter is missing. This applies for all command lines (and some will be unused)
247     */
248    protected void checkDefaultParametersValues() {
249
250        if (isDefaultValue((String) clParameters.get("fuzz"))) {
251            clParameters.put("fuzz", DEFAULT_FUZZ);
252        }
253
254        if (isDefaultValue((String) clParameters.get("highlightColor"))) {
255            clParameters.put("highlightColor", DEFAULT_HIGHLIGHT_COLOR);
256        }
257
258        if (isDefaultValue((String) clParameters.get("lowlightColor"))) {
259            clParameters.put("lowlightColor", DEFAULT_LOWLIGHT_COLOR);
260        }
261
262    }
263
264    protected boolean isDefaultValue(String inValue) {
265        return StringUtils.isBlank(inValue) || inValue.toLowerCase().equals("default");
266    }
267
268    @SuppressWarnings("null")
269    public static String buildDiffHtml(DocumentModel leftDoc, DocumentModel rightDoc, String xpath) throws IOException {
270        String html = "";
271        InputStream in = null;
272        try {
273            in = DiffPictures.class.getResourceAsStream("/files/nuxeo-diff-pictures-template.html");
274            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
275            String line;
276            while ((line = reader.readLine()) != null) {
277                html += line + "\n";
278            }
279
280        } finally {
281            if (in != null) {
282                in.close();
283            }
284        }
285
286        String leftDocId = leftDoc.getId();
287        String rightDocId = rightDoc.getId();
288        String leftLabel = leftDoc.getTitle();
289        String rightLabel = rightDoc.getTitle();
290        if (leftLabel.equals(rightLabel)) {
291            if (leftDoc.isVersion()) {
292                leftLabel = "Version " + leftDoc.getVersionLabel();
293            }
294            if (rightDoc.isVersion()) {
295                rightLabel = "Version " + rightDoc.getVersionLabel();
296            }
297        }
298
299        // Assuming the documents each have a valid blob.
300        ImagingService imagingService = Framework.getService(ImagingService.class);
301        String leftFormat = null;
302        int leftW = -1, leftH = -1;
303        String rightFormat = null;
304        int rightW = -1, rightH = -1;
305
306        Blob bLeft = DiffPicturesUtils.getDocumentBlob(leftDoc, xpath);
307        ImageInfo imgInfo = imagingService.getImageInfo(bLeft);
308        if (imgInfo != null) {
309            leftFormat = imgInfo.getFormat();
310            leftW = imgInfo.getWidth();
311            leftH = imgInfo.getHeight();
312        }
313
314        Blob bRight = DiffPicturesUtils.getDocumentBlob(rightDoc, xpath);
315        imgInfo = imagingService.getImageInfo(bRight);
316        if (imgInfo != null) {
317            rightFormat = imgInfo.getFormat();
318            rightW = imgInfo.getWidth();
319            rightH = imgInfo.getHeight();
320        }
321
322        // Update UI and command line to use, if needed.
323        boolean useProCommand;
324        if (StringUtils.isBlank(leftFormat) || StringUtils.isBlank(rightFormat) || leftW < 0 || leftH < 0 || rightW < 0
325                || rightH < 0) {
326            // If the pictures don't have the infos, let's use the "pro" command
327            useProCommand = true;
328        } else {
329            leftLabel += " (" + leftFormat + ", " + leftW + "x" + leftH + ")";
330            rightLabel += " (" + rightFormat + ", " + rightW + "x" + rightH + ")";
331
332            if (leftFormat.equalsIgnoreCase(rightFormat) && leftW == rightW && leftH == rightH) {
333                useProCommand = false;
334            } else {
335                useProCommand = true;
336            }
337        }
338
339        if (useProCommand) {
340            html = html.replace(TMPL_HIDE_TUNING, "true");
341            html = html.replace(TMPL_HIDE_TOOLS_INLINE_CSS, "display:none;");
342            html = html.replace(TMPL_IMG_RESULT_NB_COLUMNS, "sixteen");
343            html = html.replace(TMPL_IMG_RESULT_INLINE_CSS, "padding-left: 35px;");
344            html = html.replace(TMPL_FORCED_COMMAND, COMPARE_PRO_COMMAND);
345        } else {
346            html = html.replace(TMPL_HIDE_TUNING, "false");
347            html = html.replace(TMPL_HIDE_TOOLS_INLINE_CSS, "");
348            html = html.replace(TMPL_IMG_RESULT_NB_COLUMNS, "twelve");
349            html = html.replace(TMPL_IMG_RESULT_INLINE_CSS, "");
350            html = html.replace(TMPL_FORCED_COMMAND, "");
351        }
352
353        html = html.replace(TMPL_CONTEXT_PATH, VirtualHostHelper.getContextPathProperty());
354        html = html.replace(TMPL_ACTION, "diff");
355        html = html.replace(TMPL_LEFT_DOC_ID,
356                StringEscapeUtils.escapeEcmaScript(StringEscapeUtils.escapeHtml4(leftDocId)));
357        html = html.replace(TMPL_LEFT_DOC_LABEL,
358                StringEscapeUtils.escapeEcmaScript(StringEscapeUtils.escapeHtml4(leftLabel)));
359        html = html.replace(TMPL_RIGHT_DOC_ID,
360                StringEscapeUtils.escapeEcmaScript(StringEscapeUtils.escapeHtml4(rightDocId)));
361        html = html.replace(TMPL_RIGHT_DOC_LABEL,
362                StringEscapeUtils.escapeEcmaScript(StringEscapeUtils.escapeHtml4(rightLabel)));
363        if (StringUtils.isBlank(xpath) || xpath.toLowerCase().equals("default")) {
364            xpath = DEFAULT_XPATH;
365        }
366        html = html.replace(TMPL_XPATH, StringEscapeUtils.escapeEcmaScript(StringEscapeUtils.escapeHtml4(xpath)));
367        // dc:modified can be null... When running the Unit Tests for example
368        String lastModification;
369        Calendar cal = (Calendar) rightDoc.getPropertyValue("dc:modified");
370        if (cal == null) {
371            lastModification = "" + Calendar.getInstance().getTimeInMillis();
372        } else {
373            lastModification = "" + cal.getTimeInMillis();
374        }
375        html = html.replace(TMPL_TIME_STAMP, lastModification);
376
377        return html;
378    }
379
380}