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}