SkDashPath.cpp (17871B)
1 /* 2 * Copyright 2014 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 "src/utils/SkDashPathPriv.h" 9 10 #include "include/core/SkPaint.h" 11 #include "include/core/SkPath.h" 12 #include "include/core/SkPathBuilder.h" 13 #include "include/core/SkPathMeasure.h" 14 #include "include/core/SkPoint.h" 15 #include "include/core/SkRect.h" 16 #include "include/core/SkScalar.h" 17 #include "include/core/SkSpan.h" 18 #include "include/core/SkStrokeRec.h" 19 #include "include/core/SkTypes.h" 20 #include "include/private/base/SkAlign.h" 21 #include "include/private/base/SkFloatingPoint.h" 22 #include "include/private/base/SkTo.h" 23 #include "src/core/SkPathEffectBase.h" 24 #include "src/core/SkPointPriv.h" 25 26 #include <algorithm> 27 #include <cmath> 28 #include <cstddef> 29 #include <cstdint> 30 31 static inline int is_even(int x) { 32 return !(x & 1); 33 } 34 35 static SkScalar find_first_interval(SkSpan<const SkScalar> intervals, SkScalar phase, 36 size_t* index) { 37 for (size_t i = 0; i < intervals.size(); ++i) { 38 SkScalar gap = intervals[i]; 39 if (phase > gap || (phase == gap && gap)) { 40 phase -= gap; 41 } else { 42 *index = i; 43 return gap - phase; 44 } 45 } 46 // If we get here, phase "appears" to be larger than our length. This 47 // shouldn't happen with perfect precision, but we can accumulate errors 48 // during the initial length computation (rounding can make our sum be too 49 // big or too small. In that event, we just have to eat the error here. 50 *index = 0; 51 return intervals[0]; 52 } 53 54 void SkDashPath::CalcDashParameters(SkScalar phase, SkSpan<const SkScalar> intervals, 55 SkScalar* initialDashLength, size_t* initialDashIndex, 56 SkScalar* intervalLength, SkScalar* adjustedPhase) { 57 SkScalar len = 0; 58 for (SkScalar interval : intervals) { 59 len += interval; 60 } 61 *intervalLength = len; 62 // Adjust phase to be between 0 and len, "flipping" phase if negative. 63 // e.g., if len is 100, then phase of -20 (or -120) is equivalent to 80 64 if (adjustedPhase) { 65 if (phase < 0) { 66 phase = -phase; 67 if (phase > len) { 68 phase = SkScalarMod(phase, len); 69 } 70 phase = len - phase; 71 72 // Due to finite precision, it's possible that phase == len, 73 // even after the subtract (if len >>> phase), so fix that here. 74 // This fixes http://crbug.com/124652 . 75 SkASSERT(phase <= len); 76 if (phase == len) { 77 phase = 0; 78 } 79 } else if (phase >= len) { 80 phase = SkScalarMod(phase, len); 81 } 82 *adjustedPhase = phase; 83 } 84 SkASSERT(phase >= 0 && phase < len); 85 86 *initialDashLength = find_first_interval(intervals, phase, initialDashIndex); 87 88 SkASSERT(*initialDashLength >= 0); 89 SkASSERT(*initialDashIndex < intervals.size()); 90 } 91 92 static void outset_for_stroke(SkRect* rect, const SkStrokeRec& rec) { 93 SkScalar radius = SkScalarHalf(rec.getWidth()); 94 if (0 == radius) { 95 radius = SK_Scalar1; // hairlines 96 } 97 if (SkPaint::kMiter_Join == rec.getJoin()) { 98 radius *= rec.getMiter(); 99 } 100 rect->outset(radius, radius); 101 } 102 103 // If line is zero-length, bump out the end by a tiny amount 104 // to draw endcaps. The bump factor is sized so that 105 // SkPoint::Distance() computes a non-zero length. 106 // Offsets SK_ScalarNearlyZero or smaller create empty paths when Iter measures length. 107 // Large values are scaled by SK_ScalarNearlyZero so significant bits change. 108 static void adjust_zero_length_line(SkPoint pts[2]) { 109 SkASSERT(pts[0] == pts[1]); 110 pts[1].fX += std::max(1.001f, pts[1].fX) * SK_ScalarNearlyZero; 111 } 112 113 static bool clip_line(SkPoint pts[2], const SkRect& bounds, SkScalar intervalLength, 114 SkScalar priorPhase) { 115 SkVector dxy = pts[1] - pts[0]; 116 117 // only horizontal or vertical lines 118 if (dxy.fX && dxy.fY) { 119 return false; 120 } 121 int xyOffset = SkToBool(dxy.fY); // 0 to adjust horizontal, 1 to adjust vertical 122 123 SkScalar minXY = (&pts[0].fX)[xyOffset]; 124 SkScalar maxXY = (&pts[1].fX)[xyOffset]; 125 bool swapped = maxXY < minXY; 126 if (swapped) { 127 using std::swap; 128 swap(minXY, maxXY); 129 } 130 131 SkASSERT(minXY <= maxXY); 132 SkScalar leftTop = (&bounds.fLeft)[xyOffset]; 133 SkScalar rightBottom = (&bounds.fRight)[xyOffset]; 134 if (maxXY < leftTop || minXY > rightBottom) { 135 return false; 136 } 137 138 // Now we actually perform the chop, removing the excess to the left/top and 139 // right/bottom of the bounds (keeping our new line "in phase" with the dash, 140 // hence the (mod intervalLength). 141 142 if (minXY < leftTop) { 143 minXY = leftTop - SkScalarMod(leftTop - minXY, intervalLength); 144 if (!swapped) { 145 minXY -= priorPhase; // for rectangles, adjust by prior phase 146 } 147 } 148 if (maxXY > rightBottom) { 149 maxXY = rightBottom + SkScalarMod(maxXY - rightBottom, intervalLength); 150 if (swapped) { 151 maxXY += priorPhase; // for rectangles, adjust by prior phase 152 } 153 } 154 155 SkASSERT(maxXY >= minXY); 156 if (swapped) { 157 using std::swap; 158 swap(minXY, maxXY); 159 } 160 (&pts[0].fX)[xyOffset] = minXY; 161 (&pts[1].fX)[xyOffset] = maxXY; 162 163 if (minXY == maxXY) { 164 adjust_zero_length_line(pts); 165 } 166 return true; 167 } 168 169 // Handles only lines and rects. 170 // If cull_path() returns true, builder is the new smaller path, 171 // otherwise builder may have been changed but you should ignore it. 172 static bool cull_path(const SkPath& srcPath, const SkStrokeRec& rec, 173 const SkRect* cullRect, SkScalar intervalLength, SkPathBuilder* builder) { 174 if (!cullRect) { 175 SkPoint pts[2]; 176 if (srcPath.isLine(pts) && pts[0] == pts[1]) { 177 adjust_zero_length_line(pts); 178 builder->moveTo(pts[0]); 179 builder->lineTo(pts[1]); 180 return true; 181 } 182 return false; 183 } 184 185 SkRect bounds; 186 bounds = *cullRect; 187 outset_for_stroke(&bounds, rec); 188 189 { 190 SkPoint pts[2]; 191 if (srcPath.isLine(pts)) { 192 if (clip_line(pts, bounds, intervalLength, 0)) { 193 builder->moveTo(pts[0]); 194 builder->lineTo(pts[1]); 195 return true; 196 } 197 return false; 198 } 199 } 200 201 if (srcPath.isRect(nullptr)) { 202 // We'll break the rect into four lines, culling each separately. 203 SkPath::Iter iter(srcPath, false); 204 205 std::optional<SkPath::IterRec> it = iter.next(); 206 SkASSERT(it.has_value() && it->fVerb == SkPathVerb::kMove); 207 208 double accum = 0; // Sum of unculled edge lengths to keep the phase correct. 209 // Intentionally a double to minimize the risk of overflow and drift. 210 while ((it = iter.next()) && (it->fVerb == SkPathVerb::kLine)) { 211 // Notice this vector v and accum work with the original unclipped length. 212 SkVector v = it->fPoints[1] - it->fPoints[0]; 213 214 SkPoint pts[2] = {it->fPoints[0], it->fPoints[1]}; 215 if (clip_line(pts, bounds, intervalLength, std::fmod(accum, intervalLength))) { 216 // pts[0] may have just been changed by clip_line(). 217 // If that's not where we ended the previous lineTo(), we need to moveTo() there. 218 auto maybeLast = builder->getLastPt(); 219 if (!maybeLast || *maybeLast != pts[0]) { 220 builder->moveTo(pts[0]); 221 } 222 builder->lineTo(pts[1]); 223 } 224 225 // We either just traveled v.fX horizontally or v.fY vertically. 226 SkASSERT(v.fX == 0 || v.fY == 0); 227 accum += SkScalarAbs(v.fX + v.fY); 228 } 229 return !builder->isEmpty(); 230 } 231 232 return false; 233 } 234 235 class SpecialLineRec { 236 public: 237 bool init(const SkPath& src, SkPathBuilder* dst, SkStrokeRec* rec, 238 int intervalCount, SkScalar intervalLength) { 239 if (rec->isHairlineStyle() || !src.isLine(fPts)) { 240 return false; 241 } 242 243 // can relax this in the future, if we handle square and round caps 244 if (SkPaint::kButt_Cap != rec->getCap()) { 245 return false; 246 } 247 248 SkScalar pathLength = SkPoint::Distance(fPts[0], fPts[1]); 249 250 fTangent = fPts[1] - fPts[0]; 251 if (fTangent.isZero()) { 252 return false; 253 } 254 255 fPathLength = pathLength; 256 fTangent.scale(sk_ieee_float_divide(1.0f, pathLength)); 257 if (!SkIsFinite(fTangent.fX, fTangent.fY)) { 258 return false; 259 } 260 SkPointPriv::RotateCCW(fTangent, &fNormal); 261 fNormal.scale(SkScalarHalf(rec->getWidth())); 262 263 // now estimate how many quads will be added to the path 264 // resulting segments = pathLen * intervalCount / intervalLen 265 // resulting points = 4 * segments 266 267 SkScalar ptCount = pathLength * intervalCount / (float)intervalLength; 268 ptCount = std::min(ptCount, SkDashPath::kMaxDashCount); 269 if (SkIsNaN(ptCount)) { 270 return false; 271 } 272 int n = SkScalarCeilToInt(ptCount) << 2; 273 dst->incReserve(n); 274 275 // we will take care of the stroking 276 rec->setFillStyle(); 277 return true; 278 } 279 280 void addSegment(SkScalar d0, SkScalar d1, SkPathBuilder* path) const { 281 SkASSERT(d0 <= fPathLength); 282 // clamp the segment to our length 283 if (d1 > fPathLength) { 284 d1 = fPathLength; 285 } 286 287 SkScalar x0 = fPts[0].fX + fTangent.fX * d0; 288 SkScalar x1 = fPts[0].fX + fTangent.fX * d1; 289 SkScalar y0 = fPts[0].fY + fTangent.fY * d0; 290 SkScalar y1 = fPts[0].fY + fTangent.fY * d1; 291 292 SkPoint pts[4]; 293 pts[0].set(x0 + fNormal.fX, y0 + fNormal.fY); // moveTo 294 pts[1].set(x1 + fNormal.fX, y1 + fNormal.fY); // lineTo 295 pts[2].set(x1 - fNormal.fX, y1 - fNormal.fY); // lineTo 296 pts[3].set(x0 - fNormal.fX, y0 - fNormal.fY); // lineTo 297 298 path->addPolygon(pts, false); 299 } 300 301 private: 302 SkPoint fPts[2]; 303 SkVector fTangent; 304 SkVector fNormal; 305 SkScalar fPathLength; 306 }; 307 308 309 bool SkDashPath::InternalFilter(SkPathBuilder* dst, const SkPath& src, SkStrokeRec* rec, 310 const SkRect* cullRect, SkSpan<const SkScalar> aIntervals, 311 SkScalar initialDashLength, int32_t initialDashIndex, 312 SkScalar intervalLength, SkScalar startPhase, 313 StrokeRecApplication strokeRecApplication) { 314 const size_t count = aIntervals.size(); 315 // we must always have an even number of intervals 316 SkASSERT(is_even(count)); 317 318 // we do nothing if the src wants to be filled 319 SkStrokeRec::Style style = rec->getStyle(); 320 if (SkStrokeRec::kFill_Style == style || SkStrokeRec::kStrokeAndFill_Style == style) { 321 return false; 322 } 323 324 const SkScalar* intervals = aIntervals.data(); 325 SkScalar dashCount = 0; 326 327 SkPathBuilder builder; 328 SkPath cullPathStorage; 329 const SkPath* srcPtr = &src; 330 if (cull_path(src, *rec, cullRect, intervalLength, &builder)) { 331 // if rect is closed, starts in a dash, and ends in a dash, add the initial join 332 // potentially a better fix is described here: skbug.com/40038693 333 if (src.isRect(nullptr) && src.isLastContourClosed() && is_even(initialDashIndex)) { 334 SkScalar pathLength = SkPathMeasure(src, false, rec->getResScale()).getLength(); 335 SkScalar endPhase = SkScalarMod(pathLength + startPhase, intervalLength); 336 size_t index = 0; 337 while (endPhase > intervals[index]) { 338 endPhase -= intervals[index++]; 339 SkASSERT(index <= count); 340 if (index == count) { 341 // We have run out of intervals. endPhase "should" never get to this point, 342 // but it could if the subtracts underflowed. Hence we will pin it as if it 343 // perfectly ran through the intervals. 344 // See crbug.com/875494 (and skbug.com/40039544) 345 endPhase = 0; 346 break; 347 } 348 } 349 // if dash ends inside "on", or ends at beginning of "off" 350 if (is_even(index) == (endPhase > 0)) { 351 SkPoint midPoint = src.getPoint(0); 352 // get vector at end of rect 353 int last = src.countPoints() - 1; 354 while (midPoint == src.getPoint(last)) { 355 --last; 356 SkASSERT(last >= 0); 357 } 358 // get vector at start of rect 359 int next = 1; 360 while (midPoint == src.getPoint(next)) { 361 ++next; 362 SkASSERT(next < last); 363 } 364 SkVector v = midPoint - src.getPoint(last); 365 const SkScalar kTinyOffset = SK_ScalarNearlyZero; 366 // scale vector to make start of tiny right angle 367 v *= kTinyOffset; 368 builder.moveTo(midPoint - v); 369 builder.lineTo(midPoint); 370 v = midPoint - src.getPoint(next); 371 // scale vector to make end of tiny right angle 372 v *= kTinyOffset; 373 builder.lineTo(midPoint - v); 374 } 375 } 376 377 // If PathMeasure took a SkPathRaw, we could pass it the raw from src or the builder, 378 // and not need to first 'detach' a path from the builder. 379 cullPathStorage = builder.detach(); 380 srcPtr = &cullPathStorage; 381 } 382 383 SpecialLineRec lineRec; 384 bool specialLine = (StrokeRecApplication::kAllow == strokeRecApplication) && 385 lineRec.init(*srcPtr, dst, rec, count >> 1, intervalLength); 386 387 SkPathMeasure meas(*srcPtr, false, rec->getResScale()); 388 389 do { 390 bool skipFirstSegment = meas.isClosed(); 391 bool addedSegment = false; 392 SkScalar length = meas.getLength(); 393 size_t index = initialDashIndex; 394 395 // Since the path length / dash length ratio may be arbitrarily large, we can exert 396 // significant memory pressure while attempting to build the filtered path. To avoid this, 397 // we simply give up dashing beyond a certain threshold. 398 // 399 // The original bug report (http://crbug.com/165432) is based on a path yielding more than 400 // 90 million dash segments and crashing the memory allocator. A limit of 1 million 401 // segments seems reasonable: at 2 verbs per segment * 9 bytes per verb, this caps the 402 // maximum dash memory overhead at roughly 17MB per path. 403 dashCount += length * (count >> 1) / intervalLength; 404 if (dashCount > kMaxDashCount) { 405 dst->reset(); 406 return false; 407 } 408 409 // Using double precision to avoid looping indefinitely due to single precision rounding 410 // (for extreme path_length/dash_length ratios). See test_infinite_dash() unittest. 411 double distance = 0; 412 double dlen = initialDashLength; 413 414 while (distance < length) { 415 SkASSERT(dlen >= 0); 416 addedSegment = false; 417 if (is_even(index) && !skipFirstSegment) { 418 addedSegment = true; 419 420 if (specialLine) { 421 lineRec.addSegment(SkDoubleToScalar(distance), 422 SkDoubleToScalar(distance + dlen), 423 dst); 424 } else { 425 meas.getSegment(SkDoubleToScalar(distance), 426 SkDoubleToScalar(distance + dlen), 427 dst, true); 428 } 429 } 430 distance += dlen; 431 432 // clear this so we only respect it the first time around 433 skipFirstSegment = false; 434 435 // wrap around our intervals array if necessary 436 index += 1; 437 SkASSERT(index <= count); 438 if (index == count) { 439 index = 0; 440 } 441 442 // fetch our next dlen 443 dlen = intervals[index]; 444 } 445 446 // extend if we ended on a segment and we need to join up with the (skipped) initial segment 447 if (meas.isClosed() && is_even(initialDashIndex) && 448 initialDashLength >= 0) { 449 meas.getSegment(0, initialDashLength, dst, !addedSegment); 450 } 451 } while (meas.nextContour()); 452 453 return true; 454 } 455 456 bool SkDashPath::FilterDashPath(SkPathBuilder* dst, const SkPath& src, SkStrokeRec* rec, 457 const SkRect* cullRect, const SkPathEffectBase::DashInfo& info) { 458 if (!ValidDashPath(info.fPhase, info.fIntervals)) { 459 return false; 460 } 461 SkScalar initialDashLength = 0; 462 size_t initialDashIndex = 0; 463 SkScalar intervalLength = 0; 464 CalcDashParameters(info.fPhase, info.fIntervals, &initialDashLength, 465 &initialDashIndex, &intervalLength); 466 return InternalFilter(dst, src, rec, cullRect, info.fIntervals, initialDashLength, 467 initialDashIndex, intervalLength, info.fPhase); 468 } 469 470 bool SkDashPath::ValidDashPath(SkScalar phase, SkSpan<const SkScalar> intervals) { 471 if (intervals.size() < 2 || !SkIsAlign2(intervals.size())) { 472 return false; 473 } 474 SkScalar length = 0; 475 for (SkScalar interval : intervals) { 476 if (interval < 0) { 477 return false; 478 } 479 length += interval; 480 } 481 // watch out for values that might make us go out of bounds 482 return length > 0 && SkIsFinite(phase, length); 483 }