Skip to content

Instantly share code, notes, and snippets.

@nixeneko
Created December 11, 2017 14:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nixeneko/5d4813d55f69cd2452130065dd1cb003 to your computer and use it in GitHub Desktop.
Save nixeneko/5d4813d55f69cd2452130065dd1cb003 to your computer and use it in GitHub Desktop.
Draw font outline and shifted outlines
# coding: utf-8
from fontTools.ttLib import TTFont
from PIL import Image, ImageDraw
import math
INFILE = "mplus-1p-regular.ttf"
ttf = TTFont(INFILE)
tags = ttf.reader.tables
unitsPerEm = ttf['head'].unitsPerEm
sTypoAscender = ttf['OS/2'].sTypoAscender
strdraw = "ねこ"
#ttf['cmap'].getBestCmap() #<- impremented in newest FontTools
#platFormID: 3 - Windows
#encodingID: 10 - UCS-4
cmap = ttf['cmap'].getcmap(3,10).cmap
names = [cmap[ord(c)] for c in strdraw]
#ttf['hmtx'][<glyphName>]: (advanceWidth, lsb)
advanceWidths = [ttf['hmtx'][n][0] for n in names]
glyphs = [ttf['glyf'][n] for n in names]
#ttf['glyf']['a'].numberOfContours
#ttf['glyf']['a'].coordinates
#ttf['glyf']['a'].endPtsOfContours
#ttf['glyf']['a'].flags[idx] -> <flagOnCurve>
outlineData = []
for g in glyphs:
#numberOfContours = g.numberOfContours
coordinates = g.coordinates
endPtsOfContours = g.endPtsOfContours
flags = g.flags
contours = []
coords = list(zip(coordinates, flags))
lastIdx = -1
for idx in endPtsOfContours:
contours.append(coords[lastIdx+1:idx+1])
lastIdx = idx
outlineData.append(contours)
#outlineData: [[((x, y), on-curve?), ...],...]
contours = []
offsetX = 0
for n in range(len(glyphs)):
for contour in outlineData[n]:
contours.append([((pos[0]+offsetX, sTypoAscender-pos[1]), flg) for pos, flg in contour])
offsetX += advanceWidths[n]
# offsetX should be the width
height = unitsPerEm
width = offsetX
mag = 1 #image scaling factor =height/unitsPerEm
# 単位法線ベクトル
def shiftalongnormal(shiftval, contours):
contours_ret = []
for contour in contours:
pts = []
for i in range(len(contour)):
lastP, lastF = contour[i-1]
currP, currF = contour[i]
nextP, nextF = contour[(i+1)%len(contour)]
#unit normals
n0 = (currP[1]-lastP[1], lastP[0]-currP[0])
a = math.sqrt( n0[0]**2 + n0[1]**2 )
n0 = ( n0[0]/a, n0[1]/a )
n1 = (nextP[1]-currP[1], currP[0]-nextP[0])
a = math.sqrt( n1[0]**2 + n1[1]**2 )
n1 = ( n1[0]/a, n1[1]/a )
n = (n0[0]+n1[0], n0[1]+n1[1])
a = math.sqrt( n[0]**2 + n[1]**2 )
n = ( n[0]/a, n[1]/a )
pt = (currP[0]+shiftval*n[0], currP[1]+shiftval*n[1])
pts.append((pt, currF))
contours_ret.append(pts)
return contours_ret
# 法線ベクトルを1/(1+cosθ)で調整
def shiftalongnormal2(shiftval, contours):
contours_ret = []
for contour in contours:
pts = []
for i in range(len(contour)):
lastP, lastF = contour[i-1]
currP, currF = contour[i]
nextP, nextF = contour[(i+1)%len(contour)]
#unit normals
n0 = (currP[1]-lastP[1], lastP[0]-currP[0])
a = math.sqrt( n0[0]**2 + n0[1]**2 )
n0 = ( n0[0]/a, n0[1]/a )
n1 = (nextP[1]-currP[1], currP[0]-nextP[0])
a = math.sqrt( n1[0]**2 + n1[1]**2 )
n1 = ( n1[0]/a, n1[1]/a )
dp = n0[0]*n1[0] + n0[1]*n1[1] #dot product (n0, n1)
dist0 = math.sqrt(n0[0]**2+n0[1]**2) # ||n0||
dist1 = math.sqrt(n1[0]**2+n1[1]**2) # ||n1||
cosval = dp / (dist0*dist1)
f = max(0.1, 1+cosval)
n = ( (n0[0]+n1[0])/f, (n0[1]+n1[1])/f)
pt = (currP[0]+shiftval*n[0], currP[1]+shiftval*n[1])
pts.append((pt, currF))
contours_ret.append(pts)
return contours_ret
def drawquadbesier(draw, p0, p1, p2, col=(0,0,0)):
# divide a curve into two recursively using De Casteljau's algorithm
draw.point((int(p0[0]),int(p0[1])), col)
draw.point((int(p2[0]),int(p2[1])), col)
p01 = ( ( p0[0]+ p1[0])/2, ( p0[1]+ p1[1])/2 )
p12 = ( ( p1[0]+ p2[0])/2, ( p1[1]+ p2[1])/2 )
p012= ( (p01[0]+p12[0])/2, (p01[1]+p12[1])/2 )
draw.point((int(p012[0]),int(p012[1])), col)
# if the distance of the vertices is far, draw recursively.
if (int(p0[0])-int(p012[0]))**2 + (int(p0[1])-int(p012[1]))**2 > 2:
drawquadbesier(draw, p0, p01, p012, col)
if (int(p2[0])-int(p012[0]))**2 + (int(p2[1])-int(p012[1]))**2 > 2:
drawquadbesier(draw, p012, p12, p2, col)
def draw_contours(draw, contours, col=(0,0,0)):
for contour in contours:
pts = []
for i in range(len(contour)):
lastP, lastF = contour[i-1]
currP, currF = contour[i]
nextP, nextF = contour[(i+1) % len(contour)]
lastP = (lastP[0]*mag, lastP[1]*mag)
currP = (currP[0]*mag, currP[1]*mag)
nextP = (nextP[0]*mag, nextP[1]*mag)
if currF == 0: # off-curve point
# insert an on-curve point between two adjacent off-curve point
if lastF == 0: # off-curve
lastP = ( (currP[0]+lastP[0])/2, (currP[1]+lastP[1])/2 )
if nextF == 0: # off-curve
nextP = ( (currP[0]+nextP[0])/2, (currP[1]+nextP[1])/2 )
drawquadbesier(draw, lastP, currP, nextP, col)
elif lastF == currF == 1: # straight line
draw.line([(int(lastP[0]),int(lastP[1])),
(int(currP[0]),int(currP[1]))],
col, width=1)
# 単位法線ベクトル
im = Image.new("RGB", (width, height), (255,255,255))
draw = ImageDraw.Draw(im)
draw_contours(draw, contours)
for i in range(1,10):
draw_contours(draw, shiftalongnormal(8*i, contours), (25*i,255-25*i,128))
for i in range(1,9):
draw_contours(draw, shiftalongnormal(-5*i, contours), (128,255-25*i,25*i))
im.save("out1.png")
# 法線ベクトルを1/(1+cosθ)で調整
im = Image.new("RGB", (width, height), (255,255,255))
draw = ImageDraw.Draw(im)
draw_contours(draw, contours)
for i in range(1,10):
draw_contours(draw, shiftalongnormal2(8*i, contours), (25*i,255-25*i,128))
for i in range(1,9):
draw_contours(draw, shiftalongnormal2(-4*i, contours), (128,255-25*i,25*i))
im.save("out2.png")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment