blob: ce1fad9d58ccabe1f46a3702d32793d22e65f1b0 [file] [log] [blame]
zachr@google.com945708a2013-07-02 19:55:32 +00001/*
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"
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +000011#include "SkRunnable.h"
zachr@google.com945708a2013-07-02 19:55:32 +000012#include "SkStream.h"
edisonn@google.comc93c8ac2013-07-22 15:24:26 +000013#include "SkTDict.h"
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +000014#include "SkThreadPool.h"
zachr@google.com945708a2013-07-02 19:55:32 +000015
16#include "SkDiffContext.h"
17#include "SkImageDiffer.h"
18#include "skpdiff_util.h"
19
zachr@google.comee0f46d2013-07-23 12:57:52 +000020// Truncates the number of points of interests in JSON output to not freeze the parser
21static const int kMaxPOI = 100;
22
zachr@google.com945708a2013-07-02 19:55:32 +000023SkDiffContext::SkDiffContext() {
24 fRecords = NULL;
25 fDiffers = NULL;
26 fDifferCount = 0;
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +000027 fThreadCount = SkThreadPool::kThreadPerCore;
zachr@google.com945708a2013-07-02 19:55:32 +000028}
29
30SkDiffContext::~SkDiffContext() {
31 // Delete the record linked list
32 DiffRecord* currentRecord = fRecords;
33 while (NULL != currentRecord) {
34 DiffRecord* nextRecord = currentRecord->fNext;
35 SkDELETE(currentRecord);
36 currentRecord = nextRecord;
37 }
38
39 if (NULL != fDiffers) {
40 SkDELETE_ARRAY(fDiffers);
41 }
42}
43
djsollen@google.com513a7bf2013-11-07 19:24:06 +000044void SkDiffContext::setDifferenceDir(const SkString& path) {
45 if (!path.isEmpty() && sk_mkdir(path.c_str())) {
46 fDifferenceDir = path;
47 }
48}
49
zachr@google.com945708a2013-07-02 19:55:32 +000050void SkDiffContext::setDiffers(const SkTDArray<SkImageDiffer*>& differs) {
51 // Delete whatever the last array of differs was
52 if (NULL != fDiffers) {
53 SkDELETE_ARRAY(fDiffers);
54 fDiffers = NULL;
55 fDifferCount = 0;
56 }
57
58 // Copy over the new differs
59 fDifferCount = differs.count();
60 fDiffers = SkNEW_ARRAY(SkImageDiffer*, fDifferCount);
61 differs.copy(fDiffers);
62}
63
djsollen@google.com513a7bf2013-11-07 19:24:06 +000064static SkString get_common_prefix(const SkString& a, const SkString& b) {
65 const size_t maxPrefixLength = SkTMin(a.size(), b.size());
66 SkASSERT(maxPrefixLength > 0);
67 for (size_t x = 0; x < maxPrefixLength; ++x) {
68 if (a[x] != b[x]) {
69 SkString result;
70 result.set(a.c_str(), x);
71 return result;
72 }
73 }
74 if (a.size() > b.size()) {
75 return b;
76 } else {
77 return a;
78 }
79}
80
zachr@google.com945708a2013-07-02 19:55:32 +000081void SkDiffContext::addDiff(const char* baselinePath, const char* testPath) {
82 // Load the images at the paths
83 SkBitmap baselineBitmap;
84 SkBitmap testBitmap;
85 if (!SkImageDecoder::DecodeFile(baselinePath, &baselineBitmap)) {
86 SkDebugf("Failed to load bitmap \"%s\"\n", baselinePath);
87 return;
88 }
89 if (!SkImageDecoder::DecodeFile(testPath, &testBitmap)) {
90 SkDebugf("Failed to load bitmap \"%s\"\n", testPath);
91 return;
92 }
93
94 // Setup a record for this diff
95 DiffRecord* newRecord = SkNEW(DiffRecord);
96 newRecord->fBaselinePath = baselinePath;
97 newRecord->fTestPath = testPath;
98 newRecord->fNext = fRecords;
99 fRecords = newRecord;
100
djsollen@google.com513a7bf2013-11-07 19:24:06 +0000101 // compute the common name
102 SkString baseName = SkOSPath::SkBasename(baselinePath);
103 SkString testName = SkOSPath::SkBasename(testPath);
104 newRecord->fCommonName = get_common_prefix(baseName, testName);
105
106 bool alphaMaskPending = false;
107 bool alphaMaskCreated = false;
108
zachr@google.com945708a2013-07-02 19:55:32 +0000109 // Perform each diff
110 for (int differIndex = 0; differIndex < fDifferCount; differIndex++) {
111 SkImageDiffer* differ = fDiffers[differIndex];
djsollen@google.com513a7bf2013-11-07 19:24:06 +0000112 // TODO only enable for one differ
113 if (!alphaMaskCreated && !fDifferenceDir.isEmpty()) {
114 alphaMaskPending = differ->enablePOIAlphaMask();
115 }
zachr@google.com945708a2013-07-02 19:55:32 +0000116 int diffID = differ->queueDiff(&baselineBitmap, &testBitmap);
117 if (diffID >= 0) {
118
119 // Copy the results into data for this record
120 DiffData& diffData = newRecord->fDiffs.push_back();
121
122 diffData.fDiffName = differ->getName();
123 diffData.fResult = differ->getResult(diffID);
124
125 int poiCount = differ->getPointsOfInterestCount(diffID);
126 SkIPoint* poi = differ->getPointsOfInterest(diffID);
127 diffData.fPointsOfInterest.append(poiCount, poi);
128
djsollen@google.com513a7bf2013-11-07 19:24:06 +0000129 if (alphaMaskPending
130 && SkImageDiffer::RESULT_CORRECT != diffData.fResult
131 && newRecord->fDifferencePath.isEmpty()) {
132 newRecord->fDifferencePath = SkOSPath::SkPathJoin(fDifferenceDir.c_str(),
133 newRecord->fCommonName.c_str());
134
135 // compute the image diff and output it
136 SkBitmap* alphaMask = differ->getPointsOfInterestAlphaMask(diffID);
137 SkBitmap copy;
138 alphaMask->copyTo(&copy, SkBitmap::kARGB_8888_Config);
139 SkImageEncoder::EncodeFile(newRecord->fDifferencePath.c_str(), copy,
140 SkImageEncoder::kPNG_Type, 100);
141 }
142
143 if (alphaMaskPending) {
144 alphaMaskPending = false;
145 alphaMaskCreated = true;
146 }
147
zachr@google.com945708a2013-07-02 19:55:32 +0000148 // Because we are doing everything synchronously for now, we are done with the diff
149 // after reading it.
150 differ->deleteDiff(diffID);
151 }
152 }
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000153
154 // if we get a difference and we want the alpha mask then compute it here.
zachr@google.com945708a2013-07-02 19:55:32 +0000155}
156
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000157class SkThreadedDiff : public SkRunnable {
158public:
159 SkThreadedDiff() : fDiffContext(NULL) { }
160
161 void setup(SkDiffContext* diffContext, const SkString& baselinePath, const SkString& testPath) {
162 fDiffContext = diffContext;
163 fBaselinePath = baselinePath;
164 fTestPath = testPath;
165 }
166
167 virtual void run() SK_OVERRIDE {
168 fDiffContext->addDiff(fBaselinePath.c_str(), fTestPath.c_str());
169 }
170
171private:
172 SkDiffContext* fDiffContext;
173 SkString fBaselinePath;
174 SkString fTestPath;
175};
zachr@google.com945708a2013-07-02 19:55:32 +0000176
177void SkDiffContext::diffDirectories(const char baselinePath[], const char testPath[]) {
178 // Get the files in the baseline, we will then look for those inside the test path
179 SkTArray<SkString> baselineEntries;
180 if (!get_directory(baselinePath, &baselineEntries)) {
181 SkDebugf("Unable to open path \"%s\"\n", baselinePath);
182 return;
183 }
184
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000185 SkThreadPool threadPool(fThreadCount);
186 SkTArray<SkThreadedDiff> runnableDiffs;
187 runnableDiffs.reset(baselineEntries.count());
188
189 for (int x = 0; x < baselineEntries.count(); x++) {
190 const char* baseFilename = baselineEntries[x].c_str();
zachr@google.com945708a2013-07-02 19:55:32 +0000191
192 // Find the real location of each file to compare
193 SkString baselineFile = SkOSPath::SkPathJoin(baselinePath, baseFilename);
194 SkString testFile = SkOSPath::SkPathJoin(testPath, baseFilename);
195
196 // Check that the test file exists and is a file
197 if (sk_exists(testFile.c_str()) && !sk_isdir(testFile.c_str())) {
198 // Queue up the comparison with the differ
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000199 runnableDiffs[x].setup(this, baselineFile, testFile);
200 threadPool.add(&runnableDiffs[x]);
zachr@google.com945708a2013-07-02 19:55:32 +0000201 } else {
202 SkDebugf("Baseline file \"%s\" has no corresponding test file\n", baselineFile.c_str());
203 }
204 }
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000205
206 threadPool.wait();
zachr@google.com945708a2013-07-02 19:55:32 +0000207}
208
209
210void SkDiffContext::diffPatterns(const char baselinePattern[], const char testPattern[]) {
211 // Get the files in the baseline and test patterns. Because they are in sorted order, it's easy
212 // to find corresponding images by matching entry indices.
213
214 SkTArray<SkString> baselineEntries;
215 if (!glob_files(baselinePattern, &baselineEntries)) {
216 SkDebugf("Unable to get pattern \"%s\"\n", baselinePattern);
217 return;
218 }
219
220 SkTArray<SkString> testEntries;
221 if (!glob_files(testPattern, &testEntries)) {
222 SkDebugf("Unable to get pattern \"%s\"\n", testPattern);
223 return;
224 }
225
226 if (baselineEntries.count() != testEntries.count()) {
227 SkDebugf("Baseline and test patterns do not yield corresponding number of files\n");
228 return;
229 }
230
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000231 SkThreadPool threadPool(fThreadCount);
232 SkTArray<SkThreadedDiff> runnableDiffs;
233 runnableDiffs.reset(baselineEntries.count());
zachr@google.com945708a2013-07-02 19:55:32 +0000234
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000235 for (int x = 0; x < baselineEntries.count(); x++) {
236 runnableDiffs[x].setup(this, baselineEntries[x], testEntries[x]);
237 threadPool.add(&runnableDiffs[x]);
zachr@google.com945708a2013-07-02 19:55:32 +0000238 }
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000239
240 threadPool.wait();
zachr@google.com945708a2013-07-02 19:55:32 +0000241}
242
zachr@google.coma95959c2013-07-08 15:04:45 +0000243void SkDiffContext::outputRecords(SkWStream& stream, bool useJSONP) {
zachr@google.com945708a2013-07-02 19:55:32 +0000244 DiffRecord* currentRecord = fRecords;
zachr@google.coma95959c2013-07-08 15:04:45 +0000245 if (useJSONP) {
246 stream.writeText("var SkPDiffRecords = {\n");
zachr@google.com55173f22013-07-25 17:22:58 +0000247 } else {
zachr@google.coma95959c2013-07-08 15:04:45 +0000248 stream.writeText("{\n");
249 }
zachr@google.com945708a2013-07-02 19:55:32 +0000250 stream.writeText(" \"records\": [\n");
251 while (NULL != currentRecord) {
252 stream.writeText(" {\n");
253
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000254 SkString differenceAbsPath = get_absolute_path(currentRecord->fDifferencePath);
zachr@google.coma479aa12013-08-02 15:54:30 +0000255 SkString baselineAbsPath = get_absolute_path(currentRecord->fBaselinePath);
256 SkString testAbsPath = get_absolute_path(currentRecord->fTestPath);
257
djsollen@google.com1e391b52013-10-16 15:00:11 +0000258 stream.writeText(" \"commonName\": \"");
djsollen@google.com513a7bf2013-11-07 19:24:06 +0000259 stream.writeText(currentRecord->fCommonName.c_str());
djsollen@google.com1e391b52013-10-16 15:00:11 +0000260 stream.writeText("\",\n");
261
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000262 stream.writeText(" \"differencePath\": \"");
263 stream.writeText(differenceAbsPath.c_str());
264 stream.writeText("\",\n");
265
zachr@google.com945708a2013-07-02 19:55:32 +0000266 stream.writeText(" \"baselinePath\": \"");
zachr@google.coma479aa12013-08-02 15:54:30 +0000267 stream.writeText(baselineAbsPath.c_str());
zachr@google.com945708a2013-07-02 19:55:32 +0000268 stream.writeText("\",\n");
269
270 stream.writeText(" \"testPath\": \"");
zachr@google.coma479aa12013-08-02 15:54:30 +0000271 stream.writeText(testAbsPath.c_str());
zachr@google.com945708a2013-07-02 19:55:32 +0000272 stream.writeText("\",\n");
273
274 stream.writeText(" \"diffs\": [\n");
275 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) {
276 DiffData& data = currentRecord->fDiffs[diffIndex];
277 stream.writeText(" {\n");
278
279 stream.writeText(" \"differName\": \"");
280 stream.writeText(data.fDiffName);
281 stream.writeText("\",\n");
282
283 stream.writeText(" \"result\": ");
zachr@google.com35f02fb2013-07-22 17:05:24 +0000284 stream.writeScalarAsText((SkScalar)data.fResult);
zachr@google.com945708a2013-07-02 19:55:32 +0000285 stream.writeText(",\n");
286
287 stream.writeText(" \"pointsOfInterest\": [\n");
zachr@google.comee0f46d2013-07-23 12:57:52 +0000288 for (int poiIndex = 0; poiIndex < data.fPointsOfInterest.count() &&
289 poiIndex < kMaxPOI; poiIndex++) {
zachr@google.com945708a2013-07-02 19:55:32 +0000290 SkIPoint poi = data.fPointsOfInterest[poiIndex];
291 stream.writeText(" [");
292 stream.writeDecAsText(poi.x());
293 stream.writeText(",");
294 stream.writeDecAsText(poi.y());
295 stream.writeText("]");
296
297 // JSON does not allow trailing commas
zachr@google.com55173f22013-07-25 17:22:58 +0000298 if (poiIndex + 1 < data.fPointsOfInterest.count() &&
299 poiIndex + 1 < kMaxPOI) {
zachr@google.com945708a2013-07-02 19:55:32 +0000300 stream.writeText(",");
301 }
302 stream.writeText("\n");
303 }
304 stream.writeText(" ]\n");
305 stream.writeText(" }");
306
307 // JSON does not allow trailing commas
zachr@google.com55173f22013-07-25 17:22:58 +0000308 if (diffIndex + 1 < currentRecord->fDiffs.count()) {
zachr@google.com945708a2013-07-02 19:55:32 +0000309 stream.writeText(",");
310 }
311 stream.writeText(" \n");
312 }
313 stream.writeText(" ]\n");
314
315 stream.writeText(" }");
316
317 // JSON does not allow trailing commas
zachr@google.com55173f22013-07-25 17:22:58 +0000318 if (NULL != currentRecord->fNext) {
zachr@google.com945708a2013-07-02 19:55:32 +0000319 stream.writeText(",");
320 }
321 stream.writeText("\n");
322 currentRecord = currentRecord->fNext;
323 }
324 stream.writeText(" ]\n");
zachr@google.coma95959c2013-07-08 15:04:45 +0000325 if (useJSONP) {
326 stream.writeText("};\n");
zachr@google.com55173f22013-07-25 17:22:58 +0000327 } else {
zachr@google.coma95959c2013-07-08 15:04:45 +0000328 stream.writeText("}\n");
329 }
zachr@google.com945708a2013-07-02 19:55:32 +0000330}
edisonn@google.comc93c8ac2013-07-22 15:24:26 +0000331
332void SkDiffContext::outputCsv(SkWStream& stream) {
333 SkTDict<int> columns(2);
334 int cntColumns = 0;
335
336 stream.writeText("key");
337
338 DiffRecord* currentRecord = fRecords;
339
340 // Write CSV header and create a dictionary of all columns.
341 while (NULL != currentRecord) {
342 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) {
343 DiffData& data = currentRecord->fDiffs[diffIndex];
344 if (!columns.find(data.fDiffName)) {
345 columns.set(data.fDiffName, cntColumns);
346 stream.writeText(", ");
347 stream.writeText(data.fDiffName);
348 cntColumns++;
349 }
350 }
351 currentRecord = currentRecord->fNext;
352 }
353 stream.writeText("\n");
354
355 double values[100];
356 SkASSERT(cntColumns < 100); // Make the array larger, if we ever have so many diff types.
357
358 currentRecord = fRecords;
359 while (NULL != currentRecord) {
360 for (int i = 0; i < cntColumns; i++) {
361 values[i] = -1;
362 }
363
364 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) {
365 DiffData& data = currentRecord->fDiffs[diffIndex];
366 int index = -1;
367 SkAssertResult(columns.find(data.fDiffName, &index));
368 SkASSERT(index >= 0 && index < cntColumns);
369 values[index] = data.fResult;
370 }
371
372 const char* filename = currentRecord->fBaselinePath.c_str() +
373 strlen(currentRecord->fBaselinePath.c_str()) - 1;
374 while (filename > currentRecord->fBaselinePath.c_str() && *(filename - 1) != '/') {
375 filename--;
376 }
377
378 stream.writeText(filename);
379
380 for (int i = 0; i < cntColumns; i++) {
381 SkString str;
382 str.printf(", %f", values[i]);
383 stream.writeText(str.c_str());
384 }
385 stream.writeText("\n");
386
387 currentRecord = currentRecord->fNext;
388 }
389}