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}