| OLD | NEW |
| (Empty) |
| 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 = nullptr; | |
| 26 fDifferCount = 0; | |
| 27 } | |
| 28 | |
| 29 SkDiffContext::~SkDiffContext() { | |
| 30 if (fDiffers) { | |
| 31 delete[] 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 delete[] fDiffers; | |
| 61 fDiffers = nullptr; | |
| 62 fDifferCount = 0; | |
| 63 } | |
| 64 | |
| 65 // Copy over the new differs | |
| 66 fDifferCount = differs.count(); | |
| 67 fDiffers = new 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.heig
ht()); | |
| 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, &diffDa
ta.fResult)) { | |
| 146 // if the diff failed, record -1 as the result | |
| 147 // TODO(djsollen): Record more detailed information about exactly wh
at failed. | |
| 148 // (Image dimension mismatch? etc.) See https://bug.skia.org/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 https://bug.skia.org/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_st
r()); | |
| 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 void SkDiffContext::diffDirectories(const char baselinePath[], const char testPa
th[]) { | |
| 213 // Get the files in the baseline, we will then look for those inside the tes
t path | |
| 214 SkTArray<SkString> baselineEntries; | |
| 215 if (!get_directory(baselinePath, &baselineEntries)) { | |
| 216 SkDebugf("Unable to open path \"%s\"\n", baselinePath); | |
| 217 return; | |
| 218 } | |
| 219 | |
| 220 sk_parallel_for(baselineEntries.count(), [&](int i) { | |
| 221 const char* baseFilename = baselineEntries[i].c_str(); | |
| 222 | |
| 223 // Find the real location of each file to compare | |
| 224 SkString baselineFile = SkOSPath::Join(baselinePath, baseFilename); | |
| 225 SkString testFile = SkOSPath::Join(testPath, baseFilename); | |
| 226 | |
| 227 // Check that the test file exists and is a file | |
| 228 if (sk_exists(testFile.c_str()) && !sk_isdir(testFile.c_str())) { | |
| 229 this->addDiff(baselineFile.c_str(), testFile.c_str()); | |
| 230 } else { | |
| 231 SkDebugf("Baseline file \"%s\" has no corresponding test file\n", ba
selineFile.c_str()); | |
| 232 } | |
| 233 }); | |
| 234 } | |
| 235 | |
| 236 | |
| 237 void SkDiffContext::diffPatterns(const char baselinePattern[], const char testPa
ttern[]) { | |
| 238 // Get the files in the baseline and test patterns. Because they are in sort
ed order, it's easy | |
| 239 // to find corresponding images by matching entry indices. | |
| 240 | |
| 241 SkTArray<SkString> baselineEntries; | |
| 242 if (!glob_files(baselinePattern, &baselineEntries)) { | |
| 243 SkDebugf("Unable to get pattern \"%s\"\n", baselinePattern); | |
| 244 return; | |
| 245 } | |
| 246 | |
| 247 SkTArray<SkString> testEntries; | |
| 248 if (!glob_files(testPattern, &testEntries)) { | |
| 249 SkDebugf("Unable to get pattern \"%s\"\n", testPattern); | |
| 250 return; | |
| 251 } | |
| 252 | |
| 253 if (baselineEntries.count() != testEntries.count()) { | |
| 254 SkDebugf("Baseline and test patterns do not yield corresponding number o
f files\n"); | |
| 255 return; | |
| 256 } | |
| 257 | |
| 258 sk_parallel_for(baselineEntries.count(), [&](int i) { | |
| 259 this->addDiff(baselineEntries[i].c_str(), testEntries[i].c_str()); | |
| 260 }); | |
| 261 } | |
| 262 | |
| 263 void SkDiffContext::outputRecords(SkWStream& stream, bool useJSONP) { | |
| 264 RecordList::Iter iter(fRecords, RecordList::Iter::kHead_IterStart); | |
| 265 DiffRecord* currentRecord = iter.get(); | |
| 266 | |
| 267 if (useJSONP) { | |
| 268 stream.writeText("var SkPDiffRecords = {\n"); | |
| 269 } else { | |
| 270 stream.writeText("{\n"); | |
| 271 } | |
| 272 | |
| 273 // TODO(djsollen): Would it be better to use the jsoncpp library to write ou
t the JSON? | |
| 274 // This manual approach is probably more efficient, but it sure is ugly. | |
| 275 // See https://bug.skia.org/2713 ('make skpdiff use jsoncpp library to write
out | |
| 276 // JSON output, instead of manual writeText() calls?') | |
| 277 stream.writeText(" \"records\": [\n"); | |
| 278 while (currentRecord) { | |
| 279 stream.writeText(" {\n"); | |
| 280 | |
| 281 SkString baselineAbsPath = get_absolute_path(currentRecord->fBaselin
ePath); | |
| 282 SkString testAbsPath = get_absolute_path(currentRecord->fTestPath); | |
| 283 | |
| 284 stream.writeText(" \"commonName\": \""); | |
| 285 stream.writeText(currentRecord->fCommonName.c_str()); | |
| 286 stream.writeText("\",\n"); | |
| 287 | |
| 288 stream.writeText(" \"differencePath\": \""); | |
| 289 stream.writeText(get_absolute_path(currentRecord->fAlphaMaskPath).c_
str()); | |
| 290 stream.writeText("\",\n"); | |
| 291 | |
| 292 stream.writeText(" \"rgbDiffPath\": \""); | |
| 293 stream.writeText(get_absolute_path(currentRecord->fRgbDiffPath).c_st
r()); | |
| 294 stream.writeText("\",\n"); | |
| 295 | |
| 296 stream.writeText(" \"whiteDiffPath\": \""); | |
| 297 stream.writeText(get_absolute_path(currentRecord->fWhiteDiffPath).c_
str()); | |
| 298 stream.writeText("\",\n"); | |
| 299 | |
| 300 stream.writeText(" \"baselinePath\": \""); | |
| 301 stream.writeText(baselineAbsPath.c_str()); | |
| 302 stream.writeText("\",\n"); | |
| 303 | |
| 304 stream.writeText(" \"testPath\": \""); | |
| 305 stream.writeText(testAbsPath.c_str()); | |
| 306 stream.writeText("\",\n"); | |
| 307 | |
| 308 stream.writeText(" \"width\": "); | |
| 309 stream.writeDecAsText(currentRecord->fSize.width()); | |
| 310 stream.writeText(",\n"); | |
| 311 stream.writeText(" \"height\": "); | |
| 312 stream.writeDecAsText(currentRecord->fSize.height()); | |
| 313 stream.writeText(",\n"); | |
| 314 | |
| 315 stream.writeText(" \"maxRedDiff\": "); | |
| 316 stream.writeDecAsText(currentRecord->fMaxRedDiff); | |
| 317 stream.writeText(",\n"); | |
| 318 stream.writeText(" \"maxGreenDiff\": "); | |
| 319 stream.writeDecAsText(currentRecord->fMaxGreenDiff); | |
| 320 stream.writeText(",\n"); | |
| 321 stream.writeText(" \"maxBlueDiff\": "); | |
| 322 stream.writeDecAsText(currentRecord->fMaxBlueDiff); | |
| 323 stream.writeText(",\n"); | |
| 324 | |
| 325 stream.writeText(" \"diffs\": [\n"); | |
| 326 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); d
iffIndex++) { | |
| 327 DiffData& data = currentRecord->fDiffs[diffIndex]; | |
| 328 stream.writeText(" {\n"); | |
| 329 | |
| 330 stream.writeText(" \"differName\": \""); | |
| 331 stream.writeText(data.fDiffName); | |
| 332 stream.writeText("\",\n"); | |
| 333 | |
| 334 stream.writeText(" \"result\": "); | |
| 335 stream.writeScalarAsText((SkScalar)data.fResult.result); | |
| 336 stream.writeText(",\n"); | |
| 337 | |
| 338 stream.writeText(" \"pointsOfInterest\":
"); | |
| 339 stream.writeDecAsText(data.fResult.poiCount); | |
| 340 stream.writeText("\n"); | |
| 341 | |
| 342 stream.writeText(" }"); | |
| 343 | |
| 344 // JSON does not allow trailing commas | |
| 345 if (diffIndex + 1 < currentRecord->fDiffs.count()) { | |
| 346 stream.writeText(","); | |
| 347 } | |
| 348 stream.writeText(" \n"); | |
| 349 } | |
| 350 stream.writeText(" ]\n"); | |
| 351 | |
| 352 stream.writeText(" }"); | |
| 353 | |
| 354 currentRecord = iter.next(); | |
| 355 | |
| 356 // JSON does not allow trailing commas | |
| 357 if (currentRecord) { | |
| 358 stream.writeText(","); | |
| 359 } | |
| 360 stream.writeText("\n"); | |
| 361 } | |
| 362 stream.writeText(" ]\n"); | |
| 363 if (useJSONP) { | |
| 364 stream.writeText("};\n"); | |
| 365 } else { | |
| 366 stream.writeText("}\n"); | |
| 367 } | |
| 368 } | |
| 369 | |
| 370 void SkDiffContext::outputCsv(SkWStream& stream) { | |
| 371 SkTDict<int> columns(2); | |
| 372 int cntColumns = 0; | |
| 373 | |
| 374 stream.writeText("key"); | |
| 375 | |
| 376 RecordList::Iter iter(fRecords, RecordList::Iter::kHead_IterStart); | |
| 377 DiffRecord* currentRecord = iter.get(); | |
| 378 | |
| 379 // Write CSV header and create a dictionary of all columns. | |
| 380 while (currentRecord) { | |
| 381 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffI
ndex++) { | |
| 382 DiffData& data = currentRecord->fDiffs[diffIndex]; | |
| 383 if (!columns.find(data.fDiffName)) { | |
| 384 columns.set(data.fDiffName, cntColumns); | |
| 385 stream.writeText(", "); | |
| 386 stream.writeText(data.fDiffName); | |
| 387 cntColumns++; | |
| 388 } | |
| 389 } | |
| 390 currentRecord = iter.next(); | |
| 391 } | |
| 392 stream.writeText("\n"); | |
| 393 | |
| 394 double values[100]; | |
| 395 SkASSERT(cntColumns < 100); // Make the array larger, if we ever have so ma
ny diff types. | |
| 396 | |
| 397 RecordList::Iter iter2(fRecords, RecordList::Iter::kHead_IterStart); | |
| 398 currentRecord = iter2.get(); | |
| 399 while (currentRecord) { | |
| 400 for (int i = 0; i < cntColumns; i++) { | |
| 401 values[i] = -1; | |
| 402 } | |
| 403 | |
| 404 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffI
ndex++) { | |
| 405 DiffData& data = currentRecord->fDiffs[diffIndex]; | |
| 406 int index = -1; | |
| 407 SkAssertResult(columns.find(data.fDiffName, &index)); | |
| 408 SkASSERT(index >= 0 && index < cntColumns); | |
| 409 values[index] = data.fResult.result; | |
| 410 } | |
| 411 | |
| 412 const char* filename = currentRecord->fBaselinePath.c_str() + | |
| 413 strlen(currentRecord->fBaselinePath.c_str()) - 1; | |
| 414 while (filename > currentRecord->fBaselinePath.c_str() && *(filename - 1
) != '/') { | |
| 415 filename--; | |
| 416 } | |
| 417 | |
| 418 stream.writeText(filename); | |
| 419 | |
| 420 for (int i = 0; i < cntColumns; i++) { | |
| 421 SkString str; | |
| 422 str.printf(", %f", values[i]); | |
| 423 stream.writeText(str.c_str()); | |
| 424 } | |
| 425 stream.writeText("\n"); | |
| 426 | |
| 427 currentRecord = iter2.next(); | |
| 428 } | |
| 429 } | |
| OLD | NEW |