SVGPathSegUtils.cpp (15148B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "SVGPathSegUtils.h" 8 9 #include "SVGArcConverter.h" 10 #include "gfx2DGlue.h" 11 #include "mozilla/ServoStyleConsts.h" // StylePathCommand 12 #include "nsMathUtils.h" 13 #include "nsTextFormatter.h" 14 15 using namespace mozilla::gfx; 16 17 namespace mozilla { 18 19 static const float PATH_SEG_LENGTH_TOLERANCE = 0.0000001f; 20 static const uint32_t MAX_RECURSION = 10; 21 22 static float CalcDistanceBetweenPoints(const Point& aP1, const Point& aP2) { 23 return NS_hypot(aP2.x - aP1.x, aP2.y - aP1.y); 24 } 25 26 static void SplitQuadraticBezier(const Point* aCurve, Point* aLeft, 27 Point* aRight) { 28 aLeft[0].x = aCurve[0].x; 29 aLeft[0].y = aCurve[0].y; 30 aRight[2].x = aCurve[2].x; 31 aRight[2].y = aCurve[2].y; 32 aLeft[1].x = (aCurve[0].x + aCurve[1].x) / 2; 33 aLeft[1].y = (aCurve[0].y + aCurve[1].y) / 2; 34 aRight[1].x = (aCurve[1].x + aCurve[2].x) / 2; 35 aRight[1].y = (aCurve[1].y + aCurve[2].y) / 2; 36 aLeft[2].x = aRight[0].x = (aLeft[1].x + aRight[1].x) / 2; 37 aLeft[2].y = aRight[0].y = (aLeft[1].y + aRight[1].y) / 2; 38 } 39 40 static void SplitCubicBezier(const Point* aCurve, Point* aLeft, Point* aRight) { 41 Point tmp; 42 tmp.x = (aCurve[1].x + aCurve[2].x) / 4; 43 tmp.y = (aCurve[1].y + aCurve[2].y) / 4; 44 aLeft[0].x = aCurve[0].x; 45 aLeft[0].y = aCurve[0].y; 46 aRight[3].x = aCurve[3].x; 47 aRight[3].y = aCurve[3].y; 48 aLeft[1].x = (aCurve[0].x + aCurve[1].x) / 2; 49 aLeft[1].y = (aCurve[0].y + aCurve[1].y) / 2; 50 aRight[2].x = (aCurve[2].x + aCurve[3].x) / 2; 51 aRight[2].y = (aCurve[2].y + aCurve[3].y) / 2; 52 aLeft[2].x = aLeft[1].x / 2 + tmp.x; 53 aLeft[2].y = aLeft[1].y / 2 + tmp.y; 54 aRight[1].x = aRight[2].x / 2 + tmp.x; 55 aRight[1].y = aRight[2].y / 2 + tmp.y; 56 aLeft[3].x = aRight[0].x = (aLeft[2].x + aRight[1].x) / 2; 57 aLeft[3].y = aRight[0].y = (aLeft[2].y + aRight[1].y) / 2; 58 } 59 60 static float CalcBezLengthHelper(const Point* aCurve, uint32_t aNumPts, 61 uint32_t aRecursionCount, 62 void (*aSplit)(const Point*, Point*, Point*)) { 63 Point left[4]; 64 Point right[4]; 65 float length = 0, dist; 66 for (uint32_t i = 0; i < aNumPts - 1; i++) { 67 length += CalcDistanceBetweenPoints(aCurve[i], aCurve[i + 1]); 68 } 69 dist = CalcDistanceBetweenPoints(aCurve[0], aCurve[aNumPts - 1]); 70 if (length - dist > PATH_SEG_LENGTH_TOLERANCE && 71 aRecursionCount < MAX_RECURSION) { 72 aSplit(aCurve, left, right); 73 ++aRecursionCount; 74 return CalcBezLengthHelper(left, aNumPts, aRecursionCount, aSplit) + 75 CalcBezLengthHelper(right, aNumPts, aRecursionCount, aSplit); 76 } 77 return length; 78 } 79 80 static inline float CalcLengthOfCubicBezier(const Point& aPos, 81 const Point& aCP1, 82 const Point& aCP2, 83 const Point& aTo) { 84 Point curve[4] = {aPos, aCP1, aCP2, aTo}; 85 return CalcBezLengthHelper(curve, 4, 0, SplitCubicBezier); 86 } 87 88 static inline float CalcLengthOfQuadraticBezier(const Point& aPos, 89 const Point& aCP, 90 const Point& aTo) { 91 Point curve[3] = {aPos, aCP, aTo}; 92 return CalcBezLengthHelper(curve, 3, 0, SplitQuadraticBezier); 93 } 94 95 // Basically, this is just a variant version of the above TraverseXXX functions. 96 // We just put those function inside this and use StylePathCommand instead. 97 // This function and the above ones should be dropped by Bug 1388931. 98 /* static */ 99 void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, 100 SVGPathTraversalState& aState) { 101 switch (aCommand.tag) { 102 case StylePathCommand::Tag::Close: 103 if (aState.ShouldUpdateLengthAndControlPoints()) { 104 aState.length += CalcDistanceBetweenPoints(aState.pos, aState.start); 105 aState.cp1 = aState.cp2 = aState.start; 106 } 107 aState.pos = aState.start; 108 break; 109 case StylePathCommand::Tag::Move: { 110 const Point& p = aCommand.move.point.ToGfxPoint(); 111 aState.start = aState.pos = 112 aCommand.move.point.IsToPosition() ? p : aState.pos + p; 113 if (aState.ShouldUpdateLengthAndControlPoints()) { 114 // aState.length is unchanged, since move commands don't affect path= 115 // length. 116 aState.cp1 = aState.cp2 = aState.start; 117 } 118 break; 119 } 120 case StylePathCommand::Tag::Line: { 121 Point to = aCommand.line.point.IsToPosition() 122 ? aCommand.line.point.ToGfxPoint() 123 : aState.pos + aCommand.line.point.ToGfxPoint(); 124 if (aState.ShouldUpdateLengthAndControlPoints()) { 125 aState.length += CalcDistanceBetweenPoints(aState.pos, to); 126 aState.cp1 = aState.cp2 = to; 127 } 128 aState.pos = to; 129 break; 130 } 131 case StylePathCommand::Tag::CubicCurve: { 132 Point to = aCommand.cubic_curve.point.IsByCoordinate() 133 ? aState.pos + aCommand.cubic_curve.point.ToGfxPoint() 134 : aCommand.cubic_curve.point.ToGfxPoint(); 135 if (aState.ShouldUpdateLengthAndControlPoints()) { 136 Point cp1 = aCommand.cubic_curve.control1.ToGfxPoint(aState.pos, to); 137 Point cp2 = aCommand.cubic_curve.control2.ToGfxPoint(aState.pos, to); 138 aState.length += 139 (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); 140 aState.cp2 = cp2; 141 aState.cp1 = to; 142 } 143 aState.pos = to; 144 break; 145 } 146 case StylePathCommand::Tag::QuadCurve: { 147 Point to = aCommand.quad_curve.point.IsByCoordinate() 148 ? aState.pos + aCommand.quad_curve.point.ToGfxPoint() 149 : aCommand.quad_curve.point.ToGfxPoint(); 150 if (aState.ShouldUpdateLengthAndControlPoints()) { 151 Point cp = aCommand.quad_curve.control1.ToGfxPoint(aState.pos, to); 152 aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); 153 aState.cp1 = cp; 154 aState.cp2 = to; 155 } 156 aState.pos = to; 157 break; 158 } 159 case StylePathCommand::Tag::Arc: { 160 const auto& arc = aCommand.arc; 161 Point to = arc.point.IsToPosition() ? arc.point.ToGfxPoint() 162 : aState.pos + arc.point.ToGfxPoint(); 163 if (aState.ShouldUpdateLengthAndControlPoints()) { 164 float dist = 0; 165 Point radii = arc.radii.ToGfxPoint(); 166 if (radii.x == 0.0f || radii.y == 0.0f) { 167 dist = CalcDistanceBetweenPoints(aState.pos, to); 168 } else { 169 Point bez[4] = {aState.pos, Point(0, 0), Point(0, 0), Point(0, 0)}; 170 const bool largeArcFlag = arc.arc_size == StyleArcSize::Large; 171 const bool sweepFlag = arc.arc_sweep == StyleArcSweep::Cw; 172 SVGArcConverter converter(aState.pos, to, radii, arc.rotate, 173 largeArcFlag, sweepFlag); 174 while (converter.GetNextSegment(&bez[1], &bez[2], &bez[3])) { 175 dist += CalcBezLengthHelper(bez, 4, 0, SplitCubicBezier); 176 bez[0] = bez[3]; 177 } 178 } 179 aState.length += dist; 180 aState.cp1 = aState.cp2 = to; 181 } 182 aState.pos = to; 183 break; 184 } 185 case StylePathCommand::Tag::HLine: { 186 const auto x = aCommand.h_line.x.ToGfxCoord(); 187 Point to(aCommand.h_line.x.IsToPosition() ? x : aState.pos.x + x, 188 aState.pos.y); 189 if (aState.ShouldUpdateLengthAndControlPoints()) { 190 aState.length += std::fabs(to.x - aState.pos.x); 191 aState.cp1 = aState.cp2 = to; 192 } 193 aState.pos = to; 194 break; 195 } 196 case StylePathCommand::Tag::VLine: { 197 const auto y = aCommand.v_line.y.ToGfxCoord(); 198 Point to(aState.pos.x, 199 aCommand.v_line.y.IsToPosition() ? y : aState.pos.y + y); 200 if (aState.ShouldUpdateLengthAndControlPoints()) { 201 aState.length += std::fabs(to.y - aState.pos.y); 202 aState.cp1 = aState.cp2 = to; 203 } 204 aState.pos = to; 205 break; 206 } 207 case StylePathCommand::Tag::SmoothCubic: { 208 Point to = aCommand.smooth_cubic.point.IsByCoordinate() 209 ? aState.pos + aCommand.smooth_cubic.point.ToGfxPoint() 210 : aCommand.smooth_cubic.point.ToGfxPoint(); 211 if (aState.ShouldUpdateLengthAndControlPoints()) { 212 Point cp1 = aState.pos - (aState.cp2 - aState.pos); 213 Point cp2 = aCommand.smooth_cubic.control2.ToGfxPoint(aState.pos, to); 214 aState.length += 215 (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); 216 aState.cp2 = cp2; 217 aState.cp1 = to; 218 } 219 aState.pos = to; 220 break; 221 } 222 case StylePathCommand::Tag::SmoothQuad: { 223 Point to = aCommand.smooth_quad.point.IsToPosition() 224 ? aCommand.smooth_quad.point.ToGfxPoint() 225 : aState.pos + aCommand.smooth_quad.point.ToGfxPoint(); 226 if (aState.ShouldUpdateLengthAndControlPoints()) { 227 Point cp = aState.pos - (aState.cp1 - aState.pos); 228 aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); 229 aState.cp1 = cp; 230 aState.cp2 = to; 231 } 232 aState.pos = to; 233 break; 234 } 235 } 236 } 237 238 // Possible directions of an edge that doesn't immediately disqualify the path 239 // as a rectangle. 240 enum class EdgeDir { 241 LEFT, 242 RIGHT, 243 UP, 244 DOWN, 245 // NONE represents (almost) zero-length edges, they should be ignored. 246 NONE, 247 }; 248 249 Maybe<EdgeDir> GetDirection(Point v) { 250 if (!std::isfinite(v.x.value) || !std::isfinite(v.y.value)) { 251 return Nothing(); 252 } 253 254 bool x = fabs(v.x) > 0.001; 255 bool y = fabs(v.y) > 0.001; 256 if (x && y) { 257 return Nothing(); 258 } 259 260 if (!x && !y) { 261 return Some(EdgeDir::NONE); 262 } 263 264 if (x) { 265 return Some(v.x > 0.0 ? EdgeDir::RIGHT : EdgeDir::LEFT); 266 } 267 268 return Some(v.y > 0.0 ? EdgeDir::DOWN : EdgeDir::UP); 269 } 270 271 EdgeDir OppositeDirection(EdgeDir dir) { 272 switch (dir) { 273 case EdgeDir::LEFT: 274 return EdgeDir::RIGHT; 275 case EdgeDir::RIGHT: 276 return EdgeDir::LEFT; 277 case EdgeDir::UP: 278 return EdgeDir::DOWN; 279 case EdgeDir::DOWN: 280 return EdgeDir::UP; 281 default: 282 return EdgeDir::NONE; 283 } 284 } 285 286 struct IsRectHelper { 287 Point min; 288 Point max; 289 EdgeDir currentDir; 290 // Index of the next corner. 291 uint32_t idx; 292 EdgeDir dirs[4]; 293 294 bool Edge(Point from, Point to) { 295 auto edge = to - from; 296 297 auto maybeDir = GetDirection(edge); 298 if (maybeDir.isNothing()) { 299 return false; 300 } 301 302 EdgeDir dir = maybeDir.value(); 303 304 if (dir == EdgeDir::NONE) { 305 // zero-length edges aren't an issue. 306 return true; 307 } 308 309 if (dir != currentDir) { 310 // The edge forms a corner with the previous edge. 311 if (idx >= 4) { 312 // We are at the 5th corner, can't be a rectangle. 313 return false; 314 } 315 316 if (dir == OppositeDirection(currentDir)) { 317 // Can turn left or right but not a full 180 degrees. 318 return false; 319 } 320 321 dirs[idx] = dir; 322 idx += 1; 323 currentDir = dir; 324 } 325 326 min.x = fmin(min.x, to.x); 327 min.y = fmin(min.y, to.y); 328 max.x = fmax(max.x, to.x); 329 max.y = fmax(max.y, to.y); 330 331 return true; 332 } 333 334 bool EndSubpath() { 335 if (idx != 4) { 336 return false; 337 } 338 339 if (dirs[0] != OppositeDirection(dirs[2]) || 340 dirs[1] != OppositeDirection(dirs[3])) { 341 return false; 342 } 343 344 return true; 345 } 346 }; 347 348 bool ApproxEqual(gfx::Point a, gfx::Point b) { 349 auto v = b - a; 350 return fabs(v.x) < 0.001 && fabs(v.y) < 0.001; 351 } 352 353 Maybe<gfx::Rect> SVGPathToAxisAlignedRect(Span<const StylePathCommand> aPath) { 354 Point pathStart(0.0, 0.0); 355 Point segStart(0.0, 0.0); 356 IsRectHelper helper = { 357 Point(0.0, 0.0), 358 Point(0.0, 0.0), 359 EdgeDir::NONE, 360 0, 361 {EdgeDir::NONE, EdgeDir::NONE, EdgeDir::NONE, EdgeDir::NONE}, 362 }; 363 364 for (const StylePathCommand& cmd : aPath) { 365 switch (cmd.tag) { 366 case StylePathCommand::Tag::Move: { 367 Point to = cmd.move.point.ToGfxPoint(); 368 if (helper.idx != 0) { 369 // This is overly strict since empty moveto sequences such as "M 10 12 370 // M 3 2 M 0 0" render nothing, but I expect it won't make us miss a 371 // lot of rect-shaped paths in practice and lets us avoidhandling 372 // special caps for empty sub-paths like "M 0 0 L 0 0" and "M 1 2 Z". 373 return Nothing(); 374 } 375 376 if (!ApproxEqual(pathStart, segStart)) { 377 // If we were only interested in filling we could auto-close here 378 // by calling helper.Edge like in the ClosePath case and detect some 379 // unclosed paths as rectangles. 380 // 381 // For example: 382 // - "M 1 0 L 0 0 L 0 1 L 1 1 L 1 0" are both rects for filling and 383 // stroking. 384 // - "M 1 0 L 0 0 L 0 1 L 1 1" fills a rect but the stroke is shaped 385 // like a C. 386 return Nothing(); 387 } 388 389 if (helper.idx != 0 && !helper.EndSubpath()) { 390 return Nothing(); 391 } 392 393 if (cmd.move.point.IsByCoordinate()) { 394 to = segStart + to; 395 } 396 397 pathStart = to; 398 segStart = to; 399 if (helper.idx == 0) { 400 helper.min = to; 401 helper.max = to; 402 } 403 404 break; 405 } 406 case StylePathCommand::Tag::Close: { 407 if (!helper.Edge(segStart, pathStart)) { 408 return Nothing(); 409 } 410 if (!helper.EndSubpath()) { 411 return Nothing(); 412 } 413 pathStart = segStart; 414 break; 415 } 416 case StylePathCommand::Tag::Line: { 417 Point to = cmd.line.point.ToGfxPoint(); 418 if (cmd.line.point.IsByCoordinate()) { 419 to = segStart + to; 420 } 421 422 if (!helper.Edge(segStart, to)) { 423 return Nothing(); 424 } 425 segStart = to; 426 break; 427 } 428 case StylePathCommand::Tag::HLine: { 429 Point to = gfx::Point(cmd.h_line.x.ToGfxCoord(), segStart.y); 430 if (cmd.h_line.x.IsByCoordinate()) { 431 to.x += segStart.x; 432 } 433 434 if (!helper.Edge(segStart, to)) { 435 return Nothing(); 436 } 437 segStart = to; 438 break; 439 } 440 case StylePathCommand::Tag::VLine: { 441 Point to = gfx::Point(segStart.x, cmd.v_line.y.ToGfxCoord()); 442 if (cmd.v_line.y.IsByCoordinate()) { 443 to.y += segStart.y; 444 } 445 446 if (!helper.Edge(segStart, to)) { 447 return Nothing(); 448 } 449 segStart = to; 450 break; 451 } 452 default: 453 return Nothing(); 454 } 455 } 456 457 if (!ApproxEqual(pathStart, segStart)) { 458 // Same situation as with moveto regarding stroking not fullly closed path 459 // even though the fill is a rectangle. 460 return Nothing(); 461 } 462 463 if (!helper.EndSubpath()) { 464 return Nothing(); 465 } 466 467 auto size = (helper.max - helper.min); 468 return Some(Rect(helper.min, Size(size.x, size.y))); 469 } 470 471 } // namespace mozilla