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}