italics.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. from fontTools.misc.transform import Transform
  2. from robofab.world import RFont
  3. from time import clock
  4. import numpy as np
  5. import math
  6. from alignpoints import alignCorners
  7. def italicizeGlyph(f, g, angle=10, stemWidth=185):
  8. unic = g.unicode #save unicode
  9. glyph = f[g.name]
  10. slope = np.tanh(math.pi * angle / 180)
  11. # determine how far on the x axis the glyph should slide
  12. # to compensate for the slant. -600 is a magic number
  13. # that assumes a 2048 unit em square
  14. MEAN_YCENTER = -600
  15. m = Transform(1, 0, slope, 1, 0, 0)
  16. xoffset, junk = m.transformPoint((0, MEAN_YCENTER))
  17. m = Transform(.97, 0, slope, 1, xoffset, 0)
  18. if len(glyph) > 0:
  19. g2 = italicize(f[g.name], angle, xoffset=xoffset, stemWidth=stemWidth)
  20. f.insertGlyph(g2, g.name)
  21. transformFLGlyphMembers(f[g.name], m)
  22. if unic > 0xFFFF: #restore unicode
  23. g.unicode = unic
  24. def italicize(glyph, angle=12, stemWidth=180, xoffset=-50):
  25. CURVE_CORRECTION_WEIGHT = .03
  26. CORNER_WEIGHT = 10
  27. ga, subsegments = segmentGlyph(glyph,25)
  28. va, e = glyphToMesh(ga)
  29. n = len(va)
  30. grad = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
  31. cornerWeights = mapEdges(lambda a,(p,n): normalize(p-a).dot(normalize(a-n)), grad, e)[:,0].reshape((-1,1))
  32. smooth = np.ones((n,1)) * CURVE_CORRECTION_WEIGHT
  33. controlPoints = findControlPointsInMesh(glyph, va, subsegments)
  34. smooth[controlPoints > 0] = 1
  35. smooth[cornerWeights < .6] = CORNER_WEIGHT
  36. # smooth[cornerWeights >= .9999] = 1
  37. out = va.copy()
  38. hascurves = False
  39. for c in glyph.contours:
  40. for s in c.segments:
  41. if s.type == "curve":
  42. hascurves = True
  43. break
  44. if hascurves:
  45. break
  46. if stemWidth > 100:
  47. outCorrected = skewMesh(recompose(skewMesh(out, angle * 1.6), grad, e, smooth=smooth), -angle * 1.6)
  48. # out = copyMeshDetails(va, out, e, 6)
  49. else:
  50. outCorrected = out
  51. normals = edgeNormals(out, e)
  52. center = va + normals * stemWidth * .4
  53. if stemWidth > 130:
  54. center[:, 0] = va[:, 0] * .7 + center[:,0] * .3
  55. centerSkew = skewMesh(center.dot(np.array([[.97,0],[0,1]])), angle * .9)
  56. out = outCorrected + (centerSkew - center)
  57. out[:,1] = outCorrected[:,1]
  58. smooth = np.ones((n,1)) * .1
  59. out = alignCorners(glyph, out, subsegments)
  60. out = copyMeshDetails(skewMesh(va, angle), out, e, 7, smooth=smooth)
  61. # grad = mapEdges(lambda a,(p,n): normalize(p-a), skewMesh(outCorrected, angle*.9), e)
  62. # out = recompose(out, grad, e, smooth=smooth)
  63. out = skewMesh(out, angle * .1)
  64. out[:,0] += xoffset
  65. # out[:,1] = outCorrected[:,1]
  66. out[va[:,1] == 0, 1] = 0
  67. gOut = meshToGlyph(out, ga)
  68. # gOut.width *= .97
  69. # gOut.width += 10
  70. # return gOut
  71. return fitGlyph(glyph, gOut, subsegments)
  72. def transformFLGlyphMembers(g, m, transformAnchors = True):
  73. # g.transform(m)
  74. g.width = g.width * m[0]
  75. p = m.transformPoint((0,0))
  76. for c in g.components:
  77. d = m.transformPoint(c.offset)
  78. c.offset = (d[0] - p[0], d[1] - p[1])
  79. if transformAnchors:
  80. for a in g.anchors:
  81. aa = m.transformPoint((a.x,a.y))
  82. a.x = aa[0]
  83. # a.x,a.y = (aa[0] - p[0], aa[1] - p[1])
  84. # a.x = a.x - m[4]
  85. from curveFitPen import fitGlyph,segmentGlyph
  86. from numpy.linalg import norm
  87. from scipy.sparse.linalg import cg
  88. from scipy.ndimage.filters import gaussian_filter1d as gaussian
  89. from scipy.cluster.vq import vq, kmeans2, whiten
  90. def glyphToMesh(g):
  91. points = []
  92. edges = {}
  93. offset = 0
  94. for c in g.contours:
  95. if len(c) < 2:
  96. continue
  97. for i,prev,next in rangePrevNext(len(c)):
  98. points.append((c[i].points[0].x, c[i].points[0].y))
  99. edges[i + offset] = np.array([prev + offset, next + offset], dtype=int)
  100. offset += len(c)
  101. return np.array(points), edges
  102. def meshToGlyph(points, g):
  103. g1 = g.copy()
  104. j = 0
  105. for c in g1.contours:
  106. if len(c) < 2:
  107. continue
  108. for i in range(len(c)):
  109. c[i].points[0].x = points[j][0]
  110. c[i].points[0].y = points[j][1]
  111. j += 1
  112. return g1
  113. def quantizeGradient(grad, book=None):
  114. if book == None:
  115. book = np.array([(1,0),(0,1),(0,-1),(-1,0)])
  116. indexArray = vq(whiten(grad), book)[0]
  117. out = book[indexArray]
  118. for i,v in enumerate(out):
  119. out[i] = normalize(v)
  120. return out
  121. def findControlPointsInMesh(glyph, va, subsegments):
  122. controlPointIndices = np.zeros((len(va),1))
  123. index = 0
  124. for i,c in enumerate(subsegments):
  125. segmentCount = len(glyph.contours[i].segments) - 1
  126. for j,s in enumerate(c):
  127. if j < segmentCount:
  128. if glyph.contours[i].segments[j].type == "line":
  129. controlPointIndices[index] = 1
  130. index += s[1]
  131. return controlPointIndices
  132. def recompose(v, grad, e, smooth=1, P=None, distance=None):
  133. n = len(v)
  134. if distance == None:
  135. distance = mapEdges(lambda a,(p,n): norm(p - a), v, e)
  136. if (P == None):
  137. P = mP(v,e)
  138. P += np.identity(n) * smooth
  139. f = v.copy()
  140. for i,(prev,next) in e.iteritems():
  141. f[i] = (grad[next] * distance[next] - grad[i] * distance[i])
  142. out = v.copy()
  143. f += v * smooth
  144. for i in range(len(out[0,:])):
  145. out[:,i] = cg(P, f[:,i])[0]
  146. return out
  147. def mP(v,e):
  148. n = len(v)
  149. M = np.zeros((n,n))
  150. for i, edges in e.iteritems():
  151. w = -2 / float(len(edges))
  152. for index in edges:
  153. M[i,index] = w
  154. M[i,i] = 2
  155. return M
  156. def normalize(v):
  157. n = np.linalg.norm(v)
  158. if n == 0:
  159. return v
  160. return v/n
  161. def mapEdges(func,v,e,*args):
  162. b = v.copy()
  163. for i, edges in e.iteritems():
  164. b[i] = func(v[i], [v[j] for j in edges], *args)
  165. return b
  166. def getNormal(a,b,c):
  167. "Assumes TT winding direction"
  168. p = np.roll(normalize(b - a), 1)
  169. n = -np.roll(normalize(c - a), 1)
  170. p[1] *= -1
  171. n[1] *= -1
  172. # print p, n, normalize((p + n) * .5)
  173. return normalize((p + n) * .5)
  174. def edgeNormals(v,e):
  175. "Assumes a mesh where each vertex has exactly least two edges"
  176. return mapEdges(lambda a,(p,n) : getNormal(a,p,n),v,e)
  177. def rangePrevNext(count):
  178. c = np.arange(count,dtype=int)
  179. r = np.vstack((c, np.roll(c, 1), np.roll(c, -1)))
  180. return r.T
  181. def skewMesh(v,angle):
  182. slope = np.tanh([math.pi * angle / 180])
  183. return v.dot(np.array([[1,0],[slope,1]]))
  184. def labelConnected(e):
  185. label = 0
  186. labels = np.zeros((len(e),1))
  187. for i,(prev,next) in e.iteritems():
  188. labels[i] = label
  189. if next <= i:
  190. label += 1
  191. return labels
  192. def copyGradDetails(a,b,e,scale=15):
  193. n = len(a)
  194. labels = labelConnected(e)
  195. out = a.astype(float).copy()
  196. for i in range(labels[-1]+1):
  197. mask = (labels==i).flatten()
  198. out[mask,:] = gaussian(b[mask,:], scale, mode="wrap", axis=0) + a[mask,:] - gaussian(a[mask,:], scale, mode="wrap", axis=0)
  199. return out
  200. def copyMeshDetails(va,vb,e,scale=5,smooth=.01):
  201. gradA = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
  202. gradB = mapEdges(lambda a,(p,n): normalize(p-a), vb, e)
  203. grad = copyGradDetails(gradA, gradB, e, scale)
  204. grad = mapEdges(lambda a,(p,n): normalize(a), grad, e)
  205. return recompose(vb, grad, e, smooth=smooth)
  206. def condenseGlyph(glyph, scale=.8, stemWidth=185):
  207. ga, subsegments = segmentGlyph(glyph, 25)
  208. va, e = glyphToMesh(ga)
  209. n = len(va)
  210. normals = edgeNormals(va,e)
  211. cn = va.dot(np.array([[scale, 0],[0,1]]))
  212. grad = mapEdges(lambda a,(p,n): normalize(p-a), cn, e)
  213. # ograd = mapEdges(lambda a,(p,n): normalize(p-a), va, e)
  214. cn[:,0] -= normals[:,0] * stemWidth * .5 * (1 - scale)
  215. out = recompose(cn, grad, e, smooth=.5)
  216. # out = recompose(out, grad, e, smooth=.1)
  217. out = recompose(out, grad, e, smooth=.01)
  218. # cornerWeights = mapEdges(lambda a,(p,n): normalize(p-a).dot(normalize(a-n)), grad, e)[:,0].reshape((-1,1))
  219. # smooth = np.ones((n,1)) * .1
  220. # smooth[cornerWeights < .6] = 10
  221. #
  222. # grad2 = quantizeGradient(grad).astype(float)
  223. # grad2 = copyGradDetails(grad, grad2, e, scale=10)
  224. # grad2 = mapEdges(lambda a,e: normalize(a), grad2, e)
  225. # out = recompose(out, grad2, e, smooth=smooth)
  226. out[:,0] += 15
  227. out[:,1] = va[:,1]
  228. # out = recompose(out, grad, e, smooth=.5)
  229. gOut = meshToGlyph(out, ga)
  230. gOut = fitGlyph(glyph, gOut, subsegments)
  231. for i,seg in enumerate(gOut):
  232. gOut[i].points[0].y = glyph[i].points[0].y
  233. return gOut