michael@0: michael@0: /* michael@0: * Copyright 2006 The Android Open Source Project michael@0: * michael@0: * Use of this source code is governed by a BSD-style license that can be michael@0: * found in the LICENSE file. michael@0: */ michael@0: michael@0: michael@0: #include "SkDashPathEffect.h" michael@0: #include "SkReadBuffer.h" michael@0: #include "SkWriteBuffer.h" michael@0: #include "SkPathMeasure.h" michael@0: michael@0: static inline int is_even(int x) { michael@0: return (~x) << 31; michael@0: } michael@0: michael@0: static SkScalar FindFirstInterval(const SkScalar intervals[], SkScalar phase, michael@0: int32_t* index, int count) { michael@0: for (int i = 0; i < count; ++i) { michael@0: if (phase > intervals[i]) { michael@0: phase -= intervals[i]; michael@0: } else { michael@0: *index = i; michael@0: return intervals[i] - phase; michael@0: } michael@0: } michael@0: // If we get here, phase "appears" to be larger than our length. This michael@0: // shouldn't happen with perfect precision, but we can accumulate errors michael@0: // during the initial length computation (rounding can make our sum be too michael@0: // big or too small. In that event, we just have to eat the error here. michael@0: *index = 0; michael@0: return intervals[0]; michael@0: } michael@0: michael@0: SkDashPathEffect::SkDashPathEffect(const SkScalar intervals[], int count, michael@0: SkScalar phase, bool scaleToFit) michael@0: : fScaleToFit(scaleToFit) { michael@0: SkASSERT(intervals); michael@0: SkASSERT(count > 1 && SkAlign2(count) == count); michael@0: michael@0: fIntervals = (SkScalar*)sk_malloc_throw(sizeof(SkScalar) * count); michael@0: fCount = count; michael@0: michael@0: SkScalar len = 0; michael@0: for (int i = 0; i < count; i++) { michael@0: SkASSERT(intervals[i] >= 0); michael@0: fIntervals[i] = intervals[i]; michael@0: len += intervals[i]; michael@0: } michael@0: fIntervalLength = len; michael@0: michael@0: // watch out for values that might make us go out of bounds michael@0: if ((len > 0) && SkScalarIsFinite(phase) && SkScalarIsFinite(len)) { michael@0: michael@0: // Adjust phase to be between 0 and len, "flipping" phase if negative. michael@0: // e.g., if len is 100, then phase of -20 (or -120) is equivalent to 80 michael@0: if (phase < 0) { michael@0: phase = -phase; michael@0: if (phase > len) { michael@0: phase = SkScalarMod(phase, len); michael@0: } michael@0: phase = len - phase; michael@0: michael@0: // Due to finite precision, it's possible that phase == len, michael@0: // even after the subtract (if len >>> phase), so fix that here. michael@0: // This fixes http://crbug.com/124652 . michael@0: SkASSERT(phase <= len); michael@0: if (phase == len) { michael@0: phase = 0; michael@0: } michael@0: } else if (phase >= len) { michael@0: phase = SkScalarMod(phase, len); michael@0: } michael@0: SkASSERT(phase >= 0 && phase < len); michael@0: michael@0: fInitialDashLength = FindFirstInterval(intervals, phase, michael@0: &fInitialDashIndex, count); michael@0: michael@0: SkASSERT(fInitialDashLength >= 0); michael@0: SkASSERT(fInitialDashIndex >= 0 && fInitialDashIndex < fCount); michael@0: } else { michael@0: fInitialDashLength = -1; // signal bad dash intervals michael@0: } michael@0: } michael@0: michael@0: SkDashPathEffect::~SkDashPathEffect() { michael@0: sk_free(fIntervals); michael@0: } michael@0: michael@0: static void outset_for_stroke(SkRect* rect, const SkStrokeRec& rec) { michael@0: SkScalar radius = SkScalarHalf(rec.getWidth()); michael@0: if (0 == radius) { michael@0: radius = SK_Scalar1; // hairlines michael@0: } michael@0: if (SkPaint::kMiter_Join == rec.getJoin()) { michael@0: radius = SkScalarMul(radius, rec.getMiter()); michael@0: } michael@0: rect->outset(radius, radius); michael@0: } michael@0: michael@0: // Only handles lines for now. If returns true, dstPath is the new (smaller) michael@0: // path. If returns false, then dstPath parameter is ignored. michael@0: static bool cull_path(const SkPath& srcPath, const SkStrokeRec& rec, michael@0: const SkRect* cullRect, SkScalar intervalLength, michael@0: SkPath* dstPath) { michael@0: if (NULL == cullRect) { michael@0: return false; michael@0: } michael@0: michael@0: SkPoint pts[2]; michael@0: if (!srcPath.isLine(pts)) { michael@0: return false; michael@0: } michael@0: michael@0: SkRect bounds = *cullRect; michael@0: outset_for_stroke(&bounds, rec); michael@0: michael@0: SkScalar dx = pts[1].x() - pts[0].x(); michael@0: SkScalar dy = pts[1].y() - pts[0].y(); michael@0: michael@0: // just do horizontal lines for now (lazy) michael@0: if (dy) { michael@0: return false; michael@0: } michael@0: michael@0: SkScalar minX = pts[0].fX; michael@0: SkScalar maxX = pts[1].fX; michael@0: michael@0: if (maxX < bounds.fLeft || minX > bounds.fRight) { michael@0: return false; michael@0: } michael@0: michael@0: if (dx < 0) { michael@0: SkTSwap(minX, maxX); michael@0: } michael@0: michael@0: // Now we actually perform the chop, removing the excess to the left and michael@0: // right of the bounds (keeping our new line "in phase" with the dash, michael@0: // hence the (mod intervalLength). michael@0: michael@0: if (minX < bounds.fLeft) { michael@0: minX = bounds.fLeft - SkScalarMod(bounds.fLeft - minX, michael@0: intervalLength); michael@0: } michael@0: if (maxX > bounds.fRight) { michael@0: maxX = bounds.fRight + SkScalarMod(maxX - bounds.fRight, michael@0: intervalLength); michael@0: } michael@0: michael@0: SkASSERT(maxX >= minX); michael@0: if (dx < 0) { michael@0: SkTSwap(minX, maxX); michael@0: } michael@0: pts[0].fX = minX; michael@0: pts[1].fX = maxX; michael@0: michael@0: dstPath->moveTo(pts[0]); michael@0: dstPath->lineTo(pts[1]); michael@0: return true; michael@0: } michael@0: michael@0: class SpecialLineRec { michael@0: public: michael@0: bool init(const SkPath& src, SkPath* dst, SkStrokeRec* rec, michael@0: int intervalCount, SkScalar intervalLength) { michael@0: if (rec->isHairlineStyle() || !src.isLine(fPts)) { michael@0: return false; michael@0: } michael@0: michael@0: // can relax this in the future, if we handle square and round caps michael@0: if (SkPaint::kButt_Cap != rec->getCap()) { michael@0: return false; michael@0: } michael@0: michael@0: SkScalar pathLength = SkPoint::Distance(fPts[0], fPts[1]); michael@0: michael@0: fTangent = fPts[1] - fPts[0]; michael@0: if (fTangent.isZero()) { michael@0: return false; michael@0: } michael@0: michael@0: fPathLength = pathLength; michael@0: fTangent.scale(SkScalarInvert(pathLength)); michael@0: fTangent.rotateCCW(&fNormal); michael@0: fNormal.scale(SkScalarHalf(rec->getWidth())); michael@0: michael@0: // now estimate how many quads will be added to the path michael@0: // resulting segments = pathLen * intervalCount / intervalLen michael@0: // resulting points = 4 * segments michael@0: michael@0: SkScalar ptCount = SkScalarMulDiv(pathLength, michael@0: SkIntToScalar(intervalCount), michael@0: intervalLength); michael@0: int n = SkScalarCeilToInt(ptCount) << 2; michael@0: dst->incReserve(n); michael@0: michael@0: // we will take care of the stroking michael@0: rec->setFillStyle(); michael@0: return true; michael@0: } michael@0: michael@0: void addSegment(SkScalar d0, SkScalar d1, SkPath* path) const { michael@0: SkASSERT(d0 < fPathLength); michael@0: // clamp the segment to our length michael@0: if (d1 > fPathLength) { michael@0: d1 = fPathLength; michael@0: } michael@0: michael@0: SkScalar x0 = fPts[0].fX + SkScalarMul(fTangent.fX, d0); michael@0: SkScalar x1 = fPts[0].fX + SkScalarMul(fTangent.fX, d1); michael@0: SkScalar y0 = fPts[0].fY + SkScalarMul(fTangent.fY, d0); michael@0: SkScalar y1 = fPts[0].fY + SkScalarMul(fTangent.fY, d1); michael@0: michael@0: SkPoint pts[4]; michael@0: pts[0].set(x0 + fNormal.fX, y0 + fNormal.fY); // moveTo michael@0: pts[1].set(x1 + fNormal.fX, y1 + fNormal.fY); // lineTo michael@0: pts[2].set(x1 - fNormal.fX, y1 - fNormal.fY); // lineTo michael@0: pts[3].set(x0 - fNormal.fX, y0 - fNormal.fY); // lineTo michael@0: michael@0: path->addPoly(pts, SK_ARRAY_COUNT(pts), false); michael@0: } michael@0: michael@0: private: michael@0: SkPoint fPts[2]; michael@0: SkVector fTangent; michael@0: SkVector fNormal; michael@0: SkScalar fPathLength; michael@0: }; michael@0: michael@0: bool SkDashPathEffect::filterPath(SkPath* dst, const SkPath& src, michael@0: SkStrokeRec* rec, const SkRect* cullRect) const { michael@0: // we do nothing if the src wants to be filled, or if our dashlength is 0 michael@0: if (rec->isFillStyle() || fInitialDashLength < 0) { michael@0: return false; michael@0: } michael@0: michael@0: const SkScalar* intervals = fIntervals; michael@0: SkScalar dashCount = 0; michael@0: int segCount = 0; michael@0: michael@0: SkPath cullPathStorage; michael@0: const SkPath* srcPtr = &src; michael@0: if (cull_path(src, *rec, cullRect, fIntervalLength, &cullPathStorage)) { michael@0: srcPtr = &cullPathStorage; michael@0: } michael@0: michael@0: SpecialLineRec lineRec; michael@0: bool specialLine = lineRec.init(*srcPtr, dst, rec, fCount >> 1, fIntervalLength); michael@0: michael@0: SkPathMeasure meas(*srcPtr, false); michael@0: michael@0: do { michael@0: bool skipFirstSegment = meas.isClosed(); michael@0: bool addedSegment = false; michael@0: SkScalar length = meas.getLength(); michael@0: int index = fInitialDashIndex; michael@0: SkScalar scale = SK_Scalar1; michael@0: michael@0: // Since the path length / dash length ratio may be arbitrarily large, we can exert michael@0: // significant memory pressure while attempting to build the filtered path. To avoid this, michael@0: // we simply give up dashing beyond a certain threshold. michael@0: // michael@0: // The original bug report (http://crbug.com/165432) is based on a path yielding more than michael@0: // 90 million dash segments and crashing the memory allocator. A limit of 1 million michael@0: // segments seems reasonable: at 2 verbs per segment * 9 bytes per verb, this caps the michael@0: // maximum dash memory overhead at roughly 17MB per path. michael@0: static const SkScalar kMaxDashCount = 1000000; michael@0: dashCount += length * (fCount >> 1) / fIntervalLength; michael@0: if (dashCount > kMaxDashCount) { michael@0: dst->reset(); michael@0: return false; michael@0: } michael@0: michael@0: if (fScaleToFit) { michael@0: if (fIntervalLength >= length) { michael@0: scale = SkScalarDiv(length, fIntervalLength); michael@0: } else { michael@0: SkScalar div = SkScalarDiv(length, fIntervalLength); michael@0: int n = SkScalarFloorToInt(div); michael@0: scale = SkScalarDiv(length, n * fIntervalLength); michael@0: } michael@0: } michael@0: michael@0: // Using double precision to avoid looping indefinitely due to single precision rounding michael@0: // (for extreme path_length/dash_length ratios). See test_infinite_dash() unittest. michael@0: double distance = 0; michael@0: double dlen = SkScalarMul(fInitialDashLength, scale); michael@0: michael@0: while (distance < length) { michael@0: SkASSERT(dlen >= 0); michael@0: addedSegment = false; michael@0: if (is_even(index) && dlen > 0 && !skipFirstSegment) { michael@0: addedSegment = true; michael@0: ++segCount; michael@0: michael@0: if (specialLine) { michael@0: lineRec.addSegment(SkDoubleToScalar(distance), michael@0: SkDoubleToScalar(distance + dlen), michael@0: dst); michael@0: } else { michael@0: meas.getSegment(SkDoubleToScalar(distance), michael@0: SkDoubleToScalar(distance + dlen), michael@0: dst, true); michael@0: } michael@0: } michael@0: distance += dlen; michael@0: michael@0: // clear this so we only respect it the first time around michael@0: skipFirstSegment = false; michael@0: michael@0: // wrap around our intervals array if necessary michael@0: index += 1; michael@0: SkASSERT(index <= fCount); michael@0: if (index == fCount) { michael@0: index = 0; michael@0: } michael@0: michael@0: // fetch our next dlen michael@0: dlen = SkScalarMul(intervals[index], scale); michael@0: } michael@0: michael@0: // extend if we ended on a segment and we need to join up with the (skipped) initial segment michael@0: if (meas.isClosed() && is_even(fInitialDashIndex) && michael@0: fInitialDashLength > 0) { michael@0: meas.getSegment(0, SkScalarMul(fInitialDashLength, scale), dst, !addedSegment); michael@0: ++segCount; michael@0: } michael@0: } while (meas.nextContour()); michael@0: michael@0: if (segCount > 1) { michael@0: dst->setConvexity(SkPath::kConcave_Convexity); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: // Currently asPoints is more restrictive then it needs to be. In the future michael@0: // we need to: michael@0: // allow kRound_Cap capping (could allow rotations in the matrix with this) michael@0: // allow paths to be returned michael@0: bool SkDashPathEffect::asPoints(PointData* results, michael@0: const SkPath& src, michael@0: const SkStrokeRec& rec, michael@0: const SkMatrix& matrix, michael@0: const SkRect* cullRect) const { michael@0: // width < 0 -> fill && width == 0 -> hairline so requiring width > 0 rules both out michael@0: if (fInitialDashLength < 0 || 0 >= rec.getWidth()) { michael@0: return false; michael@0: } michael@0: michael@0: // TODO: this next test could be eased up. We could allow any number of michael@0: // intervals as long as all the ons match and all the offs match. michael@0: // Additionally, they do not necessarily need to be integers. michael@0: // We cannot allow arbitrary intervals since we want the returned points michael@0: // to be uniformly sized. michael@0: if (fCount != 2 || michael@0: !SkScalarNearlyEqual(fIntervals[0], fIntervals[1]) || michael@0: !SkScalarIsInt(fIntervals[0]) || michael@0: !SkScalarIsInt(fIntervals[1])) { michael@0: return false; michael@0: } michael@0: michael@0: // TODO: this next test could be eased up. The rescaling should not impact michael@0: // the equality of the ons & offs. However, we would need to remove the michael@0: // integer intervals restriction first michael@0: if (fScaleToFit) { michael@0: return false; michael@0: } michael@0: michael@0: SkPoint pts[2]; michael@0: michael@0: if (!src.isLine(pts)) { michael@0: return false; michael@0: } michael@0: michael@0: // TODO: this test could be eased up to allow circles michael@0: if (SkPaint::kButt_Cap != rec.getCap()) { michael@0: return false; michael@0: } michael@0: michael@0: // TODO: this test could be eased up for circles. Rotations could be allowed. michael@0: if (!matrix.rectStaysRect()) { michael@0: return false; michael@0: } michael@0: michael@0: SkScalar length = SkPoint::Distance(pts[1], pts[0]); michael@0: michael@0: SkVector tangent = pts[1] - pts[0]; michael@0: if (tangent.isZero()) { michael@0: return false; michael@0: } michael@0: michael@0: tangent.scale(SkScalarInvert(length)); michael@0: michael@0: // TODO: make this test for horizontal & vertical lines more robust michael@0: bool isXAxis = true; michael@0: if (SK_Scalar1 == tangent.fX || -SK_Scalar1 == tangent.fX) { michael@0: results->fSize.set(SkScalarHalf(fIntervals[0]), SkScalarHalf(rec.getWidth())); michael@0: } else if (SK_Scalar1 == tangent.fY || -SK_Scalar1 == tangent.fY) { michael@0: results->fSize.set(SkScalarHalf(rec.getWidth()), SkScalarHalf(fIntervals[0])); michael@0: isXAxis = false; michael@0: } else if (SkPaint::kRound_Cap != rec.getCap()) { michael@0: // Angled lines don't have axis-aligned boxes. michael@0: return false; michael@0: } michael@0: michael@0: if (NULL != results) { michael@0: results->fFlags = 0; michael@0: SkScalar clampedInitialDashLength = SkMinScalar(length, fInitialDashLength); michael@0: michael@0: if (SkPaint::kRound_Cap == rec.getCap()) { michael@0: results->fFlags |= PointData::kCircles_PointFlag; michael@0: } michael@0: michael@0: results->fNumPoints = 0; michael@0: SkScalar len2 = length; michael@0: if (clampedInitialDashLength > 0 || 0 == fInitialDashIndex) { michael@0: SkASSERT(len2 >= clampedInitialDashLength); michael@0: if (0 == fInitialDashIndex) { michael@0: if (clampedInitialDashLength > 0) { michael@0: if (clampedInitialDashLength >= fIntervals[0]) { michael@0: ++results->fNumPoints; // partial first dash michael@0: } michael@0: len2 -= clampedInitialDashLength; michael@0: } michael@0: len2 -= fIntervals[1]; // also skip first space michael@0: if (len2 < 0) { michael@0: len2 = 0; michael@0: } michael@0: } else { michael@0: len2 -= clampedInitialDashLength; // skip initial partial empty michael@0: } michael@0: } michael@0: int numMidPoints = SkScalarFloorToInt(SkScalarDiv(len2, fIntervalLength)); michael@0: results->fNumPoints += numMidPoints; michael@0: len2 -= numMidPoints * fIntervalLength; michael@0: bool partialLast = false; michael@0: if (len2 > 0) { michael@0: if (len2 < fIntervals[0]) { michael@0: partialLast = true; michael@0: } else { michael@0: ++numMidPoints; michael@0: ++results->fNumPoints; michael@0: } michael@0: } michael@0: michael@0: results->fPoints = new SkPoint[results->fNumPoints]; michael@0: michael@0: SkScalar distance = 0; michael@0: int curPt = 0; michael@0: michael@0: if (clampedInitialDashLength > 0 || 0 == fInitialDashIndex) { michael@0: SkASSERT(clampedInitialDashLength <= length); michael@0: michael@0: if (0 == fInitialDashIndex) { michael@0: if (clampedInitialDashLength > 0) { michael@0: // partial first block michael@0: SkASSERT(SkPaint::kRound_Cap != rec.getCap()); // can't handle partial circles michael@0: SkScalar x = pts[0].fX + SkScalarMul(tangent.fX, SkScalarHalf(clampedInitialDashLength)); michael@0: SkScalar y = pts[0].fY + SkScalarMul(tangent.fY, SkScalarHalf(clampedInitialDashLength)); michael@0: SkScalar halfWidth, halfHeight; michael@0: if (isXAxis) { michael@0: halfWidth = SkScalarHalf(clampedInitialDashLength); michael@0: halfHeight = SkScalarHalf(rec.getWidth()); michael@0: } else { michael@0: halfWidth = SkScalarHalf(rec.getWidth()); michael@0: halfHeight = SkScalarHalf(clampedInitialDashLength); michael@0: } michael@0: if (clampedInitialDashLength < fIntervals[0]) { michael@0: // This one will not be like the others michael@0: results->fFirst.addRect(x - halfWidth, y - halfHeight, michael@0: x + halfWidth, y + halfHeight); michael@0: } else { michael@0: SkASSERT(curPt < results->fNumPoints); michael@0: results->fPoints[curPt].set(x, y); michael@0: ++curPt; michael@0: } michael@0: michael@0: distance += clampedInitialDashLength; michael@0: } michael@0: michael@0: distance += fIntervals[1]; // skip over the next blank block too michael@0: } else { michael@0: distance += clampedInitialDashLength; michael@0: } michael@0: } michael@0: michael@0: if (0 != numMidPoints) { michael@0: distance += SkScalarHalf(fIntervals[0]); michael@0: michael@0: for (int i = 0; i < numMidPoints; ++i) { michael@0: SkScalar x = pts[0].fX + SkScalarMul(tangent.fX, distance); michael@0: SkScalar y = pts[0].fY + SkScalarMul(tangent.fY, distance); michael@0: michael@0: SkASSERT(curPt < results->fNumPoints); michael@0: results->fPoints[curPt].set(x, y); michael@0: ++curPt; michael@0: michael@0: distance += fIntervalLength; michael@0: } michael@0: michael@0: distance -= SkScalarHalf(fIntervals[0]); michael@0: } michael@0: michael@0: if (partialLast) { michael@0: // partial final block michael@0: SkASSERT(SkPaint::kRound_Cap != rec.getCap()); // can't handle partial circles michael@0: SkScalar temp = length - distance; michael@0: SkASSERT(temp < fIntervals[0]); michael@0: SkScalar x = pts[0].fX + SkScalarMul(tangent.fX, distance + SkScalarHalf(temp)); michael@0: SkScalar y = pts[0].fY + SkScalarMul(tangent.fY, distance + SkScalarHalf(temp)); michael@0: SkScalar halfWidth, halfHeight; michael@0: if (isXAxis) { michael@0: halfWidth = SkScalarHalf(temp); michael@0: halfHeight = SkScalarHalf(rec.getWidth()); michael@0: } else { michael@0: halfWidth = SkScalarHalf(rec.getWidth()); michael@0: halfHeight = SkScalarHalf(temp); michael@0: } michael@0: results->fLast.addRect(x - halfWidth, y - halfHeight, michael@0: x + halfWidth, y + halfHeight); michael@0: } michael@0: michael@0: SkASSERT(curPt == results->fNumPoints); michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: SkFlattenable::Factory SkDashPathEffect::getFactory() const { michael@0: return CreateProc; michael@0: } michael@0: michael@0: void SkDashPathEffect::flatten(SkWriteBuffer& buffer) const { michael@0: this->INHERITED::flatten(buffer); michael@0: buffer.writeInt(fInitialDashIndex); michael@0: buffer.writeScalar(fInitialDashLength); michael@0: buffer.writeScalar(fIntervalLength); michael@0: buffer.writeBool(fScaleToFit); michael@0: buffer.writeScalarArray(fIntervals, fCount); michael@0: } michael@0: michael@0: SkFlattenable* SkDashPathEffect::CreateProc(SkReadBuffer& buffer) { michael@0: return SkNEW_ARGS(SkDashPathEffect, (buffer)); michael@0: } michael@0: michael@0: SkDashPathEffect::SkDashPathEffect(SkReadBuffer& buffer) : INHERITED(buffer) { michael@0: fInitialDashIndex = buffer.readInt(); michael@0: fInitialDashLength = buffer.readScalar(); michael@0: fIntervalLength = buffer.readScalar(); michael@0: fScaleToFit = buffer.readBool(); michael@0: michael@0: fCount = buffer.getArrayCount(); michael@0: size_t allocSize = sizeof(SkScalar) * fCount; michael@0: if (buffer.validateAvailable(allocSize)) { michael@0: fIntervals = (SkScalar*)sk_malloc_throw(allocSize); michael@0: buffer.readScalarArray(fIntervals, fCount); michael@0: } else { michael@0: fIntervals = NULL; michael@0: } michael@0: }