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