Home | History | Annotate | Download | only in misc
      1 """fontTools.misc.bezierTools.py -- tools for working with bezier path segments.
      2 """
      3 
      4 from __future__ import print_function, division, absolute_import
      5 from fontTools.misc.py23 import *
      6 
      7 __all__ = [
      8     "calcQuadraticBounds",
      9     "calcCubicBounds",
     10     "splitLine",
     11     "splitQuadratic",
     12     "splitCubic",
     13     "splitQuadraticAtT",
     14     "splitCubicAtT",
     15     "solveQuadratic",
     16     "solveCubic",
     17 ]
     18 
     19 from fontTools.misc.arrayTools import calcBounds
     20 
     21 epsilon = 1e-12
     22 
     23 
     24 def calcQuadraticBounds(pt1, pt2, pt3):
     25     """Return the bounding rectangle for a qudratic bezier segment.
     26     pt1 and pt3 are the "anchor" points, pt2 is the "handle".
     27 
     28         >>> calcQuadraticBounds((0, 0), (50, 100), (100, 0))
     29         (0, 0, 100, 50.0)
     30         >>> calcQuadraticBounds((0, 0), (100, 0), (100, 100))
     31         (0.0, 0.0, 100, 100)
     32     """
     33     (ax, ay), (bx, by), (cx, cy) = calcQuadraticParameters(pt1, pt2, pt3)
     34     ax2 = ax*2.0
     35     ay2 = ay*2.0
     36     roots = []
     37     if ax2 != 0:
     38         roots.append(-bx/ax2)
     39     if ay2 != 0:
     40         roots.append(-by/ay2)
     41     points = [(ax*t*t + bx*t + cx, ay*t*t + by*t + cy) for t in roots if 0 <= t < 1] + [pt1, pt3]
     42     return calcBounds(points)
     43 
     44 
     45 def calcCubicBounds(pt1, pt2, pt3, pt4):
     46     """Return the bounding rectangle for a cubic bezier segment.
     47     pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles".
     48 
     49         >>> calcCubicBounds((0, 0), (25, 100), (75, 100), (100, 0))
     50         (0, 0, 100, 75.0)
     51         >>> calcCubicBounds((0, 0), (50, 0), (100, 50), (100, 100))
     52         (0.0, 0.0, 100, 100)
     53         >>> print "%f %f %f %f" % calcCubicBounds((50, 0), (0, 100), (100, 100), (50, 0))
     54         35.566243 0.000000 64.433757 75.000000
     55     """
     56     (ax, ay), (bx, by), (cx, cy), (dx, dy) = calcCubicParameters(pt1, pt2, pt3, pt4)
     57     # calc first derivative
     58     ax3 = ax * 3.0
     59     ay3 = ay * 3.0
     60     bx2 = bx * 2.0
     61     by2 = by * 2.0
     62     xRoots = [t for t in solveQuadratic(ax3, bx2, cx) if 0 <= t < 1]
     63     yRoots = [t for t in solveQuadratic(ay3, by2, cy) if 0 <= t < 1]
     64     roots = xRoots + yRoots
     65     
     66     points = [(ax*t*t*t + bx*t*t + cx * t + dx, ay*t*t*t + by*t*t + cy * t + dy) for t in roots] + [pt1, pt4]
     67     return calcBounds(points)
     68 
     69 
     70 def splitLine(pt1, pt2, where, isHorizontal):
     71     """Split the line between pt1 and pt2 at position 'where', which
     72     is an x coordinate if isHorizontal is False, a y coordinate if
     73     isHorizontal is True. Return a list of two line segments if the
     74     line was successfully split, or a list containing the original
     75     line.
     76 
     77         >>> printSegments(splitLine((0, 0), (100, 100), 50, True))
     78         ((0, 0), (50.0, 50.0))
     79         ((50.0, 50.0), (100, 100))
     80         >>> printSegments(splitLine((0, 0), (100, 100), 100, True))
     81         ((0, 0), (100, 100))
     82         >>> printSegments(splitLine((0, 0), (100, 100), 0, True))
     83         ((0, 0), (0.0, 0.0))
     84         ((0.0, 0.0), (100, 100))
     85         >>> printSegments(splitLine((0, 0), (100, 100), 0, False))
     86         ((0, 0), (0.0, 0.0))
     87         ((0.0, 0.0), (100, 100))
     88     """
     89     pt1x, pt1y = pt1
     90     pt2x, pt2y = pt2
     91 
     92     ax = (pt2x - pt1x)
     93     ay = (pt2y - pt1y)
     94 
     95     bx = pt1x
     96     by = pt1y
     97 
     98     if ax == 0:
     99         return [(pt1, pt2)]
    100 
    101     t = (where - (bx, by)[isHorizontal]) / ax
    102     if 0 <= t < 1:
    103         midPt = ax * t + bx, ay * t + by
    104         return [(pt1, midPt), (midPt, pt2)]
    105     else:
    106         return [(pt1, pt2)]
    107 
    108 
    109 def splitQuadratic(pt1, pt2, pt3, where, isHorizontal):
    110     """Split the quadratic curve between pt1, pt2 and pt3 at position 'where',
    111     which is an x coordinate if isHorizontal is False, a y coordinate if
    112     isHorizontal is True. Return a list of curve segments.
    113 
    114         >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 150, False))
    115         ((0, 0), (50, 100), (100, 0))
    116         >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, False))
    117         ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
    118         ((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
    119         >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, False))
    120         ((0.0, 0.0), (12.5, 25.0), (25.0, 37.5))
    121         ((25.0, 37.5), (62.5, 75.0), (100.0, 0.0))
    122         >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 25, True))
    123         ((0.0, 0.0), (7.32233047034, 14.6446609407), (14.6446609407, 25.0))
    124         ((14.6446609407, 25.0), (50.0, 75.0), (85.3553390593, 25.0))
    125         ((85.3553390593, 25.0), (92.6776695297, 14.6446609407), (100.0, -7.1054273576e-15))
    126         >>> # XXX I'm not at all sure if the following behavior is desirable:
    127         >>> printSegments(splitQuadratic((0, 0), (50, 100), (100, 0), 50, True))
    128         ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
    129         ((50.0, 50.0), (50.0, 50.0), (50.0, 50.0))
    130         ((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
    131     """
    132     a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
    133     solutions = solveQuadratic(a[isHorizontal], b[isHorizontal],
    134         c[isHorizontal] - where)
    135     solutions = sorted([t for t in solutions if 0 <= t < 1])
    136     if not solutions:
    137         return [(pt1, pt2, pt3)]
    138     return _splitQuadraticAtT(a, b, c, *solutions)
    139 
    140 
    141 def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal):
    142     """Split the cubic curve between pt1, pt2, pt3 and pt4 at position 'where',
    143     which is an x coordinate if isHorizontal is False, a y coordinate if
    144     isHorizontal is True. Return a list of curve segments.
    145 
    146         >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 150, False))
    147         ((0, 0), (25, 100), (75, 100), (100, 0))
    148         >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 50, False))
    149         ((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
    150         ((50.0, 75.0), (68.75, 75.0), (87.5, 50.0), (100.0, 0.0))
    151         >>> printSegments(splitCubic((0, 0), (25, 100), (75, 100), (100, 0), 25, True))
    152         ((0.0, 0.0), (2.2937927384, 9.17517095361), (4.79804488188, 17.5085042869), (7.47413641001, 25.0))
    153         ((7.47413641001, 25.0), (31.2886200204, 91.6666666667), (68.7113799796, 91.6666666667), (92.52586359, 25.0))
    154         ((92.52586359, 25.0), (95.2019551181, 17.5085042869), (97.7062072616, 9.17517095361), (100.0, 1.7763568394e-15))
    155     """
    156     a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
    157     solutions = solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal],
    158         d[isHorizontal] - where)
    159     solutions = sorted([t for t in solutions if 0 <= t < 1])
    160     if not solutions:
    161         return [(pt1, pt2, pt3, pt4)]
    162     return _splitCubicAtT(a, b, c, d, *solutions)
    163 
    164 
    165 def splitQuadraticAtT(pt1, pt2, pt3, *ts):
    166     """Split the quadratic curve between pt1, pt2 and pt3 at one or more
    167     values of t. Return a list of curve segments.
    168 
    169         >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5))
    170         ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
    171         ((50.0, 50.0), (75.0, 50.0), (100.0, 0.0))
    172         >>> printSegments(splitQuadraticAtT((0, 0), (50, 100), (100, 0), 0.5, 0.75))
    173         ((0.0, 0.0), (25.0, 50.0), (50.0, 50.0))
    174         ((50.0, 50.0), (62.5, 50.0), (75.0, 37.5))
    175         ((75.0, 37.5), (87.5, 25.0), (100.0, 0.0))
    176     """
    177     a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
    178     return _splitQuadraticAtT(a, b, c, *ts)
    179 
    180 
    181 def splitCubicAtT(pt1, pt2, pt3, pt4, *ts):
    182     """Split the cubic curve between pt1, pt2, pt3 and pt4 at one or more
    183     values of t. Return a list of curve segments.
    184 
    185         >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5))
    186         ((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
    187         ((50.0, 75.0), (68.75, 75.0), (87.5, 50.0), (100.0, 0.0))
    188         >>> printSegments(splitCubicAtT((0, 0), (25, 100), (75, 100), (100, 0), 0.5, 0.75))
    189         ((0.0, 0.0), (12.5, 50.0), (31.25, 75.0), (50.0, 75.0))
    190         ((50.0, 75.0), (59.375, 75.0), (68.75, 68.75), (77.34375, 56.25))
    191         ((77.34375, 56.25), (85.9375, 43.75), (93.75, 25.0), (100.0, 0.0))
    192     """
    193     a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
    194     return _splitCubicAtT(a, b, c, d, *ts)
    195 
    196 
    197 def _splitQuadraticAtT(a, b, c, *ts):
    198     ts = list(ts)
    199     segments = []
    200     ts.insert(0, 0.0)
    201     ts.append(1.0)
    202     ax, ay = a
    203     bx, by = b
    204     cx, cy = c
    205     for i in range(len(ts) - 1):
    206         t1 = ts[i]
    207         t2 = ts[i+1]
    208         delta = (t2 - t1)
    209         # calc new a, b and c
    210         a1x = ax * delta**2
    211         a1y = ay * delta**2
    212         b1x = (2*ax*t1 + bx) * delta
    213         b1y = (2*ay*t1 + by) * delta
    214         c1x = ax*t1**2 + bx*t1 + cx
    215         c1y = ay*t1**2 + by*t1 + cy
    216     
    217         pt1, pt2, pt3 = calcQuadraticPoints((a1x, a1y), (b1x, b1y), (c1x, c1y))
    218         segments.append((pt1, pt2, pt3))
    219     return segments
    220 
    221 
    222 def _splitCubicAtT(a, b, c, d, *ts):
    223     ts = list(ts)
    224     ts.insert(0, 0.0)
    225     ts.append(1.0)
    226     segments = []
    227     ax, ay = a
    228     bx, by = b
    229     cx, cy = c
    230     dx, dy = d
    231     for i in range(len(ts) - 1):
    232         t1 = ts[i]
    233         t2 = ts[i+1]
    234         delta = (t2 - t1)
    235         # calc new a, b, c and d
    236         a1x = ax * delta**3
    237         a1y = ay * delta**3
    238         b1x = (3*ax*t1 + bx) * delta**2
    239         b1y = (3*ay*t1 + by) * delta**2
    240         c1x = (2*bx*t1 + cx + 3*ax*t1**2) * delta
    241         c1y = (2*by*t1 + cy + 3*ay*t1**2) * delta
    242         d1x = ax*t1**3 + bx*t1**2 + cx*t1 + dx
    243         d1y = ay*t1**3 + by*t1**2 + cy*t1 + dy
    244         pt1, pt2, pt3, pt4 = calcCubicPoints((a1x, a1y), (b1x, b1y), (c1x, c1y), (d1x, d1y))
    245         segments.append((pt1, pt2, pt3, pt4))
    246     return segments
    247 
    248 
    249 #
    250 # Equation solvers.
    251 #
    252 
    253 from math import sqrt, acos, cos, pi
    254 
    255 
    256 def solveQuadratic(a, b, c,
    257         sqrt=sqrt):
    258     """Solve a quadratic equation where a, b and c are real.
    259         a*x*x + b*x + c = 0
    260     This function returns a list of roots. Note that the returned list
    261     is neither guaranteed to be sorted nor to contain unique values!
    262     """
    263     if abs(a) < epsilon:
    264         if abs(b) < epsilon:
    265             # We have a non-equation; therefore, we have no valid solution
    266             roots = []
    267         else:
    268             # We have a linear equation with 1 root.
    269             roots = [-c/b]
    270     else:
    271         # We have a true quadratic equation.  Apply the quadratic formula to find two roots.
    272         DD = b*b - 4.0*a*c
    273         if DD >= 0.0:
    274             rDD = sqrt(DD)
    275             roots = [(-b+rDD)/2.0/a, (-b-rDD)/2.0/a]
    276         else:
    277             # complex roots, ignore
    278             roots = []
    279     return roots
    280 
    281 
    282 def solveCubic(a, b, c, d):
    283     """Solve a cubic equation where a, b, c and d are real.
    284         a*x*x*x + b*x*x + c*x + d = 0
    285     This function returns a list of roots. Note that the returned list
    286     is neither guaranteed to be sorted nor to contain unique values!
    287     """
    288     #
    289     # adapted from:
    290     #   CUBIC.C - Solve a cubic polynomial
    291     #   public domain by Ross Cottrell
    292     # found at: http://www.strangecreations.com/library/snippets/Cubic.C
    293     #
    294     if abs(a) < epsilon:
    295         # don't just test for zero; for very small values of 'a' solveCubic()
    296         # returns unreliable results, so we fall back to quad.
    297         return solveQuadratic(b, c, d)
    298     a = float(a)
    299     a1 = b/a
    300     a2 = c/a
    301     a3 = d/a
    302     
    303     Q = (a1*a1 - 3.0*a2)/9.0
    304     R = (2.0*a1*a1*a1 - 9.0*a1*a2 + 27.0*a3)/54.0
    305     R2_Q3 = R*R - Q*Q*Q
    306 
    307     if R2_Q3 < 0:
    308         theta = acos(R/sqrt(Q*Q*Q))
    309         rQ2 = -2.0*sqrt(Q)
    310         x0 = rQ2*cos(theta/3.0) - a1/3.0
    311         x1 = rQ2*cos((theta+2.0*pi)/3.0) - a1/3.0
    312         x2 = rQ2*cos((theta+4.0*pi)/3.0) - a1/3.0
    313         return [x0, x1, x2]
    314     else:
    315         if Q == 0 and R == 0:
    316             x = 0
    317         else:
    318             x = pow(sqrt(R2_Q3)+abs(R), 1/3.0)
    319             x = x + Q/x
    320         if R >= 0.0:
    321             x = -x
    322         x = x - a1/3.0
    323         return [x]
    324 
    325 
    326 #
    327 # Conversion routines for points to parameters and vice versa
    328 #
    329 
    330 def calcQuadraticParameters(pt1, pt2, pt3):
    331     x2, y2 = pt2
    332     x3, y3 = pt3
    333     cx, cy = pt1
    334     bx = (x2 - cx) * 2.0
    335     by = (y2 - cy) * 2.0
    336     ax = x3 - cx - bx
    337     ay = y3 - cy - by
    338     return (ax, ay), (bx, by), (cx, cy)
    339 
    340 
    341 def calcCubicParameters(pt1, pt2, pt3, pt4):
    342     x2, y2 = pt2
    343     x3, y3 = pt3
    344     x4, y4 = pt4
    345     dx, dy = pt1
    346     cx = (x2 -dx) * 3.0
    347     cy = (y2 -dy) * 3.0
    348     bx = (x3 - x2) * 3.0 - cx
    349     by = (y3 - y2) * 3.0 - cy
    350     ax = x4 - dx - cx - bx
    351     ay = y4 - dy - cy - by
    352     return (ax, ay), (bx, by), (cx, cy), (dx, dy)
    353 
    354 
    355 def calcQuadraticPoints(a, b, c):
    356     ax, ay = a
    357     bx, by = b
    358     cx, cy = c
    359     x1 = cx
    360     y1 = cy
    361     x2 = (bx * 0.5) + cx
    362     y2 = (by * 0.5) + cy
    363     x3 = ax + bx + cx
    364     y3 = ay + by + cy
    365     return (x1, y1), (x2, y2), (x3, y3)
    366 
    367 
    368 def calcCubicPoints(a, b, c, d):
    369     ax, ay = a
    370     bx, by = b
    371     cx, cy = c
    372     dx, dy = d
    373     x1 = dx
    374     y1 = dy
    375     x2 = (cx / 3.0) + dx
    376     y2 = (cy / 3.0) + dy
    377     x3 = (bx + cx) / 3.0 + x2
    378     y3 = (by + cy) / 3.0 + y2
    379     x4 = ax + dx + cx + bx
    380     y4 = ay + dy + cy + by
    381     return (x1, y1), (x2, y2), (x3, y3), (x4, y4)
    382 
    383 
    384 def _segmentrepr(obj):
    385     """
    386         >>> _segmentrepr([1, [2, 3], [], [[2, [3, 4], [0.1, 2.2]]]])
    387         '(1, (2, 3), (), ((2, (3, 4), (0.1, 2.2))))'
    388     """
    389     try:
    390         it = iter(obj)
    391     except TypeError:
    392         return str(obj)
    393     else:
    394         return "(%s)" % ", ".join([_segmentrepr(x) for x in it])
    395 
    396 
    397 def printSegments(segments):
    398     """Helper for the doctests, displaying each segment in a list of
    399     segments on a single line as a tuple.
    400     """
    401     for segment in segments:
    402         print(_segmentrepr(segment))
    403 
    404 if __name__ == "__main__":
    405     import doctest
    406     doctest.testmod()
    407