gfx/skia/trunk/src/core/SkRegion_path.cpp

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1
michael@0 2 /*
michael@0 3 * Copyright 2006 The Android Open Source Project
michael@0 4 *
michael@0 5 * Use of this source code is governed by a BSD-style license that can be
michael@0 6 * found in the LICENSE file.
michael@0 7 */
michael@0 8
michael@0 9
michael@0 10 #include "SkRegionPriv.h"
michael@0 11 #include "SkBlitter.h"
michael@0 12 #include "SkScan.h"
michael@0 13 #include "SkTDArray.h"
michael@0 14 #include "SkPath.h"
michael@0 15
michael@0 16 class SkRgnBuilder : public SkBlitter {
michael@0 17 public:
michael@0 18 SkRgnBuilder();
michael@0 19 virtual ~SkRgnBuilder();
michael@0 20
michael@0 21 // returns true if it could allocate the working storage needed
michael@0 22 bool init(int maxHeight, int maxTransitions, bool pathIsInverse);
michael@0 23
michael@0 24 void done() {
michael@0 25 if (fCurrScanline != NULL) {
michael@0 26 fCurrScanline->fXCount = (SkRegion::RunType)((int)(fCurrXPtr - fCurrScanline->firstX()));
michael@0 27 if (!this->collapsWithPrev()) { // flush the last line
michael@0 28 fCurrScanline = fCurrScanline->nextScanline();
michael@0 29 }
michael@0 30 }
michael@0 31 }
michael@0 32
michael@0 33 int computeRunCount() const;
michael@0 34 void copyToRect(SkIRect*) const;
michael@0 35 void copyToRgn(SkRegion::RunType runs[]) const;
michael@0 36
michael@0 37 virtual void blitH(int x, int y, int width);
michael@0 38
michael@0 39 #ifdef SK_DEBUG
michael@0 40 void dump() const {
michael@0 41 SkDebugf("SkRgnBuilder: Top = %d\n", fTop);
michael@0 42 const Scanline* line = (Scanline*)fStorage;
michael@0 43 while (line < fCurrScanline) {
michael@0 44 SkDebugf("SkRgnBuilder::Scanline: LastY=%d, fXCount=%d", line->fLastY, line->fXCount);
michael@0 45 for (int i = 0; i < line->fXCount; i++) {
michael@0 46 SkDebugf(" %d", line->firstX()[i]);
michael@0 47 }
michael@0 48 SkDebugf("\n");
michael@0 49
michael@0 50 line = line->nextScanline();
michael@0 51 }
michael@0 52 }
michael@0 53 #endif
michael@0 54 private:
michael@0 55 /*
michael@0 56 * Scanline mimics a row in the region, nearly. A row in a region is:
michael@0 57 * [Bottom IntervalCount [L R]... Sentinel]
michael@0 58 * while a Scanline is
michael@0 59 * [LastY XCount [L R]... uninitialized]
michael@0 60 * The two are the same length (which is good), but we have to transmute
michael@0 61 * the scanline a little when we convert it to a region-row.
michael@0 62 *
michael@0 63 * Potentially we could recode this to exactly match the row format, in
michael@0 64 * which case copyToRgn() could be a single memcpy. Not sure that is worth
michael@0 65 * the effort.
michael@0 66 */
michael@0 67 struct Scanline {
michael@0 68 SkRegion::RunType fLastY;
michael@0 69 SkRegion::RunType fXCount;
michael@0 70
michael@0 71 SkRegion::RunType* firstX() const { return (SkRegion::RunType*)(this + 1); }
michael@0 72 Scanline* nextScanline() const {
michael@0 73 // add final +1 for the x-sentinel
michael@0 74 return (Scanline*)((SkRegion::RunType*)(this + 1) + fXCount + 1);
michael@0 75 }
michael@0 76 };
michael@0 77 SkRegion::RunType* fStorage;
michael@0 78 Scanline* fCurrScanline;
michael@0 79 Scanline* fPrevScanline;
michael@0 80 // points at next avialable x[] in fCurrScanline
michael@0 81 SkRegion::RunType* fCurrXPtr;
michael@0 82 SkRegion::RunType fTop; // first Y value
michael@0 83
michael@0 84 int fStorageCount;
michael@0 85
michael@0 86 bool collapsWithPrev() {
michael@0 87 if (fPrevScanline != NULL &&
michael@0 88 fPrevScanline->fLastY + 1 == fCurrScanline->fLastY &&
michael@0 89 fPrevScanline->fXCount == fCurrScanline->fXCount &&
michael@0 90 !memcmp(fPrevScanline->firstX(),
michael@0 91 fCurrScanline->firstX(),
michael@0 92 fCurrScanline->fXCount * sizeof(SkRegion::RunType)))
michael@0 93 {
michael@0 94 // update the height of fPrevScanline
michael@0 95 fPrevScanline->fLastY = fCurrScanline->fLastY;
michael@0 96 return true;
michael@0 97 }
michael@0 98 return false;
michael@0 99 }
michael@0 100 };
michael@0 101
michael@0 102 SkRgnBuilder::SkRgnBuilder()
michael@0 103 : fStorage(NULL) {
michael@0 104 }
michael@0 105
michael@0 106 SkRgnBuilder::~SkRgnBuilder() {
michael@0 107 sk_free(fStorage);
michael@0 108 }
michael@0 109
michael@0 110 bool SkRgnBuilder::init(int maxHeight, int maxTransitions, bool pathIsInverse) {
michael@0 111 if ((maxHeight | maxTransitions) < 0) {
michael@0 112 return false;
michael@0 113 }
michael@0 114
michael@0 115 if (pathIsInverse) {
michael@0 116 // allow for additional X transitions to "invert" each scanline
michael@0 117 // [ L' ... normal transitions ... R' ]
michael@0 118 //
michael@0 119 maxTransitions += 2;
michael@0 120 }
michael@0 121
michael@0 122 // compute the count with +1 and +3 slop for the working buffer
michael@0 123 int64_t count = sk_64_mul(maxHeight + 1, 3 + maxTransitions);
michael@0 124
michael@0 125 if (pathIsInverse) {
michael@0 126 // allow for two "empty" rows for the top and bottom
michael@0 127 // [ Y, 1, L, R, S] == 5 (*2 for top and bottom)
michael@0 128 count += 10;
michael@0 129 }
michael@0 130
michael@0 131 if (count < 0 || !sk_64_isS32(count)) {
michael@0 132 return false;
michael@0 133 }
michael@0 134 fStorageCount = sk_64_asS32(count);
michael@0 135
michael@0 136 int64_t size = sk_64_mul(fStorageCount, sizeof(SkRegion::RunType));
michael@0 137 if (size < 0 || !sk_64_isS32(size)) {
michael@0 138 return false;
michael@0 139 }
michael@0 140
michael@0 141 fStorage = (SkRegion::RunType*)sk_malloc_flags(sk_64_asS32(size), 0);
michael@0 142 if (NULL == fStorage) {
michael@0 143 return false;
michael@0 144 }
michael@0 145
michael@0 146 fCurrScanline = NULL; // signal empty collection
michael@0 147 fPrevScanline = NULL; // signal first scanline
michael@0 148 return true;
michael@0 149 }
michael@0 150
michael@0 151 void SkRgnBuilder::blitH(int x, int y, int width) {
michael@0 152 if (fCurrScanline == NULL) { // first time
michael@0 153 fTop = (SkRegion::RunType)(y);
michael@0 154 fCurrScanline = (Scanline*)fStorage;
michael@0 155 fCurrScanline->fLastY = (SkRegion::RunType)(y);
michael@0 156 fCurrXPtr = fCurrScanline->firstX();
michael@0 157 } else {
michael@0 158 SkASSERT(y >= fCurrScanline->fLastY);
michael@0 159
michael@0 160 if (y > fCurrScanline->fLastY) {
michael@0 161 // if we get here, we're done with fCurrScanline
michael@0 162 fCurrScanline->fXCount = (SkRegion::RunType)((int)(fCurrXPtr - fCurrScanline->firstX()));
michael@0 163
michael@0 164 int prevLastY = fCurrScanline->fLastY;
michael@0 165 if (!this->collapsWithPrev()) {
michael@0 166 fPrevScanline = fCurrScanline;
michael@0 167 fCurrScanline = fCurrScanline->nextScanline();
michael@0 168
michael@0 169 }
michael@0 170 if (y - 1 > prevLastY) { // insert empty run
michael@0 171 fCurrScanline->fLastY = (SkRegion::RunType)(y - 1);
michael@0 172 fCurrScanline->fXCount = 0;
michael@0 173 fCurrScanline = fCurrScanline->nextScanline();
michael@0 174 }
michael@0 175 // setup for the new curr line
michael@0 176 fCurrScanline->fLastY = (SkRegion::RunType)(y);
michael@0 177 fCurrXPtr = fCurrScanline->firstX();
michael@0 178 }
michael@0 179 }
michael@0 180 // check if we should extend the current run, or add a new one
michael@0 181 if (fCurrXPtr > fCurrScanline->firstX() && fCurrXPtr[-1] == x) {
michael@0 182 fCurrXPtr[-1] = (SkRegion::RunType)(x + width);
michael@0 183 } else {
michael@0 184 fCurrXPtr[0] = (SkRegion::RunType)(x);
michael@0 185 fCurrXPtr[1] = (SkRegion::RunType)(x + width);
michael@0 186 fCurrXPtr += 2;
michael@0 187 }
michael@0 188 SkASSERT(fCurrXPtr - fStorage < fStorageCount);
michael@0 189 }
michael@0 190
michael@0 191 int SkRgnBuilder::computeRunCount() const {
michael@0 192 if (fCurrScanline == NULL) {
michael@0 193 return 0;
michael@0 194 }
michael@0 195
michael@0 196 const SkRegion::RunType* line = fStorage;
michael@0 197 const SkRegion::RunType* stop = (const SkRegion::RunType*)fCurrScanline;
michael@0 198
michael@0 199 return 2 + (int)(stop - line);
michael@0 200 }
michael@0 201
michael@0 202 void SkRgnBuilder::copyToRect(SkIRect* r) const {
michael@0 203 SkASSERT(fCurrScanline != NULL);
michael@0 204 // A rect's scanline is [bottom intervals left right sentinel] == 5
michael@0 205 SkASSERT((const SkRegion::RunType*)fCurrScanline - fStorage == 5);
michael@0 206
michael@0 207 const Scanline* line = (const Scanline*)fStorage;
michael@0 208 SkASSERT(line->fXCount == 2);
michael@0 209
michael@0 210 r->set(line->firstX()[0], fTop, line->firstX()[1], line->fLastY + 1);
michael@0 211 }
michael@0 212
michael@0 213 void SkRgnBuilder::copyToRgn(SkRegion::RunType runs[]) const {
michael@0 214 SkASSERT(fCurrScanline != NULL);
michael@0 215 SkASSERT((const SkRegion::RunType*)fCurrScanline - fStorage > 4);
michael@0 216
michael@0 217 const Scanline* line = (const Scanline*)fStorage;
michael@0 218 const Scanline* stop = fCurrScanline;
michael@0 219
michael@0 220 *runs++ = fTop;
michael@0 221 do {
michael@0 222 *runs++ = (SkRegion::RunType)(line->fLastY + 1);
michael@0 223 int count = line->fXCount;
michael@0 224 *runs++ = count >> 1; // intervalCount
michael@0 225 if (count) {
michael@0 226 memcpy(runs, line->firstX(), count * sizeof(SkRegion::RunType));
michael@0 227 runs += count;
michael@0 228 }
michael@0 229 *runs++ = SkRegion::kRunTypeSentinel;
michael@0 230 line = line->nextScanline();
michael@0 231 } while (line < stop);
michael@0 232 SkASSERT(line == stop);
michael@0 233 *runs = SkRegion::kRunTypeSentinel;
michael@0 234 }
michael@0 235
michael@0 236 static unsigned verb_to_initial_last_index(unsigned verb) {
michael@0 237 static const uint8_t gPathVerbToInitialLastIndex[] = {
michael@0 238 0, // kMove_Verb
michael@0 239 1, // kLine_Verb
michael@0 240 2, // kQuad_Verb
michael@0 241 2, // kConic_Verb
michael@0 242 3, // kCubic_Verb
michael@0 243 0, // kClose_Verb
michael@0 244 0 // kDone_Verb
michael@0 245 };
michael@0 246 SkASSERT((unsigned)verb < SK_ARRAY_COUNT(gPathVerbToInitialLastIndex));
michael@0 247 return gPathVerbToInitialLastIndex[verb];
michael@0 248 }
michael@0 249
michael@0 250 static unsigned verb_to_max_edges(unsigned verb) {
michael@0 251 static const uint8_t gPathVerbToMaxEdges[] = {
michael@0 252 0, // kMove_Verb
michael@0 253 1, // kLine_Verb
michael@0 254 2, // kQuad_VerbB
michael@0 255 2, // kConic_VerbB
michael@0 256 3, // kCubic_Verb
michael@0 257 0, // kClose_Verb
michael@0 258 0 // kDone_Verb
michael@0 259 };
michael@0 260 SkASSERT((unsigned)verb < SK_ARRAY_COUNT(gPathVerbToMaxEdges));
michael@0 261 return gPathVerbToMaxEdges[verb];
michael@0 262 }
michael@0 263
michael@0 264
michael@0 265 static int count_path_runtype_values(const SkPath& path, int* itop, int* ibot) {
michael@0 266 SkPath::Iter iter(path, true);
michael@0 267 SkPoint pts[4];
michael@0 268 SkPath::Verb verb;
michael@0 269
michael@0 270 int maxEdges = 0;
michael@0 271 SkScalar top = SkIntToScalar(SK_MaxS16);
michael@0 272 SkScalar bot = SkIntToScalar(SK_MinS16);
michael@0 273
michael@0 274 while ((verb = iter.next(pts, false)) != SkPath::kDone_Verb) {
michael@0 275 maxEdges += verb_to_max_edges(verb);
michael@0 276
michael@0 277 int lastIndex = verb_to_initial_last_index(verb);
michael@0 278 if (lastIndex > 0) {
michael@0 279 for (int i = 1; i <= lastIndex; i++) {
michael@0 280 if (top > pts[i].fY) {
michael@0 281 top = pts[i].fY;
michael@0 282 } else if (bot < pts[i].fY) {
michael@0 283 bot = pts[i].fY;
michael@0 284 }
michael@0 285 }
michael@0 286 } else if (SkPath::kMove_Verb == verb) {
michael@0 287 if (top > pts[0].fY) {
michael@0 288 top = pts[0].fY;
michael@0 289 } else if (bot < pts[0].fY) {
michael@0 290 bot = pts[0].fY;
michael@0 291 }
michael@0 292 }
michael@0 293 }
michael@0 294 SkASSERT(top <= bot);
michael@0 295
michael@0 296 *itop = SkScalarRoundToInt(top);
michael@0 297 *ibot = SkScalarRoundToInt(bot);
michael@0 298 return maxEdges;
michael@0 299 }
michael@0 300
michael@0 301 bool SkRegion::setPath(const SkPath& path, const SkRegion& clip) {
michael@0 302 SkDEBUGCODE(this->validate();)
michael@0 303
michael@0 304 if (clip.isEmpty()) {
michael@0 305 return this->setEmpty();
michael@0 306 }
michael@0 307
michael@0 308 if (path.isEmpty()) {
michael@0 309 if (path.isInverseFillType()) {
michael@0 310 return this->set(clip);
michael@0 311 } else {
michael@0 312 return this->setEmpty();
michael@0 313 }
michael@0 314 }
michael@0 315
michael@0 316 // compute worst-case rgn-size for the path
michael@0 317 int pathTop, pathBot;
michael@0 318 int pathTransitions = count_path_runtype_values(path, &pathTop, &pathBot);
michael@0 319 int clipTop, clipBot;
michael@0 320 int clipTransitions;
michael@0 321
michael@0 322 clipTransitions = clip.count_runtype_values(&clipTop, &clipBot);
michael@0 323
michael@0 324 int top = SkMax32(pathTop, clipTop);
michael@0 325 int bot = SkMin32(pathBot, clipBot);
michael@0 326
michael@0 327 if (top >= bot)
michael@0 328 return this->setEmpty();
michael@0 329
michael@0 330 SkRgnBuilder builder;
michael@0 331
michael@0 332 if (!builder.init(bot - top,
michael@0 333 SkMax32(pathTransitions, clipTransitions),
michael@0 334 path.isInverseFillType())) {
michael@0 335 // can't allocate working space, so return false
michael@0 336 return this->setEmpty();
michael@0 337 }
michael@0 338
michael@0 339 SkScan::FillPath(path, clip, &builder);
michael@0 340 builder.done();
michael@0 341
michael@0 342 int count = builder.computeRunCount();
michael@0 343 if (count == 0) {
michael@0 344 return this->setEmpty();
michael@0 345 } else if (count == kRectRegionRuns) {
michael@0 346 builder.copyToRect(&fBounds);
michael@0 347 this->setRect(fBounds);
michael@0 348 } else {
michael@0 349 SkRegion tmp;
michael@0 350
michael@0 351 tmp.fRunHead = RunHead::Alloc(count);
michael@0 352 builder.copyToRgn(tmp.fRunHead->writable_runs());
michael@0 353 tmp.fRunHead->computeRunBounds(&tmp.fBounds);
michael@0 354 this->swap(tmp);
michael@0 355 }
michael@0 356 SkDEBUGCODE(this->validate();)
michael@0 357 return true;
michael@0 358 }
michael@0 359
michael@0 360 /////////////////////////////////////////////////////////////////////////////////////////////////
michael@0 361 /////////////////////////////////////////////////////////////////////////////////////////////////
michael@0 362
michael@0 363 struct Edge {
michael@0 364 enum {
michael@0 365 kY0Link = 0x01,
michael@0 366 kY1Link = 0x02,
michael@0 367
michael@0 368 kCompleteLink = (kY0Link | kY1Link)
michael@0 369 };
michael@0 370
michael@0 371 SkRegion::RunType fX;
michael@0 372 SkRegion::RunType fY0, fY1;
michael@0 373 uint8_t fFlags;
michael@0 374 Edge* fNext;
michael@0 375
michael@0 376 void set(int x, int y0, int y1) {
michael@0 377 SkASSERT(y0 != y1);
michael@0 378
michael@0 379 fX = (SkRegion::RunType)(x);
michael@0 380 fY0 = (SkRegion::RunType)(y0);
michael@0 381 fY1 = (SkRegion::RunType)(y1);
michael@0 382 fFlags = 0;
michael@0 383 SkDEBUGCODE(fNext = NULL;)
michael@0 384 }
michael@0 385
michael@0 386 int top() const {
michael@0 387 return SkFastMin32(fY0, fY1);
michael@0 388 }
michael@0 389 };
michael@0 390
michael@0 391 static void find_link(Edge* base, Edge* stop) {
michael@0 392 SkASSERT(base < stop);
michael@0 393
michael@0 394 if (base->fFlags == Edge::kCompleteLink) {
michael@0 395 SkASSERT(base->fNext);
michael@0 396 return;
michael@0 397 }
michael@0 398
michael@0 399 SkASSERT(base + 1 < stop);
michael@0 400
michael@0 401 int y0 = base->fY0;
michael@0 402 int y1 = base->fY1;
michael@0 403
michael@0 404 Edge* e = base;
michael@0 405 if ((base->fFlags & Edge::kY0Link) == 0) {
michael@0 406 for (;;) {
michael@0 407 e += 1;
michael@0 408 if ((e->fFlags & Edge::kY1Link) == 0 && y0 == e->fY1) {
michael@0 409 SkASSERT(NULL == e->fNext);
michael@0 410 e->fNext = base;
michael@0 411 e->fFlags = SkToU8(e->fFlags | Edge::kY1Link);
michael@0 412 break;
michael@0 413 }
michael@0 414 }
michael@0 415 }
michael@0 416
michael@0 417 e = base;
michael@0 418 if ((base->fFlags & Edge::kY1Link) == 0) {
michael@0 419 for (;;) {
michael@0 420 e += 1;
michael@0 421 if ((e->fFlags & Edge::kY0Link) == 0 && y1 == e->fY0) {
michael@0 422 SkASSERT(NULL == base->fNext);
michael@0 423 base->fNext = e;
michael@0 424 e->fFlags = SkToU8(e->fFlags | Edge::kY0Link);
michael@0 425 break;
michael@0 426 }
michael@0 427 }
michael@0 428 }
michael@0 429
michael@0 430 base->fFlags = Edge::kCompleteLink;
michael@0 431 }
michael@0 432
michael@0 433 static int extract_path(Edge* edge, Edge* stop, SkPath* path) {
michael@0 434 while (0 == edge->fFlags) {
michael@0 435 edge++; // skip over "used" edges
michael@0 436 }
michael@0 437
michael@0 438 SkASSERT(edge < stop);
michael@0 439
michael@0 440 Edge* base = edge;
michael@0 441 Edge* prev = edge;
michael@0 442 edge = edge->fNext;
michael@0 443 SkASSERT(edge != base);
michael@0 444
michael@0 445 int count = 1;
michael@0 446 path->moveTo(SkIntToScalar(prev->fX), SkIntToScalar(prev->fY0));
michael@0 447 prev->fFlags = 0;
michael@0 448 do {
michael@0 449 if (prev->fX != edge->fX || prev->fY1 != edge->fY0) { // skip collinear
michael@0 450 path->lineTo(SkIntToScalar(prev->fX), SkIntToScalar(prev->fY1)); // V
michael@0 451 path->lineTo(SkIntToScalar(edge->fX), SkIntToScalar(edge->fY0)); // H
michael@0 452 }
michael@0 453 prev = edge;
michael@0 454 edge = edge->fNext;
michael@0 455 count += 1;
michael@0 456 prev->fFlags = 0;
michael@0 457 } while (edge != base);
michael@0 458 path->lineTo(SkIntToScalar(prev->fX), SkIntToScalar(prev->fY1)); // V
michael@0 459 path->close();
michael@0 460 return count;
michael@0 461 }
michael@0 462
michael@0 463 #include "SkTSearch.h"
michael@0 464
michael@0 465 static int EdgeProc(const Edge* a, const Edge* b) {
michael@0 466 return (a->fX == b->fX) ? a->top() - b->top() : a->fX - b->fX;
michael@0 467 }
michael@0 468
michael@0 469 bool SkRegion::getBoundaryPath(SkPath* path) const {
michael@0 470 // path could safely be NULL if we're empty, but the caller shouldn't
michael@0 471 // *know* that
michael@0 472 SkASSERT(path);
michael@0 473
michael@0 474 if (this->isEmpty()) {
michael@0 475 return false;
michael@0 476 }
michael@0 477
michael@0 478 const SkIRect& bounds = this->getBounds();
michael@0 479
michael@0 480 if (this->isRect()) {
michael@0 481 SkRect r;
michael@0 482 r.set(bounds); // this converts the ints to scalars
michael@0 483 path->addRect(r);
michael@0 484 return true;
michael@0 485 }
michael@0 486
michael@0 487 SkRegion::Iterator iter(*this);
michael@0 488 SkTDArray<Edge> edges;
michael@0 489
michael@0 490 for (const SkIRect& r = iter.rect(); !iter.done(); iter.next()) {
michael@0 491 Edge* edge = edges.append(2);
michael@0 492 edge[0].set(r.fLeft, r.fBottom, r.fTop);
michael@0 493 edge[1].set(r.fRight, r.fTop, r.fBottom);
michael@0 494 }
michael@0 495 qsort(edges.begin(), edges.count(), sizeof(Edge), SkCastForQSort(EdgeProc));
michael@0 496
michael@0 497 int count = edges.count();
michael@0 498 Edge* start = edges.begin();
michael@0 499 Edge* stop = start + count;
michael@0 500 Edge* e;
michael@0 501
michael@0 502 for (e = start; e != stop; e++) {
michael@0 503 find_link(e, stop);
michael@0 504 }
michael@0 505
michael@0 506 #ifdef SK_DEBUG
michael@0 507 for (e = start; e != stop; e++) {
michael@0 508 SkASSERT(e->fNext != NULL);
michael@0 509 SkASSERT(e->fFlags == Edge::kCompleteLink);
michael@0 510 }
michael@0 511 #endif
michael@0 512
michael@0 513 path->incReserve(count << 1);
michael@0 514 do {
michael@0 515 SkASSERT(count > 1);
michael@0 516 count -= extract_path(start, stop, path);
michael@0 517 } while (count > 0);
michael@0 518
michael@0 519 return true;
michael@0 520 }

mercurial