1 /* 2 * Copyright 2012 Google Inc. 3 * 4 * Use of this source code is governed by a BSD-style license that can be 5 * found in the LICENSE file. 6 */ 7 #include "skdiff.h" 8 #include "skdiff_utils.h" 9 #include "SkBitmap.h" 10 #include "SkData.h" 11 #include "SkImageEncoder.h" 12 #include "SkOSFile.h" 13 #include "SkTypes.h" 14 15 #include <stdio.h> 16 17 /// If outputDir.isEmpty(), don't write out diff files. 18 static void create_diff_images (DiffMetricProc dmp, 19 const int colorThreshold, 20 const SkString& baseFile, 21 const SkString& comparisonFile, 22 const SkString& outputDir, 23 const SkString& outputFilename, 24 DiffRecord* drp) { 25 SkASSERT(!baseFile.isEmpty()); 26 SkASSERT(!comparisonFile.isEmpty()); 27 28 drp->fBase.fFilename = baseFile; 29 drp->fBase.fFullPath = baseFile; 30 drp->fBase.fStatus = DiffResource::kSpecified_Status; 31 32 drp->fComparison.fFilename = comparisonFile; 33 drp->fComparison.fFullPath = comparisonFile; 34 drp->fComparison.fStatus = DiffResource::kSpecified_Status; 35 36 sk_sp<SkData> baseFileBits = read_file(drp->fBase.fFullPath.c_str()); 37 if (baseFileBits) { 38 drp->fBase.fStatus = DiffResource::kRead_Status; 39 } 40 sk_sp<SkData> comparisonFileBits = read_file(drp->fComparison.fFullPath.c_str()); 41 if (comparisonFileBits) { 42 drp->fComparison.fStatus = DiffResource::kRead_Status; 43 } 44 if (nullptr == baseFileBits || nullptr == comparisonFileBits) { 45 if (nullptr == baseFileBits) { 46 drp->fBase.fStatus = DiffResource::kCouldNotRead_Status; 47 } 48 if (nullptr == comparisonFileBits) { 49 drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status; 50 } 51 drp->fResult = DiffRecord::kCouldNotCompare_Result; 52 return; 53 } 54 55 if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) { 56 drp->fResult = DiffRecord::kEqualBits_Result; 57 return; 58 } 59 60 get_bitmap(baseFileBits, drp->fBase, false); 61 get_bitmap(comparisonFileBits, drp->fComparison, false); 62 if (DiffResource::kDecoded_Status != drp->fBase.fStatus || 63 DiffResource::kDecoded_Status != drp->fComparison.fStatus) 64 { 65 drp->fResult = DiffRecord::kCouldNotCompare_Result; 66 return; 67 } 68 69 create_and_write_diff_image(drp, dmp, colorThreshold, outputDir, outputFilename); 70 //TODO: copy fBase.fFilename and fComparison.fFilename to outputDir 71 // svn and git often present tmp files to diff tools which are promptly deleted 72 73 //TODO: serialize drp to outputDir 74 // write a tool to deserialize them and call print_diff_page 75 76 SkASSERT(DiffRecord::kUnknown_Result != drp->fResult); 77 } 78 79 static void usage (char * argv0) { 80 SkDebugf("Skia image diff tool\n"); 81 SkDebugf("\n" 82 "Usage: \n" 83 " %s <baseFile> <comparisonFile>\n" , argv0); 84 SkDebugf( 85 "\nArguments:" 86 "\n --failonresult <result>: After comparing all file pairs, exit with nonzero" 87 "\n return code (number of file pairs yielding this" 88 "\n result) if any file pairs yielded this result." 89 "\n This flag may be repeated, in which case the" 90 "\n return code will be the number of fail pairs" 91 "\n yielding ANY of these results." 92 "\n --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return" 93 "\n code if any file pairs yeilded this status." 94 "\n --help: display this info" 95 "\n --listfilenames: list all filenames for each result type in stdout" 96 "\n --nodiffs: don't write out image diffs, just generate report on stdout" 97 "\n --outputdir: directory to write difference images" 98 "\n --threshold <n>: only report differences > n (per color channel) [default 0]" 99 "\n -u: ignored. Recognized for compatibility with svn diff." 100 "\n -L: first occurrence label for base, second occurrence label for comparison." 101 "\n Labels must be of the form \"<filename>(\t<specifier>)?\"." 102 "\n The base <filename> will be used to create files in outputdir." 103 "\n" 104 "\n baseFile: baseline image file." 105 "\n comparisonFile: comparison image file" 106 "\n" 107 "\nIf no sort is specified, it will sort by fraction of pixels mismatching." 108 "\n"); 109 } 110 111 const int kNoError = 0; 112 const int kGenericError = -1; 113 114 int main(int argc, char** argv) { 115 DiffMetricProc diffProc = compute_diff_pmcolor; 116 117 // Maximum error tolerated in any one color channel in any one pixel before 118 // a difference is reported. 119 int colorThreshold = 0; 120 SkString baseFile; 121 SkString baseLabel; 122 SkString comparisonFile; 123 SkString comparisonLabel; 124 SkString outputDir; 125 126 bool listFilenames = false; 127 128 bool failOnResultType[DiffRecord::kResultCount]; 129 for (int i = 0; i < DiffRecord::kResultCount; i++) { 130 failOnResultType[i] = false; 131 } 132 133 bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount]; 134 for (int base = 0; base < DiffResource::kStatusCount; ++base) { 135 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { 136 failOnStatusType[base][comparison] = false; 137 } 138 } 139 140 int i; 141 int numUnflaggedArguments = 0; 142 int numLabelArguments = 0; 143 for (i = 1; i < argc; i++) { 144 if (!strcmp(argv[i], "--failonresult")) { 145 if (argc == ++i) { 146 SkDebugf("failonresult expects one argument.\n"); 147 continue; 148 } 149 DiffRecord::Result type = DiffRecord::getResultByName(argv[i]); 150 if (type != DiffRecord::kResultCount) { 151 failOnResultType[type] = true; 152 } else { 153 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]); 154 } 155 continue; 156 } 157 if (!strcmp(argv[i], "--failonstatus")) { 158 if (argc == ++i) { 159 SkDebugf("failonstatus missing base status.\n"); 160 continue; 161 } 162 bool baseStatuses[DiffResource::kStatusCount]; 163 if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) { 164 SkDebugf("unrecognized base status <%s>\n", argv[i]); 165 } 166 167 if (argc == ++i) { 168 SkDebugf("failonstatus missing comparison status.\n"); 169 continue; 170 } 171 bool comparisonStatuses[DiffResource::kStatusCount]; 172 if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) { 173 SkDebugf("unrecognized comarison status <%s>\n", argv[i]); 174 } 175 176 for (int base = 0; base < DiffResource::kStatusCount; ++base) { 177 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { 178 failOnStatusType[base][comparison] |= 179 baseStatuses[base] && comparisonStatuses[comparison]; 180 } 181 } 182 continue; 183 } 184 if (!strcmp(argv[i], "--help")) { 185 usage(argv[0]); 186 return kNoError; 187 } 188 if (!strcmp(argv[i], "--listfilenames")) { 189 listFilenames = true; 190 continue; 191 } 192 if (!strcmp(argv[i], "--outputdir")) { 193 if (argc == ++i) { 194 SkDebugf("outputdir expects one argument.\n"); 195 continue; 196 } 197 outputDir.set(argv[i]); 198 continue; 199 } 200 if (!strcmp(argv[i], "--threshold")) { 201 colorThreshold = atoi(argv[++i]); 202 continue; 203 } 204 if (!strcmp(argv[i], "-u")) { 205 //we don't produce unified diffs, ignore parameter to work with svn diff 206 continue; 207 } 208 if (!strcmp(argv[i], "-L")) { 209 if (argc == ++i) { 210 SkDebugf("label expects one argument.\n"); 211 continue; 212 } 213 switch (numLabelArguments++) { 214 case 0: 215 baseLabel.set(argv[i]); 216 continue; 217 case 1: 218 comparisonLabel.set(argv[i]); 219 continue; 220 default: 221 SkDebugf("extra label argument <%s>\n", argv[i]); 222 usage(argv[0]); 223 return kGenericError; 224 } 225 continue; 226 } 227 if (argv[i][0] != '-') { 228 switch (numUnflaggedArguments++) { 229 case 0: 230 baseFile.set(argv[i]); 231 continue; 232 case 1: 233 comparisonFile.set(argv[i]); 234 continue; 235 default: 236 SkDebugf("extra unflagged argument <%s>\n", argv[i]); 237 usage(argv[0]); 238 return kGenericError; 239 } 240 } 241 242 SkDebugf("Unrecognized argument <%s>\n", argv[i]); 243 usage(argv[0]); 244 return kGenericError; 245 } 246 247 if (numUnflaggedArguments != 2) { 248 usage(argv[0]); 249 return kGenericError; 250 } 251 252 if (listFilenames) { 253 printf("Base file is [%s]\n", baseFile.c_str()); 254 } 255 256 if (listFilenames) { 257 printf("Comparison file is [%s]\n", comparisonFile.c_str()); 258 } 259 260 if (outputDir.isEmpty()) { 261 if (listFilenames) { 262 printf("Not writing any diffs. No output dir specified.\n"); 263 } 264 } else { 265 if (!outputDir.endsWith(PATH_DIV_STR)) { 266 outputDir.append(PATH_DIV_STR); 267 } 268 if (listFilenames) { 269 printf("Writing diffs. Output dir is [%s]\n", outputDir.c_str()); 270 } 271 } 272 273 // Some obscure documentation about diff/patch labels: 274 // 275 // Posix says the format is: <filename><tab><date> 276 // It also states that if a filename contains <tab> or <newline> 277 // the result is implementation defined 278 // 279 // Svn diff --diff-cmd provides labels of the form: <filename><tab><revision> 280 // 281 // Git diff --ext-diff does not supply arguments compatible with diff. 282 // However, it does provide the filename directly. 283 // skimagediff_git.sh: skimagediff %2 %5 -L "%1\t(%3)" -L "%1\t(%6)" 284 // 285 // Git difftool sets $LOCAL, $REMOTE, $MERGED, and $BASE instead of command line parameters. 286 // difftool.<>.cmd: skimagediff $LOCAL $REMOTE -L "$MERGED\t(local)" -L "$MERGED\t(remote)" 287 // 288 // Diff will write any specified label verbatim. Without a specified label diff will write 289 // <filename><tab><date> 290 // However, diff will encode the filename as a cstring if the filename contains 291 // Any of <space> or <double quote> 292 // A char less than 32 293 // Any escapable character \\, \a, \b, \t, \n, \v, \f, \r 294 // 295 // Patch decodes: 296 // If first <non-white-space> is <double quote>, parse filename from cstring. 297 // If there is a <tab> after the first <non-white-space>, filename is 298 // [first <non-white-space>, the next run of <white-space> with an embedded <tab>). 299 // Otherwise the filename is [first <non-space>, the next <white-space>). 300 // 301 // The filename /dev/null means the file does not exist (used in adds and deletes). 302 303 // Considering the above, skimagediff will consider the contents of a -L parameter as 304 // <filename>(\t<specifier>)? 305 SkString outputFile; 306 307 if (baseLabel.isEmpty()) { 308 baseLabel.set(baseFile); 309 outputFile = baseLabel; 310 } else { 311 const char* baseLabelCstr = baseLabel.c_str(); 312 const char* tab = strchr(baseLabelCstr, '\t'); 313 if (nullptr == tab) { 314 outputFile = baseLabel; 315 } else { 316 outputFile.set(baseLabelCstr, tab - baseLabelCstr); 317 } 318 } 319 if (comparisonLabel.isEmpty()) { 320 comparisonLabel.set(comparisonFile); 321 } 322 printf("Base: %s\n", baseLabel.c_str()); 323 printf("Comparison: %s\n", comparisonLabel.c_str()); 324 325 DiffRecord dr; 326 create_diff_images(diffProc, colorThreshold, baseFile, comparisonFile, outputDir, outputFile, 327 &dr); 328 329 if (DiffResource::isStatusFailed(dr.fBase.fStatus)) { 330 printf("Base %s.\n", DiffResource::getStatusDescription(dr.fBase.fStatus)); 331 } 332 if (DiffResource::isStatusFailed(dr.fComparison.fStatus)) { 333 printf("Comparison %s.\n", DiffResource::getStatusDescription(dr.fComparison.fStatus)); 334 } 335 printf("Base and Comparison %s.\n", DiffRecord::getResultDescription(dr.fResult)); 336 337 if (DiffRecord::kDifferentPixels_Result == dr.fResult) { 338 printf("%.4f%% of pixels differ", 100 * dr.fFractionDifference); 339 printf(" (%.4f%% weighted)", 100 * dr.fWeightedFraction); 340 if (dr.fFractionDifference < 0.01) { 341 printf(" %d pixels", static_cast<int>(dr.fFractionDifference * 342 dr.fBase.fBitmap.width() * 343 dr.fBase.fBitmap.height())); 344 } 345 346 printf("\nAverage color mismatch: "); 347 printf("%d", static_cast<int>(MAX3(dr.fAverageMismatchR, 348 dr.fAverageMismatchG, 349 dr.fAverageMismatchB))); 350 printf("\nMax color mismatch: "); 351 printf("%d", MAX3(dr.fMaxMismatchR, 352 dr.fMaxMismatchG, 353 dr.fMaxMismatchB)); 354 printf("\n"); 355 } 356 printf("\n"); 357 358 int num_failing_results = 0; 359 if (failOnResultType[dr.fResult]) { 360 ++num_failing_results; 361 } 362 if (failOnStatusType[dr.fBase.fStatus][dr.fComparison.fStatus]) { 363 ++num_failing_results; 364 } 365 366 return num_failing_results; 367 } 368