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 "SkStream.h" 13 #include "SkTDict.h" 14 #include "SkThreadPool.h" 15 16 #include "SkDiffContext.h" 17 #include "skpdiff_util.h" 18 19 SkDiffContext::SkDiffContext() { 20 fDiffers = NULL; 21 fDifferCount = 0; 22 fThreadCount = SkThreadPool::kThreadPerCore; 23 } 24 25 SkDiffContext::~SkDiffContext() { 26 if (NULL != fDiffers) { 27 SkDELETE_ARRAY(fDiffers); 28 } 29 } 30 31 void SkDiffContext::setDifferenceDir(const SkString& path) { 32 if (!path.isEmpty() && sk_mkdir(path.c_str())) { 33 fDifferenceDir = path; 34 } 35 } 36 37 void SkDiffContext::setDiffers(const SkTDArray<SkImageDiffer*>& differs) { 38 // Delete whatever the last array of differs was 39 if (NULL != fDiffers) { 40 SkDELETE_ARRAY(fDiffers); 41 fDiffers = NULL; 42 fDifferCount = 0; 43 } 44 45 // Copy over the new differs 46 fDifferCount = differs.count(); 47 fDiffers = SkNEW_ARRAY(SkImageDiffer*, fDifferCount); 48 differs.copy(fDiffers); 49 } 50 51 static SkString get_common_prefix(const SkString& a, const SkString& b) { 52 const size_t maxPrefixLength = SkTMin(a.size(), b.size()); 53 SkASSERT(maxPrefixLength > 0); 54 for (size_t x = 0; x < maxPrefixLength; ++x) { 55 if (a[x] != b[x]) { 56 SkString result; 57 result.set(a.c_str(), x); 58 return result; 59 } 60 } 61 if (a.size() > b.size()) { 62 return b; 63 } else { 64 return a; 65 } 66 } 67 68 void SkDiffContext::addDiff(const char* baselinePath, const char* testPath) { 69 // Load the images at the paths 70 SkBitmap baselineBitmap; 71 SkBitmap testBitmap; 72 if (!SkImageDecoder::DecodeFile(baselinePath, &baselineBitmap)) { 73 SkDebugf("Failed to load bitmap \"%s\"\n", baselinePath); 74 return; 75 } 76 if (!SkImageDecoder::DecodeFile(testPath, &testBitmap)) { 77 SkDebugf("Failed to load bitmap \"%s\"\n", testPath); 78 return; 79 } 80 81 // Setup a record for this diff 82 fRecordMutex.acquire(); 83 DiffRecord* newRecord = fRecords.addToHead(DiffRecord()); 84 fRecordMutex.release(); 85 86 // compute the common name 87 SkString baseName = SkOSPath::SkBasename(baselinePath); 88 SkString testName = SkOSPath::SkBasename(testPath); 89 newRecord->fCommonName = get_common_prefix(baseName, testName); 90 91 newRecord->fBaselinePath = baselinePath; 92 newRecord->fTestPath = testPath; 93 94 bool alphaMaskPending = false; 95 96 // only enable alpha masks if a difference dir has been provided 97 if (!fDifferenceDir.isEmpty()) { 98 alphaMaskPending = true; 99 } 100 101 // Perform each diff 102 for (int differIndex = 0; differIndex < fDifferCount; differIndex++) { 103 SkImageDiffer* differ = fDiffers[differIndex]; 104 105 // Copy the results into data for this record 106 DiffData& diffData = newRecord->fDiffs.push_back(); 107 diffData.fDiffName = differ->getName(); 108 109 if (!differ->diff(&baselineBitmap, &testBitmap, alphaMaskPending, &diffData.fResult)) { 110 // if the diff failed the remove its entry from the list 111 newRecord->fDiffs.pop_back(); 112 continue; 113 } 114 115 if (alphaMaskPending 116 && SkImageDiffer::RESULT_CORRECT != diffData.fResult.result 117 && !diffData.fResult.poiAlphaMask.empty() 118 && !newRecord->fCommonName.isEmpty()) { 119 120 newRecord->fDifferencePath = SkOSPath::SkPathJoin(fDifferenceDir.c_str(), 121 newRecord->fCommonName.c_str()); 122 123 // compute the image diff and output it 124 SkBitmap copy; 125 diffData.fResult.poiAlphaMask.copyTo(©, SkBitmap::kARGB_8888_Config); 126 SkImageEncoder::EncodeFile(newRecord->fDifferencePath.c_str(), copy, 127 SkImageEncoder::kPNG_Type, 100); 128 129 // cleanup the existing bitmap to free up resources; 130 diffData.fResult.poiAlphaMask.reset(); 131 132 alphaMaskPending = false; 133 } 134 } 135 } 136 137 class SkThreadedDiff : public SkRunnable { 138 public: 139 SkThreadedDiff() : fDiffContext(NULL) { } 140 141 void setup(SkDiffContext* diffContext, const SkString& baselinePath, const SkString& testPath) { 142 fDiffContext = diffContext; 143 fBaselinePath = baselinePath; 144 fTestPath = testPath; 145 } 146 147 virtual void run() SK_OVERRIDE { 148 fDiffContext->addDiff(fBaselinePath.c_str(), fTestPath.c_str()); 149 } 150 151 private: 152 SkDiffContext* fDiffContext; 153 SkString fBaselinePath; 154 SkString fTestPath; 155 }; 156 157 void SkDiffContext::diffDirectories(const char baselinePath[], const char testPath[]) { 158 // Get the files in the baseline, we will then look for those inside the test path 159 SkTArray<SkString> baselineEntries; 160 if (!get_directory(baselinePath, &baselineEntries)) { 161 SkDebugf("Unable to open path \"%s\"\n", baselinePath); 162 return; 163 } 164 165 SkThreadPool threadPool(fThreadCount); 166 SkTArray<SkThreadedDiff> runnableDiffs; 167 runnableDiffs.reset(baselineEntries.count()); 168 169 for (int x = 0; x < baselineEntries.count(); x++) { 170 const char* baseFilename = baselineEntries[x].c_str(); 171 172 // Find the real location of each file to compare 173 SkString baselineFile = SkOSPath::SkPathJoin(baselinePath, baseFilename); 174 SkString testFile = SkOSPath::SkPathJoin(testPath, baseFilename); 175 176 // Check that the test file exists and is a file 177 if (sk_exists(testFile.c_str()) && !sk_isdir(testFile.c_str())) { 178 // Queue up the comparison with the differ 179 runnableDiffs[x].setup(this, baselineFile, testFile); 180 threadPool.add(&runnableDiffs[x]); 181 } else { 182 SkDebugf("Baseline file \"%s\" has no corresponding test file\n", baselineFile.c_str()); 183 } 184 } 185 186 threadPool.wait(); 187 } 188 189 190 void SkDiffContext::diffPatterns(const char baselinePattern[], const char testPattern[]) { 191 // Get the files in the baseline and test patterns. Because they are in sorted order, it's easy 192 // to find corresponding images by matching entry indices. 193 194 SkTArray<SkString> baselineEntries; 195 if (!glob_files(baselinePattern, &baselineEntries)) { 196 SkDebugf("Unable to get pattern \"%s\"\n", baselinePattern); 197 return; 198 } 199 200 SkTArray<SkString> testEntries; 201 if (!glob_files(testPattern, &testEntries)) { 202 SkDebugf("Unable to get pattern \"%s\"\n", testPattern); 203 return; 204 } 205 206 if (baselineEntries.count() != testEntries.count()) { 207 SkDebugf("Baseline and test patterns do not yield corresponding number of files\n"); 208 return; 209 } 210 211 SkThreadPool threadPool(fThreadCount); 212 SkTArray<SkThreadedDiff> runnableDiffs; 213 runnableDiffs.reset(baselineEntries.count()); 214 215 for (int x = 0; x < baselineEntries.count(); x++) { 216 runnableDiffs[x].setup(this, baselineEntries[x], testEntries[x]); 217 threadPool.add(&runnableDiffs[x]); 218 } 219 220 threadPool.wait(); 221 } 222 223 void SkDiffContext::outputRecords(SkWStream& stream, bool useJSONP) { 224 SkTLList<DiffRecord>::Iter iter(fRecords, SkTLList<DiffRecord>::Iter::kHead_IterStart); 225 DiffRecord* currentRecord = iter.get(); 226 227 if (useJSONP) { 228 stream.writeText("var SkPDiffRecords = {\n"); 229 } else { 230 stream.writeText("{\n"); 231 } 232 stream.writeText(" \"records\": [\n"); 233 while (NULL != currentRecord) { 234 stream.writeText(" {\n"); 235 236 SkString differenceAbsPath = get_absolute_path(currentRecord->fDifferencePath); 237 SkString baselineAbsPath = get_absolute_path(currentRecord->fBaselinePath); 238 SkString testAbsPath = get_absolute_path(currentRecord->fTestPath); 239 240 stream.writeText(" \"commonName\": \""); 241 stream.writeText(currentRecord->fCommonName.c_str()); 242 stream.writeText("\",\n"); 243 244 stream.writeText(" \"differencePath\": \""); 245 stream.writeText(differenceAbsPath.c_str()); 246 stream.writeText("\",\n"); 247 248 stream.writeText(" \"baselinePath\": \""); 249 stream.writeText(baselineAbsPath.c_str()); 250 stream.writeText("\",\n"); 251 252 stream.writeText(" \"testPath\": \""); 253 stream.writeText(testAbsPath.c_str()); 254 stream.writeText("\",\n"); 255 256 stream.writeText(" \"diffs\": [\n"); 257 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) { 258 DiffData& data = currentRecord->fDiffs[diffIndex]; 259 stream.writeText(" {\n"); 260 261 stream.writeText(" \"differName\": \""); 262 stream.writeText(data.fDiffName); 263 stream.writeText("\",\n"); 264 265 stream.writeText(" \"result\": "); 266 stream.writeScalarAsText((SkScalar)data.fResult.result); 267 stream.writeText(",\n"); 268 269 stream.writeText(" \"pointsOfInterest\": "); 270 stream.writeDecAsText(data.fResult.poiCount); 271 stream.writeText("\n"); 272 273 stream.writeText(" }"); 274 275 // JSON does not allow trailing commas 276 if (diffIndex + 1 < currentRecord->fDiffs.count()) { 277 stream.writeText(","); 278 } 279 stream.writeText(" \n"); 280 } 281 stream.writeText(" ]\n"); 282 283 stream.writeText(" }"); 284 285 currentRecord = iter.next(); 286 287 // JSON does not allow trailing commas 288 if (NULL != currentRecord) { 289 stream.writeText(","); 290 } 291 stream.writeText("\n"); 292 } 293 stream.writeText(" ]\n"); 294 if (useJSONP) { 295 stream.writeText("};\n"); 296 } else { 297 stream.writeText("}\n"); 298 } 299 } 300 301 void SkDiffContext::outputCsv(SkWStream& stream) { 302 SkTDict<int> columns(2); 303 int cntColumns = 0; 304 305 stream.writeText("key"); 306 307 SkTLList<DiffRecord>::Iter iter(fRecords, SkTLList<DiffRecord>::Iter::kHead_IterStart); 308 DiffRecord* currentRecord = iter.get(); 309 310 // Write CSV header and create a dictionary of all columns. 311 while (NULL != currentRecord) { 312 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) { 313 DiffData& data = currentRecord->fDiffs[diffIndex]; 314 if (!columns.find(data.fDiffName)) { 315 columns.set(data.fDiffName, cntColumns); 316 stream.writeText(", "); 317 stream.writeText(data.fDiffName); 318 cntColumns++; 319 } 320 } 321 currentRecord = iter.next(); 322 } 323 stream.writeText("\n"); 324 325 double values[100]; 326 SkASSERT(cntColumns < 100); // Make the array larger, if we ever have so many diff types. 327 328 SkTLList<DiffRecord>::Iter iter2(fRecords, SkTLList<DiffRecord>::Iter::kHead_IterStart); 329 currentRecord = iter2.get(); 330 while (NULL != currentRecord) { 331 for (int i = 0; i < cntColumns; i++) { 332 values[i] = -1; 333 } 334 335 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) { 336 DiffData& data = currentRecord->fDiffs[diffIndex]; 337 int index = -1; 338 SkAssertResult(columns.find(data.fDiffName, &index)); 339 SkASSERT(index >= 0 && index < cntColumns); 340 values[index] = data.fResult.result; 341 } 342 343 const char* filename = currentRecord->fBaselinePath.c_str() + 344 strlen(currentRecord->fBaselinePath.c_str()) - 1; 345 while (filename > currentRecord->fBaselinePath.c_str() && *(filename - 1) != '/') { 346 filename--; 347 } 348 349 stream.writeText(filename); 350 351 for (int i = 0; i < cntColumns; i++) { 352 SkString str; 353 str.printf(", %f", values[i]); 354 stream.writeText(str.c_str()); 355 } 356 stream.writeText("\n"); 357 358 currentRecord = iter2.next(); 359 } 360 } 361