1 /* 2 * Copyright 2013 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 8 #include "SkBitmap.h" 9 #include "SkImageDecoder.h" 10 #include "SkOSFile.h" 11 #include "SkRunnable.h" 12 #include "SkSize.h" 13 #include "SkStream.h" 14 #include "SkTDict.h" 15 #include "SkTaskGroup.h" 16 17 // from the tools directory for replace_char(...) 18 #include "picture_utils.h" 19 20 #include "SkDiffContext.h" 21 #include "SkImageDiffer.h" 22 #include "skpdiff_util.h" 23 24 SkDiffContext::SkDiffContext() { 25 fDiffers = NULL; 26 fDifferCount = 0; 27 } 28 29 SkDiffContext::~SkDiffContext() { 30 if (fDiffers) { 31 SkDELETE_ARRAY(fDiffers); 32 } 33 } 34 35 void SkDiffContext::setAlphaMaskDir(const SkString& path) { 36 if (!path.isEmpty() && sk_mkdir(path.c_str())) { 37 fAlphaMaskDir = path; 38 } 39 } 40 41 void SkDiffContext::setRgbDiffDir(const SkString& path) { 42 if (!path.isEmpty() && sk_mkdir(path.c_str())) { 43 fRgbDiffDir = path; 44 } 45 } 46 47 void SkDiffContext::setWhiteDiffDir(const SkString& path) { 48 if (!path.isEmpty() && sk_mkdir(path.c_str())) { 49 fWhiteDiffDir = path; 50 } 51 } 52 53 void SkDiffContext::setLongNames(const bool useLongNames) { 54 longNames = useLongNames; 55 } 56 57 void SkDiffContext::setDiffers(const SkTDArray<SkImageDiffer*>& differs) { 58 // Delete whatever the last array of differs was 59 if (fDiffers) { 60 SkDELETE_ARRAY(fDiffers); 61 fDiffers = NULL; 62 fDifferCount = 0; 63 } 64 65 // Copy over the new differs 66 fDifferCount = differs.count(); 67 fDiffers = SkNEW_ARRAY(SkImageDiffer*, fDifferCount); 68 differs.copy(fDiffers); 69 } 70 71 static SkString get_common_prefix(const SkString& a, const SkString& b) { 72 const size_t maxPrefixLength = SkTMin(a.size(), b.size()); 73 SkASSERT(maxPrefixLength > 0); 74 for (size_t x = 0; x < maxPrefixLength; ++x) { 75 if (a[x] != b[x]) { 76 SkString result; 77 result.set(a.c_str(), x); 78 return result; 79 } 80 } 81 if (a.size() > b.size()) { 82 return b; 83 } else { 84 return a; 85 } 86 } 87 88 static SkString get_combined_name(const SkString& a, const SkString& b) { 89 // Note (stephana): We must keep this function in sync with 90 // getImageDiffRelativeUrl() in static/loader.js (under rebaseline_server). 91 SkString result = a; 92 result.append("-vs-"); 93 result.append(b); 94 sk_tools::replace_char(&result, '.', '_'); 95 return result; 96 } 97 98 void SkDiffContext::addDiff(const char* baselinePath, const char* testPath) { 99 // Load the images at the paths 100 SkBitmap baselineBitmap; 101 SkBitmap testBitmap; 102 if (!SkImageDecoder::DecodeFile(baselinePath, &baselineBitmap)) { 103 SkDebugf("Failed to load bitmap \"%s\"\n", baselinePath); 104 return; 105 } 106 if (!SkImageDecoder::DecodeFile(testPath, &testBitmap)) { 107 SkDebugf("Failed to load bitmap \"%s\"\n", testPath); 108 return; 109 } 110 111 // Setup a record for this diff 112 fRecordMutex.acquire(); 113 DiffRecord* newRecord = fRecords.addToHead(DiffRecord()); 114 fRecordMutex.release(); 115 116 // compute the common name 117 SkString baseName = SkOSPath::Basename(baselinePath); 118 SkString testName = SkOSPath::Basename(testPath); 119 120 if (longNames) { 121 newRecord->fCommonName = get_combined_name(baseName, testName); 122 } else { 123 newRecord->fCommonName = get_common_prefix(baseName, testName); 124 } 125 newRecord->fCommonName.append(".png"); 126 127 newRecord->fBaselinePath = baselinePath; 128 newRecord->fTestPath = testPath; 129 newRecord->fSize = SkISize::Make(baselineBitmap.width(), baselineBitmap.height()); 130 131 // only generate diff images if we have a place to store them 132 SkImageDiffer::BitmapsToCreate bitmapsToCreate; 133 bitmapsToCreate.alphaMask = !fAlphaMaskDir.isEmpty(); 134 bitmapsToCreate.rgbDiff = !fRgbDiffDir.isEmpty(); 135 bitmapsToCreate.whiteDiff = !fWhiteDiffDir.isEmpty(); 136 137 // Perform each diff 138 for (int differIndex = 0; differIndex < fDifferCount; differIndex++) { 139 SkImageDiffer* differ = fDiffers[differIndex]; 140 141 // Copy the results into data for this record 142 DiffData& diffData = newRecord->fDiffs.push_back(); 143 diffData.fDiffName = differ->getName(); 144 145 if (!differ->diff(&baselineBitmap, &testBitmap, bitmapsToCreate, &diffData.fResult)) { 146 // if the diff failed, record -1 as the result 147 // TODO(djsollen): Record more detailed information about exactly what failed. 148 // (Image dimension mismatch? etc.) See http://skbug.com/2710 ('make skpdiff 149 // report more detail when it fails to compare two images') 150 diffData.fResult.result = -1; 151 continue; 152 } 153 154 if (bitmapsToCreate.alphaMask 155 && SkImageDiffer::RESULT_CORRECT != diffData.fResult.result 156 && !diffData.fResult.poiAlphaMask.empty() 157 && !newRecord->fCommonName.isEmpty()) { 158 159 newRecord->fAlphaMaskPath = SkOSPath::Join(fAlphaMaskDir.c_str(), 160 newRecord->fCommonName.c_str()); 161 162 // compute the image diff and output it 163 SkBitmap copy; 164 diffData.fResult.poiAlphaMask.copyTo(©, kN32_SkColorType); 165 SkImageEncoder::EncodeFile(newRecord->fAlphaMaskPath.c_str(), copy, 166 SkImageEncoder::kPNG_Type, 100); 167 168 // cleanup the existing bitmap to free up resources; 169 diffData.fResult.poiAlphaMask.reset(); 170 171 bitmapsToCreate.alphaMask = false; 172 } 173 174 if (bitmapsToCreate.rgbDiff 175 && SkImageDiffer::RESULT_CORRECT != diffData.fResult.result 176 && !diffData.fResult.rgbDiffBitmap.empty() 177 && !newRecord->fCommonName.isEmpty()) { 178 // TODO(djsollen): Rather than taking the max r/g/b diffs that come back from 179 // a particular differ and storing them as toplevel fields within 180 // newRecord, we should extend outputRecords() to report optional 181 // fields for each differ (not just "result" and "pointsOfInterest"). 182 // See http://skbug.com/2712 ('allow skpdiff to report different sets 183 // of result fields for different comparison algorithms') 184 newRecord->fMaxRedDiff = diffData.fResult.maxRedDiff; 185 newRecord->fMaxGreenDiff = diffData.fResult.maxGreenDiff; 186 newRecord->fMaxBlueDiff = diffData.fResult.maxBlueDiff; 187 188 newRecord->fRgbDiffPath = SkOSPath::Join(fRgbDiffDir.c_str(), 189 newRecord->fCommonName.c_str()); 190 SkImageEncoder::EncodeFile(newRecord->fRgbDiffPath.c_str(), 191 diffData.fResult.rgbDiffBitmap, 192 SkImageEncoder::kPNG_Type, 100); 193 diffData.fResult.rgbDiffBitmap.reset(); 194 bitmapsToCreate.rgbDiff = false; 195 } 196 197 if (bitmapsToCreate.whiteDiff 198 && SkImageDiffer::RESULT_CORRECT != diffData.fResult.result 199 && !diffData.fResult.whiteDiffBitmap.empty() 200 && !newRecord->fCommonName.isEmpty()) { 201 newRecord->fWhiteDiffPath = SkOSPath::Join(fWhiteDiffDir.c_str(), 202 newRecord->fCommonName.c_str()); 203 SkImageEncoder::EncodeFile(newRecord->fWhiteDiffPath.c_str(), 204 diffData.fResult.whiteDiffBitmap, 205 SkImageEncoder::kPNG_Type, 100); 206 diffData.fResult.whiteDiffBitmap.reset(); 207 bitmapsToCreate.whiteDiff = false; 208 } 209 } 210 } 211 212 class SkThreadedDiff : public SkRunnable { 213 public: 214 SkThreadedDiff() : fDiffContext(NULL) { } 215 216 void setup(SkDiffContext* diffContext, const SkString& baselinePath, const SkString& testPath) { 217 fDiffContext = diffContext; 218 fBaselinePath = baselinePath; 219 fTestPath = testPath; 220 } 221 222 virtual void run() SK_OVERRIDE { 223 fDiffContext->addDiff(fBaselinePath.c_str(), fTestPath.c_str()); 224 } 225 226 private: 227 SkDiffContext* fDiffContext; 228 SkString fBaselinePath; 229 SkString fTestPath; 230 }; 231 232 void SkDiffContext::diffDirectories(const char baselinePath[], const char testPath[]) { 233 // Get the files in the baseline, we will then look for those inside the test path 234 SkTArray<SkString> baselineEntries; 235 if (!get_directory(baselinePath, &baselineEntries)) { 236 SkDebugf("Unable to open path \"%s\"\n", baselinePath); 237 return; 238 } 239 240 SkTaskGroup tg; 241 SkTArray<SkThreadedDiff> runnableDiffs; 242 runnableDiffs.reset(baselineEntries.count()); 243 244 for (int x = 0; x < baselineEntries.count(); x++) { 245 const char* baseFilename = baselineEntries[x].c_str(); 246 247 // Find the real location of each file to compare 248 SkString baselineFile = SkOSPath::Join(baselinePath, baseFilename); 249 SkString testFile = SkOSPath::Join(testPath, baseFilename); 250 251 // Check that the test file exists and is a file 252 if (sk_exists(testFile.c_str()) && !sk_isdir(testFile.c_str())) { 253 // Queue up the comparison with the differ 254 runnableDiffs[x].setup(this, baselineFile, testFile); 255 tg.add(&runnableDiffs[x]); 256 } else { 257 SkDebugf("Baseline file \"%s\" has no corresponding test file\n", baselineFile.c_str()); 258 } 259 } 260 } 261 262 263 void SkDiffContext::diffPatterns(const char baselinePattern[], const char testPattern[]) { 264 // Get the files in the baseline and test patterns. Because they are in sorted order, it's easy 265 // to find corresponding images by matching entry indices. 266 267 SkTArray<SkString> baselineEntries; 268 if (!glob_files(baselinePattern, &baselineEntries)) { 269 SkDebugf("Unable to get pattern \"%s\"\n", baselinePattern); 270 return; 271 } 272 273 SkTArray<SkString> testEntries; 274 if (!glob_files(testPattern, &testEntries)) { 275 SkDebugf("Unable to get pattern \"%s\"\n", testPattern); 276 return; 277 } 278 279 if (baselineEntries.count() != testEntries.count()) { 280 SkDebugf("Baseline and test patterns do not yield corresponding number of files\n"); 281 return; 282 } 283 284 SkTaskGroup tg; 285 SkTArray<SkThreadedDiff> runnableDiffs; 286 runnableDiffs.reset(baselineEntries.count()); 287 288 for (int x = 0; x < baselineEntries.count(); x++) { 289 runnableDiffs[x].setup(this, baselineEntries[x], testEntries[x]); 290 tg.add(&runnableDiffs[x]); 291 } 292 tg.wait(); 293 } 294 295 void SkDiffContext::outputRecords(SkWStream& stream, bool useJSONP) { 296 SkTLList<DiffRecord>::Iter iter(fRecords, SkTLList<DiffRecord>::Iter::kHead_IterStart); 297 DiffRecord* currentRecord = iter.get(); 298 299 if (useJSONP) { 300 stream.writeText("var SkPDiffRecords = {\n"); 301 } else { 302 stream.writeText("{\n"); 303 } 304 305 // TODO(djsollen): Would it be better to use the jsoncpp library to write out the JSON? 306 // This manual approach is probably more efficient, but it sure is ugly. 307 // See http://skbug.com/2713 ('make skpdiff use jsoncpp library to write out 308 // JSON output, instead of manual writeText() calls?') 309 stream.writeText(" \"records\": [\n"); 310 while (currentRecord) { 311 stream.writeText(" {\n"); 312 313 SkString baselineAbsPath = get_absolute_path(currentRecord->fBaselinePath); 314 SkString testAbsPath = get_absolute_path(currentRecord->fTestPath); 315 316 stream.writeText(" \"commonName\": \""); 317 stream.writeText(currentRecord->fCommonName.c_str()); 318 stream.writeText("\",\n"); 319 320 stream.writeText(" \"differencePath\": \""); 321 stream.writeText(get_absolute_path(currentRecord->fAlphaMaskPath).c_str()); 322 stream.writeText("\",\n"); 323 324 stream.writeText(" \"rgbDiffPath\": \""); 325 stream.writeText(get_absolute_path(currentRecord->fRgbDiffPath).c_str()); 326 stream.writeText("\",\n"); 327 328 stream.writeText(" \"whiteDiffPath\": \""); 329 stream.writeText(get_absolute_path(currentRecord->fWhiteDiffPath).c_str()); 330 stream.writeText("\",\n"); 331 332 stream.writeText(" \"baselinePath\": \""); 333 stream.writeText(baselineAbsPath.c_str()); 334 stream.writeText("\",\n"); 335 336 stream.writeText(" \"testPath\": \""); 337 stream.writeText(testAbsPath.c_str()); 338 stream.writeText("\",\n"); 339 340 stream.writeText(" \"width\": "); 341 stream.writeDecAsText(currentRecord->fSize.width()); 342 stream.writeText(",\n"); 343 stream.writeText(" \"height\": "); 344 stream.writeDecAsText(currentRecord->fSize.height()); 345 stream.writeText(",\n"); 346 347 stream.writeText(" \"maxRedDiff\": "); 348 stream.writeDecAsText(currentRecord->fMaxRedDiff); 349 stream.writeText(",\n"); 350 stream.writeText(" \"maxGreenDiff\": "); 351 stream.writeDecAsText(currentRecord->fMaxGreenDiff); 352 stream.writeText(",\n"); 353 stream.writeText(" \"maxBlueDiff\": "); 354 stream.writeDecAsText(currentRecord->fMaxBlueDiff); 355 stream.writeText(",\n"); 356 357 stream.writeText(" \"diffs\": [\n"); 358 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) { 359 DiffData& data = currentRecord->fDiffs[diffIndex]; 360 stream.writeText(" {\n"); 361 362 stream.writeText(" \"differName\": \""); 363 stream.writeText(data.fDiffName); 364 stream.writeText("\",\n"); 365 366 stream.writeText(" \"result\": "); 367 stream.writeScalarAsText((SkScalar)data.fResult.result); 368 stream.writeText(",\n"); 369 370 stream.writeText(" \"pointsOfInterest\": "); 371 stream.writeDecAsText(data.fResult.poiCount); 372 stream.writeText("\n"); 373 374 stream.writeText(" }"); 375 376 // JSON does not allow trailing commas 377 if (diffIndex + 1 < currentRecord->fDiffs.count()) { 378 stream.writeText(","); 379 } 380 stream.writeText(" \n"); 381 } 382 stream.writeText(" ]\n"); 383 384 stream.writeText(" }"); 385 386 currentRecord = iter.next(); 387 388 // JSON does not allow trailing commas 389 if (currentRecord) { 390 stream.writeText(","); 391 } 392 stream.writeText("\n"); 393 } 394 stream.writeText(" ]\n"); 395 if (useJSONP) { 396 stream.writeText("};\n"); 397 } else { 398 stream.writeText("}\n"); 399 } 400 } 401 402 void SkDiffContext::outputCsv(SkWStream& stream) { 403 SkTDict<int> columns(2); 404 int cntColumns = 0; 405 406 stream.writeText("key"); 407 408 SkTLList<DiffRecord>::Iter iter(fRecords, SkTLList<DiffRecord>::Iter::kHead_IterStart); 409 DiffRecord* currentRecord = iter.get(); 410 411 // Write CSV header and create a dictionary of all columns. 412 while (currentRecord) { 413 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) { 414 DiffData& data = currentRecord->fDiffs[diffIndex]; 415 if (!columns.find(data.fDiffName)) { 416 columns.set(data.fDiffName, cntColumns); 417 stream.writeText(", "); 418 stream.writeText(data.fDiffName); 419 cntColumns++; 420 } 421 } 422 currentRecord = iter.next(); 423 } 424 stream.writeText("\n"); 425 426 double values[100]; 427 SkASSERT(cntColumns < 100); // Make the array larger, if we ever have so many diff types. 428 429 SkTLList<DiffRecord>::Iter iter2(fRecords, SkTLList<DiffRecord>::Iter::kHead_IterStart); 430 currentRecord = iter2.get(); 431 while (currentRecord) { 432 for (int i = 0; i < cntColumns; i++) { 433 values[i] = -1; 434 } 435 436 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) { 437 DiffData& data = currentRecord->fDiffs[diffIndex]; 438 int index = -1; 439 SkAssertResult(columns.find(data.fDiffName, &index)); 440 SkASSERT(index >= 0 && index < cntColumns); 441 values[index] = data.fResult.result; 442 } 443 444 const char* filename = currentRecord->fBaselinePath.c_str() + 445 strlen(currentRecord->fBaselinePath.c_str()) - 1; 446 while (filename > currentRecord->fBaselinePath.c_str() && *(filename - 1) != '/') { 447 filename--; 448 } 449 450 stream.writeText(filename); 451 452 for (int i = 0; i < cntColumns; i++) { 453 SkString str; 454 str.printf(", %f", values[i]); 455 stream.writeText(str.c_str()); 456 } 457 stream.writeText("\n"); 458 459 currentRecord = iter2.next(); 460 } 461 } 462