blob: 26898c92f38fc0168b1f998974a6228775c896b7 [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"
epoger54f1ad82014-07-02 07:43:04 -070012#include "SkSize.h"
zachr@google.com945708a2013-07-02 19:55:32 +000013#include "SkStream.h"
edisonn@google.comc93c8ac2013-07-22 15:24:26 +000014#include "SkTDict.h"
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +000015#include "SkThreadPool.h"
zachr@google.com945708a2013-07-02 19:55:32 +000016
17#include "SkDiffContext.h"
epoger54f1ad82014-07-02 07:43:04 -070018#include "SkImageDiffer.h"
zachr@google.com945708a2013-07-02 19:55:32 +000019#include "skpdiff_util.h"
20
21SkDiffContext::SkDiffContext() {
zachr@google.com945708a2013-07-02 19:55:32 +000022 fDiffers = NULL;
23 fDifferCount = 0;
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +000024 fThreadCount = SkThreadPool::kThreadPerCore;
zachr@google.com945708a2013-07-02 19:55:32 +000025}
26
27SkDiffContext::~SkDiffContext() {
zachr@google.com945708a2013-07-02 19:55:32 +000028 if (NULL != fDiffers) {
29 SkDELETE_ARRAY(fDiffers);
30 }
31}
32
epoger54f1ad82014-07-02 07:43:04 -070033void SkDiffContext::setAlphaMaskDir(const SkString& path) {
djsollen@google.com513a7bf2013-11-07 19:24:06 +000034 if (!path.isEmpty() && sk_mkdir(path.c_str())) {
epoger54f1ad82014-07-02 07:43:04 -070035 fAlphaMaskDir = path;
36 }
37}
38
39void SkDiffContext::setRgbDiffDir(const SkString& path) {
40 if (!path.isEmpty() && sk_mkdir(path.c_str())) {
41 fRgbDiffDir = path;
42 }
43}
44
45void SkDiffContext::setWhiteDiffDir(const SkString& path) {
46 if (!path.isEmpty() && sk_mkdir(path.c_str())) {
47 fWhiteDiffDir = path;
djsollen@google.com513a7bf2013-11-07 19:24:06 +000048 }
49}
50
zachr@google.com945708a2013-07-02 19:55:32 +000051void SkDiffContext::setDiffers(const SkTDArray<SkImageDiffer*>& differs) {
52 // Delete whatever the last array of differs was
53 if (NULL != fDiffers) {
54 SkDELETE_ARRAY(fDiffers);
55 fDiffers = NULL;
56 fDifferCount = 0;
57 }
58
59 // Copy over the new differs
60 fDifferCount = differs.count();
61 fDiffers = SkNEW_ARRAY(SkImageDiffer*, fDifferCount);
62 differs.copy(fDiffers);
63}
64
djsollen@google.com513a7bf2013-11-07 19:24:06 +000065static SkString get_common_prefix(const SkString& a, const SkString& b) {
66 const size_t maxPrefixLength = SkTMin(a.size(), b.size());
67 SkASSERT(maxPrefixLength > 0);
68 for (size_t x = 0; x < maxPrefixLength; ++x) {
69 if (a[x] != b[x]) {
70 SkString result;
71 result.set(a.c_str(), x);
72 return result;
73 }
74 }
75 if (a.size() > b.size()) {
76 return b;
77 } else {
78 return a;
79 }
80}
81
zachr@google.com945708a2013-07-02 19:55:32 +000082void SkDiffContext::addDiff(const char* baselinePath, const char* testPath) {
83 // Load the images at the paths
84 SkBitmap baselineBitmap;
85 SkBitmap testBitmap;
86 if (!SkImageDecoder::DecodeFile(baselinePath, &baselineBitmap)) {
87 SkDebugf("Failed to load bitmap \"%s\"\n", baselinePath);
88 return;
89 }
90 if (!SkImageDecoder::DecodeFile(testPath, &testBitmap)) {
91 SkDebugf("Failed to load bitmap \"%s\"\n", testPath);
92 return;
93 }
94
95 // Setup a record for this diff
djsollen@google.comefc51b72013-11-12 18:29:17 +000096 fRecordMutex.acquire();
97 DiffRecord* newRecord = fRecords.addToHead(DiffRecord());
98 fRecordMutex.release();
zachr@google.com945708a2013-07-02 19:55:32 +000099
djsollen@google.com513a7bf2013-11-07 19:24:06 +0000100 // compute the common name
101 SkString baseName = SkOSPath::SkBasename(baselinePath);
102 SkString testName = SkOSPath::SkBasename(testPath);
103 newRecord->fCommonName = get_common_prefix(baseName, testName);
104
djsollen@google.comefc51b72013-11-12 18:29:17 +0000105 newRecord->fBaselinePath = baselinePath;
106 newRecord->fTestPath = testPath;
epoger54f1ad82014-07-02 07:43:04 -0700107 newRecord->fSize = SkISize::Make(baselineBitmap.width(), baselineBitmap.height());
djsollen@google.comefc51b72013-11-12 18:29:17 +0000108
epoger54f1ad82014-07-02 07:43:04 -0700109 // only generate diff images if we have a place to store them
110 SkImageDiffer::BitmapsToCreate bitmapsToCreate;
111 bitmapsToCreate.alphaMask = !fAlphaMaskDir.isEmpty();
112 bitmapsToCreate.rgbDiff = !fRgbDiffDir.isEmpty();
113 bitmapsToCreate.whiteDiff = !fWhiteDiffDir.isEmpty();
djsollen@google.com513a7bf2013-11-07 19:24:06 +0000114
zachr@google.com945708a2013-07-02 19:55:32 +0000115 // Perform each diff
116 for (int differIndex = 0; differIndex < fDifferCount; differIndex++) {
117 SkImageDiffer* differ = fDiffers[differIndex];
djsollen@google.comefc51b72013-11-12 18:29:17 +0000118
119 // Copy the results into data for this record
120 DiffData& diffData = newRecord->fDiffs.push_back();
121 diffData.fDiffName = differ->getName();
122
epoger54f1ad82014-07-02 07:43:04 -0700123 if (!differ->diff(&baselineBitmap, &testBitmap, bitmapsToCreate, &diffData.fResult)) {
124 // if the diff failed, record -1 as the result
125 // TODO(djsollen): Record more detailed information about exactly what failed.
126 // (Image dimension mismatch? etc.) See http://skbug.com/2710 ('make skpdiff
127 // report more detail when it fails to compare two images')
djsollen@google.com27d7ede2014-02-11 16:29:39 +0000128 diffData.fResult.result = -1;
djsollen@google.comefc51b72013-11-12 18:29:17 +0000129 continue;
djsollen@google.com513a7bf2013-11-07 19:24:06 +0000130 }
zachr@google.com945708a2013-07-02 19:55:32 +0000131
epoger54f1ad82014-07-02 07:43:04 -0700132 if (bitmapsToCreate.alphaMask
djsollen@google.comefc51b72013-11-12 18:29:17 +0000133 && SkImageDiffer::RESULT_CORRECT != diffData.fResult.result
134 && !diffData.fResult.poiAlphaMask.empty()
135 && !newRecord->fCommonName.isEmpty()) {
zachr@google.com945708a2013-07-02 19:55:32 +0000136
epoger54f1ad82014-07-02 07:43:04 -0700137 newRecord->fAlphaMaskPath = SkOSPath::SkPathJoin(fAlphaMaskDir.c_str(),
138 newRecord->fCommonName.c_str());
zachr@google.com945708a2013-07-02 19:55:32 +0000139
djsollen@google.comefc51b72013-11-12 18:29:17 +0000140 // compute the image diff and output it
141 SkBitmap copy;
commit-bot@chromium.org28fcae22014-04-11 17:15:40 +0000142 diffData.fResult.poiAlphaMask.copyTo(&copy, kN32_SkColorType);
epoger54f1ad82014-07-02 07:43:04 -0700143 SkImageEncoder::EncodeFile(newRecord->fAlphaMaskPath.c_str(), copy,
djsollen@google.comefc51b72013-11-12 18:29:17 +0000144 SkImageEncoder::kPNG_Type, 100);
zachr@google.com945708a2013-07-02 19:55:32 +0000145
djsollen@google.comefc51b72013-11-12 18:29:17 +0000146 // cleanup the existing bitmap to free up resources;
147 diffData.fResult.poiAlphaMask.reset();
djsollen@google.com513a7bf2013-11-07 19:24:06 +0000148
epoger54f1ad82014-07-02 07:43:04 -0700149 bitmapsToCreate.alphaMask = false;
150 }
151
152 if (bitmapsToCreate.rgbDiff
153 && SkImageDiffer::RESULT_CORRECT != diffData.fResult.result
154 && !diffData.fResult.rgbDiffBitmap.empty()
155 && !newRecord->fCommonName.isEmpty()) {
156 // TODO(djsollen): Rather than taking the max r/g/b diffs that come back from
157 // a particular differ and storing them as toplevel fields within
158 // newRecord, we should extend outputRecords() to report optional
159 // fields for each differ (not just "result" and "pointsOfInterest").
160 // See http://skbug.com/2712 ('allow skpdiff to report different sets
161 // of result fields for different comparison algorithms')
162 newRecord->fMaxRedDiff = diffData.fResult.maxRedDiff;
163 newRecord->fMaxGreenDiff = diffData.fResult.maxGreenDiff;
164 newRecord->fMaxBlueDiff = diffData.fResult.maxBlueDiff;
165
166 newRecord->fRgbDiffPath = SkOSPath::SkPathJoin(fRgbDiffDir.c_str(),
167 newRecord->fCommonName.c_str());
168 SkImageEncoder::EncodeFile(newRecord->fRgbDiffPath.c_str(),
169 diffData.fResult.rgbDiffBitmap,
170 SkImageEncoder::kPNG_Type, 100);
171 diffData.fResult.rgbDiffBitmap.reset();
172 bitmapsToCreate.rgbDiff = false;
173 }
174
175 if (bitmapsToCreate.whiteDiff
176 && SkImageDiffer::RESULT_CORRECT != diffData.fResult.result
177 && !diffData.fResult.whiteDiffBitmap.empty()
178 && !newRecord->fCommonName.isEmpty()) {
179 newRecord->fWhiteDiffPath = SkOSPath::SkPathJoin(fWhiteDiffDir.c_str(),
180 newRecord->fCommonName.c_str());
181 SkImageEncoder::EncodeFile(newRecord->fWhiteDiffPath.c_str(),
182 diffData.fResult.whiteDiffBitmap,
183 SkImageEncoder::kPNG_Type, 100);
184 diffData.fResult.whiteDiffBitmap.reset();
185 bitmapsToCreate.whiteDiff = false;
zachr@google.com945708a2013-07-02 19:55:32 +0000186 }
187 }
188}
189
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000190class SkThreadedDiff : public SkRunnable {
191public:
192 SkThreadedDiff() : fDiffContext(NULL) { }
193
194 void setup(SkDiffContext* diffContext, const SkString& baselinePath, const SkString& testPath) {
195 fDiffContext = diffContext;
196 fBaselinePath = baselinePath;
197 fTestPath = testPath;
198 }
199
200 virtual void run() SK_OVERRIDE {
201 fDiffContext->addDiff(fBaselinePath.c_str(), fTestPath.c_str());
202 }
203
204private:
205 SkDiffContext* fDiffContext;
206 SkString fBaselinePath;
207 SkString fTestPath;
208};
zachr@google.com945708a2013-07-02 19:55:32 +0000209
210void SkDiffContext::diffDirectories(const char baselinePath[], const char testPath[]) {
211 // Get the files in the baseline, we will then look for those inside the test path
212 SkTArray<SkString> baselineEntries;
213 if (!get_directory(baselinePath, &baselineEntries)) {
214 SkDebugf("Unable to open path \"%s\"\n", baselinePath);
215 return;
216 }
217
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000218 SkThreadPool threadPool(fThreadCount);
219 SkTArray<SkThreadedDiff> runnableDiffs;
220 runnableDiffs.reset(baselineEntries.count());
221
222 for (int x = 0; x < baselineEntries.count(); x++) {
223 const char* baseFilename = baselineEntries[x].c_str();
zachr@google.com945708a2013-07-02 19:55:32 +0000224
225 // Find the real location of each file to compare
226 SkString baselineFile = SkOSPath::SkPathJoin(baselinePath, baseFilename);
227 SkString testFile = SkOSPath::SkPathJoin(testPath, baseFilename);
228
229 // Check that the test file exists and is a file
230 if (sk_exists(testFile.c_str()) && !sk_isdir(testFile.c_str())) {
231 // Queue up the comparison with the differ
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000232 runnableDiffs[x].setup(this, baselineFile, testFile);
233 threadPool.add(&runnableDiffs[x]);
zachr@google.com945708a2013-07-02 19:55:32 +0000234 } else {
235 SkDebugf("Baseline file \"%s\" has no corresponding test file\n", baselineFile.c_str());
236 }
237 }
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000238
239 threadPool.wait();
zachr@google.com945708a2013-07-02 19:55:32 +0000240}
241
242
243void SkDiffContext::diffPatterns(const char baselinePattern[], const char testPattern[]) {
244 // Get the files in the baseline and test patterns. Because they are in sorted order, it's easy
245 // to find corresponding images by matching entry indices.
246
247 SkTArray<SkString> baselineEntries;
248 if (!glob_files(baselinePattern, &baselineEntries)) {
249 SkDebugf("Unable to get pattern \"%s\"\n", baselinePattern);
250 return;
251 }
252
253 SkTArray<SkString> testEntries;
254 if (!glob_files(testPattern, &testEntries)) {
255 SkDebugf("Unable to get pattern \"%s\"\n", testPattern);
256 return;
257 }
258
259 if (baselineEntries.count() != testEntries.count()) {
260 SkDebugf("Baseline and test patterns do not yield corresponding number of files\n");
261 return;
262 }
263
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000264 SkThreadPool threadPool(fThreadCount);
265 SkTArray<SkThreadedDiff> runnableDiffs;
266 runnableDiffs.reset(baselineEntries.count());
zachr@google.com945708a2013-07-02 19:55:32 +0000267
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000268 for (int x = 0; x < baselineEntries.count(); x++) {
269 runnableDiffs[x].setup(this, baselineEntries[x], testEntries[x]);
270 threadPool.add(&runnableDiffs[x]);
zachr@google.com945708a2013-07-02 19:55:32 +0000271 }
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000272
273 threadPool.wait();
zachr@google.com945708a2013-07-02 19:55:32 +0000274}
275
zachr@google.coma95959c2013-07-08 15:04:45 +0000276void SkDiffContext::outputRecords(SkWStream& stream, bool useJSONP) {
djsollen@google.comefc51b72013-11-12 18:29:17 +0000277 SkTLList<DiffRecord>::Iter iter(fRecords, SkTLList<DiffRecord>::Iter::kHead_IterStart);
278 DiffRecord* currentRecord = iter.get();
279
zachr@google.coma95959c2013-07-08 15:04:45 +0000280 if (useJSONP) {
281 stream.writeText("var SkPDiffRecords = {\n");
zachr@google.com55173f22013-07-25 17:22:58 +0000282 } else {
zachr@google.coma95959c2013-07-08 15:04:45 +0000283 stream.writeText("{\n");
284 }
epoger54f1ad82014-07-02 07:43:04 -0700285
286 // TODO(djsollen): Would it be better to use the jsoncpp library to write out the JSON?
287 // This manual approach is probably more efficient, but it sure is ugly.
288 // See http://skbug.com/2713 ('make skpdiff use jsoncpp library to write out
289 // JSON output, instead of manual writeText() calls?')
zachr@google.com945708a2013-07-02 19:55:32 +0000290 stream.writeText(" \"records\": [\n");
291 while (NULL != currentRecord) {
292 stream.writeText(" {\n");
293
zachr@google.coma479aa12013-08-02 15:54:30 +0000294 SkString baselineAbsPath = get_absolute_path(currentRecord->fBaselinePath);
295 SkString testAbsPath = get_absolute_path(currentRecord->fTestPath);
296
djsollen@google.com1e391b52013-10-16 15:00:11 +0000297 stream.writeText(" \"commonName\": \"");
djsollen@google.com513a7bf2013-11-07 19:24:06 +0000298 stream.writeText(currentRecord->fCommonName.c_str());
djsollen@google.com1e391b52013-10-16 15:00:11 +0000299 stream.writeText("\",\n");
300
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000301 stream.writeText(" \"differencePath\": \"");
epoger54f1ad82014-07-02 07:43:04 -0700302 stream.writeText(get_absolute_path(currentRecord->fAlphaMaskPath).c_str());
303 stream.writeText("\",\n");
304
305 stream.writeText(" \"rgbDiffPath\": \"");
306 stream.writeText(get_absolute_path(currentRecord->fRgbDiffPath).c_str());
307 stream.writeText("\",\n");
308
309 stream.writeText(" \"whiteDiffPath\": \"");
310 stream.writeText(get_absolute_path(currentRecord->fWhiteDiffPath).c_str());
djsollen@google.comcbbf1ca2013-10-16 18:36:49 +0000311 stream.writeText("\",\n");
312
zachr@google.com945708a2013-07-02 19:55:32 +0000313 stream.writeText(" \"baselinePath\": \"");
zachr@google.coma479aa12013-08-02 15:54:30 +0000314 stream.writeText(baselineAbsPath.c_str());
zachr@google.com945708a2013-07-02 19:55:32 +0000315 stream.writeText("\",\n");
316
317 stream.writeText(" \"testPath\": \"");
zachr@google.coma479aa12013-08-02 15:54:30 +0000318 stream.writeText(testAbsPath.c_str());
zachr@google.com945708a2013-07-02 19:55:32 +0000319 stream.writeText("\",\n");
320
epoger54f1ad82014-07-02 07:43:04 -0700321 stream.writeText(" \"width\": ");
322 stream.writeDecAsText(currentRecord->fSize.width());
323 stream.writeText(",\n");
324 stream.writeText(" \"height\": ");
325 stream.writeDecAsText(currentRecord->fSize.height());
326 stream.writeText(",\n");
327
328 stream.writeText(" \"maxRedDiff\": ");
329 stream.writeDecAsText(currentRecord->fMaxRedDiff);
330 stream.writeText(",\n");
331 stream.writeText(" \"maxGreenDiff\": ");
332 stream.writeDecAsText(currentRecord->fMaxGreenDiff);
333 stream.writeText(",\n");
334 stream.writeText(" \"maxBlueDiff\": ");
335 stream.writeDecAsText(currentRecord->fMaxBlueDiff);
336 stream.writeText(",\n");
337
zachr@google.com945708a2013-07-02 19:55:32 +0000338 stream.writeText(" \"diffs\": [\n");
339 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) {
340 DiffData& data = currentRecord->fDiffs[diffIndex];
341 stream.writeText(" {\n");
342
343 stream.writeText(" \"differName\": \"");
344 stream.writeText(data.fDiffName);
345 stream.writeText("\",\n");
346
347 stream.writeText(" \"result\": ");
djsollen@google.comefc51b72013-11-12 18:29:17 +0000348 stream.writeScalarAsText((SkScalar)data.fResult.result);
zachr@google.com945708a2013-07-02 19:55:32 +0000349 stream.writeText(",\n");
350
djsollen@google.comefc51b72013-11-12 18:29:17 +0000351 stream.writeText(" \"pointsOfInterest\": ");
352 stream.writeDecAsText(data.fResult.poiCount);
353 stream.writeText("\n");
zachr@google.com945708a2013-07-02 19:55:32 +0000354
zachr@google.com945708a2013-07-02 19:55:32 +0000355 stream.writeText(" }");
356
357 // JSON does not allow trailing commas
zachr@google.com55173f22013-07-25 17:22:58 +0000358 if (diffIndex + 1 < currentRecord->fDiffs.count()) {
zachr@google.com945708a2013-07-02 19:55:32 +0000359 stream.writeText(",");
360 }
361 stream.writeText(" \n");
362 }
363 stream.writeText(" ]\n");
364
365 stream.writeText(" }");
366
djsollen@google.comefc51b72013-11-12 18:29:17 +0000367 currentRecord = iter.next();
368
zachr@google.com945708a2013-07-02 19:55:32 +0000369 // JSON does not allow trailing commas
djsollen@google.comefc51b72013-11-12 18:29:17 +0000370 if (NULL != currentRecord) {
zachr@google.com945708a2013-07-02 19:55:32 +0000371 stream.writeText(",");
372 }
373 stream.writeText("\n");
zachr@google.com945708a2013-07-02 19:55:32 +0000374 }
375 stream.writeText(" ]\n");
zachr@google.coma95959c2013-07-08 15:04:45 +0000376 if (useJSONP) {
377 stream.writeText("};\n");
zachr@google.com55173f22013-07-25 17:22:58 +0000378 } else {
zachr@google.coma95959c2013-07-08 15:04:45 +0000379 stream.writeText("}\n");
380 }
zachr@google.com945708a2013-07-02 19:55:32 +0000381}
edisonn@google.comc93c8ac2013-07-22 15:24:26 +0000382
383void SkDiffContext::outputCsv(SkWStream& stream) {
384 SkTDict<int> columns(2);
385 int cntColumns = 0;
386
387 stream.writeText("key");
388
djsollen@google.comefc51b72013-11-12 18:29:17 +0000389 SkTLList<DiffRecord>::Iter iter(fRecords, SkTLList<DiffRecord>::Iter::kHead_IterStart);
390 DiffRecord* currentRecord = iter.get();
edisonn@google.comc93c8ac2013-07-22 15:24:26 +0000391
392 // Write CSV header and create a dictionary of all columns.
393 while (NULL != currentRecord) {
394 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) {
395 DiffData& data = currentRecord->fDiffs[diffIndex];
396 if (!columns.find(data.fDiffName)) {
397 columns.set(data.fDiffName, cntColumns);
398 stream.writeText(", ");
399 stream.writeText(data.fDiffName);
400 cntColumns++;
401 }
402 }
djsollen@google.comefc51b72013-11-12 18:29:17 +0000403 currentRecord = iter.next();
edisonn@google.comc93c8ac2013-07-22 15:24:26 +0000404 }
405 stream.writeText("\n");
406
407 double values[100];
408 SkASSERT(cntColumns < 100); // Make the array larger, if we ever have so many diff types.
409
djsollen@google.comefc51b72013-11-12 18:29:17 +0000410 SkTLList<DiffRecord>::Iter iter2(fRecords, SkTLList<DiffRecord>::Iter::kHead_IterStart);
411 currentRecord = iter2.get();
edisonn@google.comc93c8ac2013-07-22 15:24:26 +0000412 while (NULL != currentRecord) {
413 for (int i = 0; i < cntColumns; i++) {
414 values[i] = -1;
415 }
416
417 for (int diffIndex = 0; diffIndex < currentRecord->fDiffs.count(); diffIndex++) {
418 DiffData& data = currentRecord->fDiffs[diffIndex];
419 int index = -1;
420 SkAssertResult(columns.find(data.fDiffName, &index));
421 SkASSERT(index >= 0 && index < cntColumns);
djsollen@google.comefc51b72013-11-12 18:29:17 +0000422 values[index] = data.fResult.result;
edisonn@google.comc93c8ac2013-07-22 15:24:26 +0000423 }
424
425 const char* filename = currentRecord->fBaselinePath.c_str() +
426 strlen(currentRecord->fBaselinePath.c_str()) - 1;
427 while (filename > currentRecord->fBaselinePath.c_str() && *(filename - 1) != '/') {
428 filename--;
429 }
430
431 stream.writeText(filename);
432
433 for (int i = 0; i < cntColumns; i++) {
434 SkString str;
435 str.printf(", %f", values[i]);
436 stream.writeText(str.c_str());
437 }
438 stream.writeText("\n");
439
djsollen@google.comefc51b72013-11-12 18:29:17 +0000440 currentRecord = iter2.next();
edisonn@google.comc93c8ac2013-07-22 15:24:26 +0000441 }
442}