Home | History | Annotate | Download | only in skpdiff
      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(&copy, 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