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 |