bungeman@google.com | e3c8ddf | 2012-12-05 20:13:12 +0000 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2012 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 "skdiff.h" |
| 9 | #include "skdiff_html.h" |
| 10 | #include "SkStream.h" |
| 11 | #include "SkTime.h" |
| 12 | |
| 13 | /// Make layout more consistent by scaling image to 240 height, 360 width, |
| 14 | /// or natural size, whichever is smallest. |
| 15 | static int compute_image_height(int height, int width) { |
| 16 | int retval = 240; |
| 17 | if (height < retval) { |
| 18 | retval = height; |
| 19 | } |
| 20 | float scale = (float) retval / height; |
| 21 | if (width * scale > 360) { |
| 22 | scale = (float) 360 / width; |
| 23 | retval = static_cast<int>(height * scale); |
| 24 | } |
| 25 | return retval; |
| 26 | } |
| 27 | |
| 28 | static void print_table_header(SkFILEWStream* stream, |
| 29 | const int matchCount, |
| 30 | const int colorThreshold, |
| 31 | const RecordArray& differences, |
| 32 | const SkString &baseDir, |
| 33 | const SkString &comparisonDir, |
| 34 | bool doOutputDate = false) { |
| 35 | stream->writeText("<table>\n"); |
| 36 | stream->writeText("<tr><th>"); |
| 37 | stream->writeText("select image</th>\n<th>"); |
| 38 | if (doOutputDate) { |
| 39 | SkTime::DateTime dt; |
| 40 | SkTime::GetDateTime(&dt); |
| 41 | stream->writeText("SkDiff run at "); |
| 42 | stream->writeDecAsText(dt.fHour); |
| 43 | stream->writeText(":"); |
| 44 | if (dt.fMinute < 10) { |
| 45 | stream->writeText("0"); |
| 46 | } |
| 47 | stream->writeDecAsText(dt.fMinute); |
| 48 | stream->writeText(":"); |
| 49 | if (dt.fSecond < 10) { |
| 50 | stream->writeText("0"); |
| 51 | } |
| 52 | stream->writeDecAsText(dt.fSecond); |
| 53 | stream->writeText("<br>"); |
| 54 | } |
| 55 | stream->writeDecAsText(matchCount); |
| 56 | stream->writeText(" of "); |
| 57 | stream->writeDecAsText(differences.count()); |
| 58 | stream->writeText(" diffs matched "); |
| 59 | if (colorThreshold == 0) { |
| 60 | stream->writeText("exactly"); |
| 61 | } else { |
| 62 | stream->writeText("within "); |
| 63 | stream->writeDecAsText(colorThreshold); |
| 64 | stream->writeText(" color units per component"); |
| 65 | } |
| 66 | stream->writeText(".<br>"); |
| 67 | stream->writeText("</th>\n<th>"); |
| 68 | stream->writeText("every different pixel shown in white"); |
| 69 | stream->writeText("</th>\n<th>"); |
| 70 | stream->writeText("color difference at each pixel"); |
| 71 | stream->writeText("</th>\n<th>baseDir: "); |
| 72 | stream->writeText(baseDir.c_str()); |
| 73 | stream->writeText("</th>\n<th>comparisonDir: "); |
| 74 | stream->writeText(comparisonDir.c_str()); |
| 75 | stream->writeText("</th>\n"); |
| 76 | stream->writeText("</tr>\n"); |
| 77 | } |
| 78 | |
| 79 | static void print_pixel_count(SkFILEWStream* stream, const DiffRecord& diff) { |
| 80 | stream->writeText("<br>("); |
| 81 | stream->writeDecAsText(static_cast<int>(diff.fFractionDifference * |
| 82 | diff.fBase.fBitmap.width() * |
| 83 | diff.fBase.fBitmap.height())); |
| 84 | stream->writeText(" pixels)"); |
| 85 | /* |
| 86 | stream->writeDecAsText(diff.fWeightedFraction * |
| 87 | diff.fBaseWidth * |
| 88 | diff.fBaseHeight); |
| 89 | stream->writeText(" weighted pixels)"); |
| 90 | */ |
| 91 | } |
| 92 | |
| 93 | static void print_checkbox_cell(SkFILEWStream* stream, const DiffRecord& diff) { |
| 94 | stream->writeText("<td><input type=\"checkbox\" name=\""); |
| 95 | stream->writeText(diff.fBase.fFilename.c_str()); |
| 96 | stream->writeText("\" checked=\"yes\"></td>"); |
| 97 | } |
| 98 | |
| 99 | static void print_label_cell(SkFILEWStream* stream, const DiffRecord& diff) { |
| 100 | char metricBuf [20]; |
| 101 | |
| 102 | stream->writeText("<td><b>"); |
| 103 | stream->writeText(diff.fBase.fFilename.c_str()); |
| 104 | stream->writeText("</b><br>"); |
| 105 | switch (diff.fResult) { |
| 106 | case DiffRecord::kEqualBits_Result: |
| 107 | SkDEBUGFAIL("should not encounter DiffRecord with kEqualBits here"); |
| 108 | return; |
| 109 | case DiffRecord::kEqualPixels_Result: |
| 110 | SkDEBUGFAIL("should not encounter DiffRecord with kEqualPixels here"); |
| 111 | return; |
| 112 | case DiffRecord::kDifferentSizes_Result: |
| 113 | stream->writeText("Image sizes differ</td>"); |
| 114 | return; |
| 115 | case DiffRecord::kDifferentPixels_Result: |
| 116 | sprintf(metricBuf, "%.4f%%", 100 * diff.fFractionDifference); |
| 117 | stream->writeText(metricBuf); |
| 118 | stream->writeText(" of pixels differ"); |
| 119 | stream->writeText("\n ("); |
| 120 | sprintf(metricBuf, "%.4f%%", 100 * diff.fWeightedFraction); |
| 121 | stream->writeText(metricBuf); |
| 122 | stream->writeText(" weighted)"); |
| 123 | // Write the actual number of pixels that differ if it's < 1% |
| 124 | if (diff.fFractionDifference < 0.01) { |
| 125 | print_pixel_count(stream, diff); |
| 126 | } |
rmistry@google.com | ee5a5ee | 2013-01-03 19:23:22 +0000 | [diff] [blame] | 127 | stream->writeText("<br>"); |
reed@google.com | 89d15a2 | 2013-01-07 22:26:05 +0000 | [diff] [blame] | 128 | if (SkScalarRoundToInt(diff.fAverageMismatchA) > 0) { |
rmistry@google.com | ee5a5ee | 2013-01-03 19:23:22 +0000 | [diff] [blame] | 129 | stream->writeText("<br>Average alpha channel mismatch "); |
reed@google.com | 89d15a2 | 2013-01-07 22:26:05 +0000 | [diff] [blame] | 130 | stream->writeDecAsText(SkScalarRoundToInt(diff.fAverageMismatchA)); |
rmistry@google.com | ee5a5ee | 2013-01-03 19:23:22 +0000 | [diff] [blame] | 131 | } |
| 132 | |
| 133 | stream->writeText("<br>Max alpha channel mismatch "); |
reed@google.com | 89d15a2 | 2013-01-07 22:26:05 +0000 | [diff] [blame] | 134 | stream->writeDecAsText(SkScalarRoundToInt(diff.fMaxMismatchA)); |
rmistry@google.com | ee5a5ee | 2013-01-03 19:23:22 +0000 | [diff] [blame] | 135 | |
| 136 | stream->writeText("<br>Total alpha channel mismatch "); |
| 137 | stream->writeDecAsText(static_cast<int>(diff.fTotalMismatchA)); |
| 138 | |
| 139 | stream->writeText("<br>"); |
bungeman@google.com | e3c8ddf | 2012-12-05 20:13:12 +0000 | [diff] [blame] | 140 | stream->writeText("<br>Average color mismatch "); |
reed@google.com | 89d15a2 | 2013-01-07 22:26:05 +0000 | [diff] [blame] | 141 | stream->writeDecAsText(SkScalarRoundToInt(MAX3(diff.fAverageMismatchR, |
| 142 | diff.fAverageMismatchG, |
| 143 | diff.fAverageMismatchB))); |
bungeman@google.com | e3c8ddf | 2012-12-05 20:13:12 +0000 | [diff] [blame] | 144 | stream->writeText("<br>Max color mismatch "); |
| 145 | stream->writeDecAsText(MAX3(diff.fMaxMismatchR, |
| 146 | diff.fMaxMismatchG, |
| 147 | diff.fMaxMismatchB)); |
| 148 | stream->writeText("</td>"); |
| 149 | break; |
| 150 | case DiffRecord::kCouldNotCompare_Result: |
| 151 | stream->writeText("Could not compare.<br>base: "); |
| 152 | stream->writeText(DiffResource::getStatusDescription(diff.fBase.fStatus)); |
| 153 | stream->writeText("<br>comparison: "); |
| 154 | stream->writeText(DiffResource::getStatusDescription(diff.fComparison.fStatus)); |
| 155 | stream->writeText("</td>"); |
| 156 | return; |
| 157 | default: |
| 158 | SkDEBUGFAIL("encountered DiffRecord with unknown result type"); |
| 159 | return; |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | static void print_image_cell(SkFILEWStream* stream, const SkString& path, int height) { |
| 164 | stream->writeText("<td><a href=\""); |
| 165 | stream->writeText(path.c_str()); |
| 166 | stream->writeText("\"><img src=\""); |
| 167 | stream->writeText(path.c_str()); |
| 168 | stream->writeText("\" height=\""); |
| 169 | stream->writeDecAsText(height); |
| 170 | stream->writeText("px\"></a></td>"); |
| 171 | } |
| 172 | |
| 173 | static void print_link_cell(SkFILEWStream* stream, const SkString& path, const char* text) { |
| 174 | stream->writeText("<td><a href=\""); |
| 175 | stream->writeText(path.c_str()); |
| 176 | stream->writeText("\">"); |
| 177 | stream->writeText(text); |
| 178 | stream->writeText("</a></td>"); |
| 179 | } |
| 180 | |
| 181 | static void print_diff_resource_cell(SkFILEWStream* stream, DiffResource& resource, |
| 182 | const SkString& relativePath, bool local) { |
| 183 | if (resource.fBitmap.empty()) { |
| 184 | if (DiffResource::kCouldNotDecode_Status == resource.fStatus) { |
| 185 | if (local && !resource.fFilename.isEmpty()) { |
| 186 | print_link_cell(stream, resource.fFilename, "N/A"); |
| 187 | return; |
| 188 | } |
| 189 | if (!resource.fFullPath.isEmpty()) { |
| 190 | if (!resource.fFullPath.startsWith(PATH_DIV_STR)) { |
| 191 | resource.fFullPath.prepend(relativePath); |
| 192 | } |
| 193 | print_link_cell(stream, resource.fFullPath, "N/A"); |
| 194 | return; |
| 195 | } |
| 196 | } |
| 197 | stream->writeText("<td>N/A</td>"); |
| 198 | return; |
| 199 | } |
| 200 | |
| 201 | int height = compute_image_height(resource.fBitmap.height(), resource.fBitmap.width()); |
| 202 | if (local) { |
| 203 | print_image_cell(stream, resource.fFilename, height); |
| 204 | return; |
| 205 | } |
| 206 | if (!resource.fFullPath.startsWith(PATH_DIV_STR)) { |
| 207 | resource.fFullPath.prepend(relativePath); |
| 208 | } |
| 209 | print_image_cell(stream, resource.fFullPath, height); |
| 210 | } |
| 211 | |
| 212 | static void print_diff_row(SkFILEWStream* stream, DiffRecord& diff, const SkString& relativePath) { |
| 213 | stream->writeText("<tr>\n"); |
| 214 | print_checkbox_cell(stream, diff); |
| 215 | print_label_cell(stream, diff); |
| 216 | print_diff_resource_cell(stream, diff.fWhite, relativePath, true); |
| 217 | print_diff_resource_cell(stream, diff.fDifference, relativePath, true); |
| 218 | print_diff_resource_cell(stream, diff.fBase, relativePath, false); |
| 219 | print_diff_resource_cell(stream, diff.fComparison, relativePath, false); |
| 220 | stream->writeText("</tr>\n"); |
| 221 | stream->flush(); |
| 222 | } |
| 223 | |
| 224 | void print_diff_page(const int matchCount, |
| 225 | const int colorThreshold, |
| 226 | const RecordArray& differences, |
| 227 | const SkString& baseDir, |
| 228 | const SkString& comparisonDir, |
| 229 | const SkString& outputDir) { |
| 230 | |
| 231 | SkASSERT(!baseDir.isEmpty()); |
| 232 | SkASSERT(!comparisonDir.isEmpty()); |
| 233 | SkASSERT(!outputDir.isEmpty()); |
| 234 | |
| 235 | SkString outputPath(outputDir); |
| 236 | outputPath.append("index.html"); |
| 237 | //SkFILEWStream outputStream ("index.html"); |
| 238 | SkFILEWStream outputStream(outputPath.c_str()); |
| 239 | |
| 240 | // Need to convert paths from relative-to-cwd to relative-to-outputDir |
| 241 | // FIXME this doesn't work if there are '..' inside the outputDir |
| 242 | |
| 243 | bool isPathAbsolute = false; |
| 244 | // On Windows or Linux, a path starting with PATH_DIV_CHAR is absolute. |
| 245 | if (outputDir.size() > 0 && PATH_DIV_CHAR == outputDir[0]) { |
| 246 | isPathAbsolute = true; |
| 247 | } |
| 248 | #ifdef SK_BUILD_FOR_WIN32 |
| 249 | // On Windows, absolute paths can also start with "x:", where x is any |
| 250 | // drive letter. |
| 251 | if (outputDir.size() > 1 && ':' == outputDir[1]) { |
| 252 | isPathAbsolute = true; |
| 253 | } |
| 254 | #endif |
| 255 | |
| 256 | SkString relativePath; |
| 257 | if (!isPathAbsolute) { |
| 258 | unsigned int ui; |
| 259 | for (ui = 0; ui < outputDir.size(); ui++) { |
| 260 | if (outputDir[ui] == PATH_DIV_CHAR) { |
| 261 | relativePath.append(".." PATH_DIV_STR); |
| 262 | } |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | outputStream.writeText( |
| 267 | "<html>\n<head>\n" |
| 268 | "<script src=\"https://ajax.googleapis.com/ajax/" |
| 269 | "libs/jquery/1.7.2/jquery.min.js\"></script>\n" |
| 270 | "<script type=\"text/javascript\">\n" |
| 271 | "function generateCheckedList() {\n" |
| 272 | "var boxes = $(\":checkbox:checked\");\n" |
| 273 | "var fileCmdLineString = '';\n" |
| 274 | "var fileMultiLineString = '';\n" |
| 275 | "for (var i = 0; i < boxes.length; i++) {\n" |
| 276 | "fileMultiLineString += boxes[i].name + '<br>';\n" |
| 277 | "fileCmdLineString += boxes[i].name + ' ';\n" |
| 278 | "}\n" |
| 279 | "$(\"#checkedList\").html(fileCmdLineString + " |
| 280 | "'<br><br>' + fileMultiLineString);\n" |
| 281 | "}\n" |
| 282 | "</script>\n</head>\n<body>\n"); |
| 283 | print_table_header(&outputStream, matchCount, colorThreshold, differences, |
| 284 | baseDir, comparisonDir); |
| 285 | int i; |
| 286 | for (i = 0; i < differences.count(); i++) { |
| 287 | DiffRecord* diff = differences[i]; |
| 288 | |
| 289 | switch (diff->fResult) { |
| 290 | // Cases in which there is no diff to report. |
| 291 | case DiffRecord::kEqualBits_Result: |
| 292 | case DiffRecord::kEqualPixels_Result: |
| 293 | continue; |
| 294 | // Cases in which we want a detailed pixel diff. |
| 295 | case DiffRecord::kDifferentPixels_Result: |
| 296 | case DiffRecord::kDifferentSizes_Result: |
| 297 | case DiffRecord::kCouldNotCompare_Result: |
| 298 | print_diff_row(&outputStream, *diff, relativePath); |
| 299 | continue; |
| 300 | default: |
| 301 | SkDEBUGFAIL("encountered DiffRecord with unknown result type"); |
| 302 | continue; |
| 303 | } |
| 304 | } |
| 305 | outputStream.writeText( |
| 306 | "</table>\n" |
| 307 | "<input type=\"button\" " |
| 308 | "onclick=\"generateCheckedList()\" " |
| 309 | "value=\"Create Rebaseline List\">\n" |
| 310 | "<div id=\"checkedList\"></div>\n" |
| 311 | "</body>\n</html>\n"); |
| 312 | outputStream.flush(); |
| 313 | } |