001/*
002 * (C) Copyright 2015 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 *     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.lang.StringEscapeUtils;
033import org.apache.commons.lang.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
221            return tempBlob;
222
223        } catch (IOException e) {
224            if (filePath1 == null) {
225                throw new IOException("Could not get a valid File from left blob.", e);
226            }
227            if (filePath2 == null) {
228                throw new IOException("Could not get a valid File from right blob.", e);
229            }
230
231            throw e;
232
233        } finally {
234            if (cf1 != null) {
235                cf1.close();
236            }
237            if (cf2 != null) {
238                cf2.close();
239            }
240        }
241    }
242
243    /*
244     * Adds the default values if a parameter is missing. This applies for all command lines (and some will be unused)
245     */
246    protected void checkDefaultParametersValues() {
247
248        if (isDefaultValue((String) clParameters.get("fuzz"))) {
249            clParameters.put("fuzz", DEFAULT_FUZZ);
250        }
251
252        if (isDefaultValue((String) clParameters.get("highlightColor"))) {
253            clParameters.put("highlightColor", DEFAULT_HIGHLIGHT_COLOR);
254        }
255
256        if (isDefaultValue((String) clParameters.get("lowlightColor"))) {
257            clParameters.put("lowlightColor", DEFAULT_LOWLIGHT_COLOR);
258        }
259
260    }
261
262    protected boolean isDefaultValue(String inValue) {
263        return StringUtils.isBlank(inValue) || inValue.toLowerCase().equals("default");
264    }
265
266    @SuppressWarnings("null")
267    public static String buildDiffHtml(DocumentModel leftDoc, DocumentModel rightDoc, String xpath) throws IOException {
268        String html = "";
269        InputStream in = null;
270        try {
271            in = DiffPictures.class.getResourceAsStream("/files/nuxeo-diff-pictures-template.html");
272            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
273            String line;
274            while ((line = reader.readLine()) != null) {
275                html += line + "\n";
276            }
277
278        } finally {
279            if (in != null) {
280                in.close();
281            }
282        }
283
284        String leftDocId = leftDoc.getId();
285        String rightDocId = rightDoc.getId();
286        String leftLabel = leftDoc.getTitle();
287        String rightLabel = rightDoc.getTitle();
288        if (leftLabel.equals(rightLabel)) {
289            if (leftDoc.isVersion()) {
290                leftLabel = "Version " + leftDoc.getVersionLabel();
291            }
292            if (rightDoc.isVersion()) {
293                rightLabel = "Version " + rightDoc.getVersionLabel();
294            }
295        }
296
297        // Assuming the documents each have a valid blob.
298        ImagingService imagingService = Framework.getService(ImagingService.class);
299        String leftFormat = null;
300        int leftW = -1, leftH = -1;
301        String rightFormat = null;
302        int rightW = -1, rightH = -1;
303
304        Blob bLeft = DiffPicturesUtils.getDocumentBlob(leftDoc, xpath);
305        ImageInfo imgInfo = imagingService.getImageInfo(bLeft);
306        if (imgInfo != null) {
307            leftFormat = imgInfo.getFormat();
308            leftW = imgInfo.getWidth();
309            leftH = imgInfo.getHeight();
310        }
311
312        Blob bRight = DiffPicturesUtils.getDocumentBlob(rightDoc, xpath);
313        imgInfo = imagingService.getImageInfo(bRight);
314        if (imgInfo != null) {
315            rightFormat = imgInfo.getFormat();
316            rightW = imgInfo.getWidth();
317            rightH = imgInfo.getHeight();
318        }
319
320        // Update UI and command line to use, if needed.
321        boolean useProCommand;
322        if (StringUtils.isBlank(leftFormat) || StringUtils.isBlank(rightFormat) || leftW < 0 || leftH < 0 || rightW < 0
323                || rightH < 0) {
324            // If the pictures don't have the infos, let's use the "pro" command
325            useProCommand = true;
326        } else {
327            leftLabel += " (" + leftFormat + ", " + leftW + "x" + leftH + ")";
328            rightLabel += " (" + rightFormat + ", " + rightW + "x" + rightH + ")";
329
330            if (leftFormat.equalsIgnoreCase(rightFormat) && leftW == rightW && leftH == rightH) {
331                useProCommand = false;
332            } else {
333                useProCommand = true;
334            }
335        }
336
337        if (useProCommand) {
338            html = html.replace(TMPL_HIDE_TUNING, "true");
339            html = html.replace(TMPL_HIDE_TOOLS_INLINE_CSS, "display:none;");
340            html = html.replace(TMPL_IMG_RESULT_NB_COLUMNS, "sixteen");
341            html = html.replace(TMPL_IMG_RESULT_INLINE_CSS, "padding-left: 35px;");
342            html = html.replace(TMPL_FORCED_COMMAND, COMPARE_PRO_COMMAND);
343        } else {
344            html = html.replace(TMPL_HIDE_TUNING, "false");
345            html = html.replace(TMPL_HIDE_TOOLS_INLINE_CSS, "");
346            html = html.replace(TMPL_IMG_RESULT_NB_COLUMNS, "twelve");
347            html = html.replace(TMPL_IMG_RESULT_INLINE_CSS, "");
348            html = html.replace(TMPL_FORCED_COMMAND, "");
349        }
350
351        html = html.replace(TMPL_CONTEXT_PATH, VirtualHostHelper.getContextPathProperty());
352        html = html.replace(TMPL_ACTION, "diff");
353        html = html.replace(TMPL_LEFT_DOC_ID,
354                StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(leftDocId)));
355        html = html.replace(TMPL_LEFT_DOC_LABEL,
356                StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(leftLabel)));
357        html = html.replace(TMPL_RIGHT_DOC_ID,
358                StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(rightDocId)));
359        html = html.replace(TMPL_RIGHT_DOC_LABEL,
360                StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(rightLabel)));
361        if (StringUtils.isBlank(xpath) || xpath.toLowerCase().equals("default")) {
362            xpath = DEFAULT_XPATH;
363        }
364        html = html.replace(TMPL_XPATH, StringEscapeUtils.escapeJavaScript(StringEscapeUtils.escapeHtml(xpath)));
365        // dc:modified can be null... When running the Unit Tests for example
366        String lastModification;
367        Calendar cal = (Calendar) rightDoc.getPropertyValue("dc:modified");
368        if (cal == null) {
369            lastModification = "" + Calendar.getInstance().getTimeInMillis();
370        } else {
371            lastModification = "" + cal.getTimeInMillis();
372        }
373        html = html.replace(TMPL_TIME_STAMP, lastModification);
374
375        return html;
376    }
377
378}