curveFitPen.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. #! /opt/local/bin/pythonw2.7
  2. #
  3. # Copyright 2015 Google Inc. All Rights Reserved.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. __all__ = ["SubsegmentPen","SubsegmentsToCurvesPen", "segmentGlyph", "fitGlyph"]
  17. from fontTools.pens.basePen import BasePen
  18. import numpy as np
  19. from numpy import array as v
  20. from numpy.linalg import norm
  21. from robofab.pens.adapterPens import GuessSmoothPointPen
  22. from robofab.pens.pointPen import BasePointToSegmentPen
  23. class SubsegmentsToCurvesPointPen(BasePointToSegmentPen):
  24. def __init__(self, glyph, subsegmentGlyph, subsegments):
  25. BasePointToSegmentPen.__init__(self)
  26. self.glyph = glyph
  27. self.subPen = SubsegmentsToCurvesPen(None, glyph.getPen(), subsegmentGlyph, subsegments)
  28. def setMatchTangents(self, b):
  29. self.subPen.matchTangents = b
  30. def _flushContour(self, segments):
  31. #
  32. # adapted from robofab.pens.adapterPens.rfUFOPointPen
  33. #
  34. assert len(segments) >= 1
  35. # if we only have one point and it has a name, we must have an anchor
  36. first = segments[0]
  37. segmentType, points = first
  38. pt, smooth, name, kwargs = points[0]
  39. if len(segments) == 1 and name != None:
  40. self.glyph.appendAnchor(name, pt)
  41. return
  42. else:
  43. segmentType, points = segments[-1]
  44. movePt, smooth, name, kwargs = points[-1]
  45. if smooth:
  46. # last point is smooth, set pen to start smooth
  47. self.subPen.setLastSmooth(True)
  48. if segmentType == 'line':
  49. del segments[-1]
  50. self.subPen.moveTo(movePt)
  51. # do the rest of the segments
  52. for segmentType, points in segments:
  53. isSmooth = True in [smooth for pt, smooth, name, kwargs in points]
  54. pp = [pt for pt, smooth, name, kwargs in points]
  55. if segmentType == "line":
  56. assert len(pp) == 1
  57. if isSmooth:
  58. self.subPen.smoothLineTo(pp[0])
  59. else:
  60. self.subPen.lineTo(pp[0])
  61. elif segmentType == "curve":
  62. assert len(pp) == 3
  63. if isSmooth:
  64. self.subPen.smoothCurveTo(*pp)
  65. else:
  66. self.subPen.curveTo(*pp)
  67. elif segmentType == "qcurve":
  68. assert 0, "qcurve not supported"
  69. else:
  70. assert 0, "illegal segmentType: %s" % segmentType
  71. self.subPen.closePath()
  72. def addComponent(self, glyphName, transform):
  73. self.subPen.addComponent(glyphName, transform)
  74. class SubsegmentsToCurvesPen(BasePen):
  75. def __init__(self, glyphSet, otherPen, subsegmentGlyph, subsegments):
  76. BasePen.__init__(self, None)
  77. self.otherPen = otherPen
  78. self.ssglyph = subsegmentGlyph
  79. self.subsegments = subsegments
  80. self.contourIndex = -1
  81. self.segmentIndex = -1
  82. self.lastPoint = (0,0)
  83. self.lastSmooth = False
  84. self.nextSmooth = False
  85. def setLastSmooth(self, b):
  86. self.lastSmooth = b
  87. def _moveTo(self, (x, y)):
  88. self.contourIndex += 1
  89. self.segmentIndex = 0
  90. self.startPoint = (x,y)
  91. p = self.ssglyph.contours[self.contourIndex][0].points[0]
  92. self.otherPen.moveTo((p.x, p.y))
  93. self.lastPoint = (x,y)
  94. def _lineTo(self, (x, y)):
  95. self.segmentIndex += 1
  96. index = self.subsegments[self.contourIndex][self.segmentIndex][0]
  97. p = self.ssglyph.contours[self.contourIndex][index].points[0]
  98. self.otherPen.lineTo((p.x, p.y))
  99. self.lastPoint = (x,y)
  100. self.lastSmooth = False
  101. def smoothLineTo(self, (x, y)):
  102. self.lineTo((x,y))
  103. self.lastSmooth = True
  104. def smoothCurveTo(self, (x1, y1), (x2, y2), (x3, y3)):
  105. self.nextSmooth = True
  106. self.curveTo((x1, y1), (x2, y2), (x3, y3))
  107. self.nextSmooth = False
  108. self.lastSmooth = True
  109. def _curveToOne(self, (x1, y1), (x2, y2), (x3, y3)):
  110. self.segmentIndex += 1
  111. c = self.ssglyph.contours[self.contourIndex]
  112. n = len(c)
  113. startIndex = (self.subsegments[self.contourIndex][self.segmentIndex-1][0])
  114. segmentCount = (self.subsegments[self.contourIndex][self.segmentIndex][1])
  115. endIndex = (startIndex + segmentCount + 1) % (n)
  116. indices = [(startIndex + i) % (n) for i in range(segmentCount + 1)]
  117. points = np.array([(c[i].points[0].x, c[i].points[0].y) for i in indices])
  118. prevPoint = (c[(startIndex - 1)].points[0].x, c[(startIndex - 1)].points[0].y)
  119. nextPoint = (c[(endIndex) % n].points[0].x, c[(endIndex) % n].points[0].y)
  120. prevTangent = prevPoint - points[0]
  121. nextTangent = nextPoint - points[-1]
  122. tangent1 = points[1] - points[0]
  123. tangent3 = points[-2] - points[-1]
  124. prevTangent /= np.linalg.norm(prevTangent)
  125. nextTangent /= np.linalg.norm(nextTangent)
  126. tangent1 /= np.linalg.norm(tangent1)
  127. tangent3 /= np.linalg.norm(tangent3)
  128. tangent1, junk = self.smoothTangents(tangent1, prevTangent, self.lastSmooth)
  129. tangent3, junk = self.smoothTangents(tangent3, nextTangent, self.nextSmooth)
  130. if self.matchTangents == True:
  131. cp = fitBezier(points, tangent1, tangent3)
  132. cp[1] = norm(cp[1] - cp[0]) * tangent1 / norm(tangent1) + cp[0]
  133. cp[2] = norm(cp[2] - cp[3]) * tangent3 / norm(tangent3) + cp[3]
  134. else:
  135. cp = fitBezier(points)
  136. # if self.ssglyph.name == 'r':
  137. # print "-----------"
  138. # print self.lastSmooth, self.nextSmooth
  139. # print "%i %i : %i %i \n %i %i : %i %i \n %i %i : %i %i"%(x1,y1, cp[1,0], cp[1,1], x2,y2, cp[2,0], cp[2,1], x3,y3, cp[3,0], cp[3,1])
  140. self.otherPen.curveTo((cp[1,0], cp[1,1]), (cp[2,0], cp[2,1]), (cp[3,0], cp[3,1]))
  141. self.lastPoint = (x3, y3)
  142. self.lastSmooth = False
  143. def smoothTangents(self,t1,t2,forceSmooth = False):
  144. if forceSmooth or (abs(t1.dot(t2)) > .95 and norm(t1-t2) > 1):
  145. # print t1,t2,
  146. t1 = (t1 - t2) / 2
  147. t2 = -t1
  148. # print t1,t2
  149. return t1 / norm(t1), t2 / norm(t2)
  150. def _closePath(self):
  151. self.otherPen.closePath()
  152. def _endPath(self):
  153. self.otherPen.endPath()
  154. def addComponent(self, glyphName, transformation):
  155. self.otherPen.addComponent(glyphName, transformation)
  156. class SubsegmentPointPen(BasePointToSegmentPen):
  157. def __init__(self, glyph, resolution):
  158. BasePointToSegmentPen.__init__(self)
  159. self.glyph = glyph
  160. self.resolution = resolution
  161. self.subPen = SubsegmentPen(None, glyph.getPen())
  162. def getSubsegments(self):
  163. return self.subPen.subsegments[:]
  164. def _flushContour(self, segments):
  165. #
  166. # adapted from robofab.pens.adapterPens.rfUFOPointPen
  167. #
  168. assert len(segments) >= 1
  169. # if we only have one point and it has a name, we must have an anchor
  170. first = segments[0]
  171. segmentType, points = first
  172. pt, smooth, name, kwargs = points[0]
  173. if len(segments) == 1 and name != None:
  174. self.glyph.appendAnchor(name, pt)
  175. return
  176. else:
  177. segmentType, points = segments[-1]
  178. movePt, smooth, name, kwargs = points[-1]
  179. if segmentType == 'line':
  180. del segments[-1]
  181. self.subPen.moveTo(movePt)
  182. # do the rest of the segments
  183. for segmentType, points in segments:
  184. points = [pt for pt, smooth, name, kwargs in points]
  185. if segmentType == "line":
  186. assert len(points) == 1
  187. self.subPen.lineTo(points[0])
  188. elif segmentType == "curve":
  189. assert len(points) == 3
  190. self.subPen.curveTo(*points)
  191. elif segmentType == "qcurve":
  192. assert 0, "qcurve not supported"
  193. else:
  194. assert 0, "illegal segmentType: %s" % segmentType
  195. self.subPen.closePath()
  196. def addComponent(self, glyphName, transform):
  197. self.subPen.addComponent(glyphName, transform)
  198. class SubsegmentPen(BasePen):
  199. def __init__(self, glyphSet, otherPen, resolution=25):
  200. BasePen.__init__(self,glyphSet)
  201. self.resolution = resolution
  202. self.otherPen = otherPen
  203. self.subsegments = []
  204. self.startContour = (0,0)
  205. self.contourIndex = -1
  206. def _moveTo(self, (x, y)):
  207. self.contourIndex += 1
  208. self.segmentIndex = 0
  209. self.subsegments.append([])
  210. self.subsegmentCount = 0
  211. self.subsegments[self.contourIndex].append([self.subsegmentCount, 0])
  212. self.startContour = (x,y)
  213. self.lastPoint = (x,y)
  214. self.otherPen.moveTo((x,y))
  215. def _lineTo(self, (x, y)):
  216. count = self.stepsForSegment((x,y),self.lastPoint)
  217. if count < 1:
  218. count = 1
  219. self.subsegmentCount += count
  220. self.subsegments[self.contourIndex].append([self.subsegmentCount, count])
  221. for i in range(1,count+1):
  222. x1 = self.lastPoint[0] + (x - self.lastPoint[0]) * i/float(count)
  223. y1 = self.lastPoint[1] + (y - self.lastPoint[1]) * i/float(count)
  224. self.otherPen.lineTo((x1,y1))
  225. self.lastPoint = (x,y)
  226. def _curveToOne(self, (x1, y1), (x2, y2), (x3, y3)):
  227. count = self.stepsForSegment((x3,y3),self.lastPoint)
  228. if count < 2:
  229. count = 2
  230. self.subsegmentCount += count
  231. self.subsegments[self.contourIndex].append([self.subsegmentCount,count])
  232. x = self.renderCurve((self.lastPoint[0],x1,x2,x3),count)
  233. y = self.renderCurve((self.lastPoint[1],y1,y2,y3),count)
  234. assert len(x) == count
  235. if (x3 == self.startContour[0] and y3 == self.startContour[1]):
  236. count -= 1
  237. for i in range(count):
  238. self.otherPen.lineTo((x[i],y[i]))
  239. self.lastPoint = (x3,y3)
  240. def _closePath(self):
  241. if not (self.lastPoint[0] == self.startContour[0] and self.lastPoint[1] == self.startContour[1]):
  242. self._lineTo(self.startContour)
  243. # round values used by otherPen (a RoboFab SegmentToPointPen) to decide
  244. # whether to delete duplicate points at start and end of contour
  245. #TODO(jamesgk) figure out why we have to do this hack, then remove it
  246. c = self.otherPen.contour
  247. for i in [0, -1]:
  248. c[i] = [[round(n, 5) for n in c[i][0]]] + list(c[i][1:])
  249. self.otherPen.closePath()
  250. def _endPath(self):
  251. self.otherPen.endPath()
  252. def addComponent(self, glyphName, transformation):
  253. self.otherPen.addComponent(glyphName, transformation)
  254. def stepsForSegment(self, p1, p2):
  255. dist = np.linalg.norm(v(p1) - v(p2))
  256. out = int(dist / self.resolution)
  257. return out
  258. def renderCurve(self,p,count):
  259. curvePoints = []
  260. t = 1.0 / float(count)
  261. temp = t * t
  262. f = p[0]
  263. fd = 3 * (p[1] - p[0]) * t
  264. fdd_per_2 = 3 * (p[0] - 2 * p[1] + p[2]) * temp
  265. fddd_per_2 = 3 * (3 * (p[1] - p[2]) + p[3] - p[0]) * temp * t
  266. fddd = fddd_per_2 + fddd_per_2
  267. fdd = fdd_per_2 + fdd_per_2
  268. fddd_per_6 = fddd_per_2 * (1.0 / 3)
  269. for i in range(count):
  270. f = f + fd + fdd_per_2 + fddd_per_6
  271. fd = fd + fdd + fddd_per_2
  272. fdd = fdd + fddd
  273. fdd_per_2 = fdd_per_2 + fddd_per_2
  274. curvePoints.append(f)
  275. return curvePoints
  276. def fitBezierSimple(pts):
  277. T = [np.linalg.norm(pts[i]-pts[i-1]) for i in range(1,len(pts))]
  278. tsum = np.sum(T)
  279. T = [0] + T
  280. T = [np.sum(T[0:i+1])/tsum for i in range(len(pts))]
  281. T = [[t**3, t**2, t, 1] for t in T]
  282. T = np.array(T)
  283. M = np.array([[-1, 3, -3, 1],
  284. [ 3, -6, 3, 0],
  285. [-3, 3, 0, 0],
  286. [ 1, 0, 0, 0]])
  287. T = T.dot(M)
  288. T = np.concatenate((T, np.array([[100,0,0,0], [0,0,0,100]])))
  289. # pts = np.vstack((pts, pts[0] * 100, pts[-1] * 100))
  290. C = np.linalg.lstsq(T, pts)
  291. return C[0]
  292. def subdivideLineSegment(pts):
  293. out = [pts[0]]
  294. for i in range(1, len(pts)):
  295. out.append(pts[i-1] + (pts[i] - pts[i-1]) * .5)
  296. out.append(pts[i])
  297. return np.array(out)
  298. def fitBezier(pts,tangent0=None,tangent3=None):
  299. if len(pts < 4):
  300. pts = subdivideLineSegment(pts)
  301. T = [np.linalg.norm(pts[i]-pts[i-1]) for i in range(1,len(pts))]
  302. tsum = np.sum(T)
  303. T = [0] + T
  304. T = [np.sum(T[0:i+1])/tsum for i in range(len(pts))]
  305. T = [[t**3, t**2, t, 1] for t in T]
  306. T = np.array(T)
  307. M = np.array([[-1, 3, -3, 1],
  308. [ 3, -6, 3, 0],
  309. [-3, 3, 0, 0],
  310. [ 1, 0, 0, 0]])
  311. T = T.dot(M)
  312. n = len(pts)
  313. pout = pts.copy()
  314. pout[:,0] -= (T[:,0] * pts[0,0]) + (T[:,3] * pts[-1,0])
  315. pout[:,1] -= (T[:,0] * pts[0,1]) + (T[:,3] * pts[-1,1])
  316. TT = np.zeros((n*2,4))
  317. for i in range(n):
  318. for j in range(2):
  319. TT[i*2,j*2] = T[i,j+1]
  320. TT[i*2+1,j*2+1] = T[i,j+1]
  321. pout = pout.reshape((n*2,1),order="C")
  322. if tangent0 != None and tangent3 != None:
  323. tangentConstraintsT = np.array([
  324. [tangent0[1], -tangent0[0], 0, 0],
  325. [0, 0, tangent3[1], -tangent3[0]]
  326. ])
  327. tangentConstraintsP = np.array([
  328. [pts[0][1] * -tangent0[0] + pts[0][0] * tangent0[1]],
  329. [pts[-1][1] * -tangent3[0] + pts[-1][0] * tangent3[1]]
  330. ])
  331. TT = np.concatenate((TT, tangentConstraintsT * 1000))
  332. pout = np.concatenate((pout, tangentConstraintsP * 1000))
  333. C = np.linalg.lstsq(TT,pout)[0].reshape((2,2))
  334. return np.array([pts[0], C[0], C[1], pts[-1]])
  335. def segmentGlyph(glyph,resolution=50):
  336. g1 = glyph.copy()
  337. g1.clear()
  338. dp = SubsegmentPointPen(g1, resolution)
  339. glyph.drawPoints(dp)
  340. return g1, dp.getSubsegments()
  341. def fitGlyph(glyph, subsegmentGlyph, subsegmentIndices, matchTangents=True):
  342. outGlyph = glyph.copy()
  343. outGlyph.clear()
  344. fitPen = SubsegmentsToCurvesPointPen(outGlyph, subsegmentGlyph, subsegmentIndices)
  345. fitPen.setMatchTangents(matchTangents)
  346. # smoothPen = GuessSmoothPointPen(fitPen)
  347. glyph.drawPoints(fitPen)
  348. outGlyph.width = subsegmentGlyph.width
  349. return outGlyph
  350. if __name__ == '__main__':
  351. p = SubsegmentPen(None, None)
  352. pts = np.array([
  353. [0,0],
  354. [.5,.5],
  355. [.5,.5],
  356. [1,1]
  357. ])
  358. print np.array(p.renderCurve(pts,10)) * 10