327 lines
12 KiB
Python
327 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import unicode_literals
|
|
'''Creates Latin script tone letters (pitch contours)'''
|
|
__url__ = 'https://github.com/silnrsi/pysilfont'
|
|
__copyright__ = 'Copyright (c) 2017 SIL International (https://www.sil.org)'
|
|
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
|
|
__author__ = 'Victor Gaultney'
|
|
|
|
# Usage: psftoneletters ifont ofont
|
|
# Assumption is that the named tone letters already exist in the font,
|
|
# so this script is only to update (rebuild) them. New tone letter spaces
|
|
# in the font can be created with psfbuildcomp.py
|
|
|
|
# To Do
|
|
# Get parameters from lib.plist org.sil.lcg.toneLetters
|
|
|
|
# main input, output, and execution handled by pysilfont framework
|
|
from silfont.core import execute
|
|
import silfont.ufo as UFO
|
|
|
|
from robofab.world import OpenFont
|
|
|
|
from math import tan, radians, sqrt
|
|
|
|
suffix = '_toneletters'
|
|
argspec = [
|
|
('ifont',{'help': 'Input font file'}, {'type': 'filename'}),
|
|
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'filename', 'def': "_"+suffix}),
|
|
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': suffix+'log'})]
|
|
|
|
|
|
def getParameters(font):
|
|
global glyphHeight, marginFlatLeft, marginPointLeft, marginFlatRight, marginPointRight, contourWidth, marginDotLeft, marginDotRight, dotSpacing, italicAngle, radius, strokeHeight, strokeDepth, contourGap, fakeBottom, dotRadius, dotBCP, contourGapDot, fakeBottomDot, anchorHeight, anchorOffset
|
|
|
|
source = font.lib.getval("org.sil.lcg.toneLetters")
|
|
|
|
strokeThickness = int(source["strokeThickness"]) # total width of stroke (ideally an even number)
|
|
glyphHeight = int(source["glyphHeight"]) # height, including overshoot
|
|
glyphDepth = int(source["glyphDepth"]) # depth - essentially overshoot (typically negative)
|
|
marginFlatLeft = int(source["marginFlatLeft"]) # left sidebearing for straight bar
|
|
marginPointLeft = int(source["marginPointLeft"]) # left sidebearing for endpoints
|
|
marginFlatRight = int(source["marginFlatRight"]) # left sidebearing for straight bar
|
|
marginPointRight = int(source["marginPointRight"]) # left sidebearing for endpoints
|
|
contourWidth = int(source["contourWidth"]) # this is how wide the contour portions are, from the middle
|
|
# of one end to the other, in the horizontal axis. The actual
|
|
# bounding box of the contours would then be this plus the
|
|
# strokeThickness.
|
|
marginDotLeft = int(source["marginDotLeft"]) # left sidebearing for dots
|
|
marginDotRight = int(source["marginDotRight"]) # right sidebearing for dots
|
|
dotSize = int(source["dotSize"]) # the diameter of the dot, normally 150% of the stroke weight
|
|
# (ideally an even number)
|
|
dotSpacing = int(source["dotSpacing"]) # the space between the edge of the dot and the
|
|
# edge of the expanded stroke
|
|
italicAngle = float(source["italicAngle"]) # angle of italic slant, 0 for upright
|
|
|
|
radius = round(strokeThickness / 2)
|
|
strokeHeight = glyphHeight - radius # for the unexpanded stroke
|
|
strokeDepth = glyphDepth + radius
|
|
strokeLength = strokeHeight - strokeDepth
|
|
contourGap = round(strokeLength / 4) # gap between contour levels
|
|
fakeBottom = strokeDepth - contourGap # a false 'bottom' for building contours
|
|
|
|
dotRadius = round(dotSize / 2) # this gets redefined during nine tone process
|
|
dotBCP = round((dotSize / 2) * .55) # this gets redefined during nine tone process
|
|
contourGapDot = round(( (glyphHeight - dotRadius) - (glyphDepth + dotRadius) ) / 4)
|
|
fakeBottomDot = (glyphDepth + dotRadius) - contourGapDot
|
|
|
|
anchorHeight = [ 0 , strokeDepth , (strokeDepth + contourGap) , (strokeDepth + contourGap * 2) , (strokeHeight - contourGap) , strokeHeight ]
|
|
anchorOffset = 20 # hardcoded for now
|
|
|
|
# drawing functions
|
|
|
|
def drawLine(glyph,startX,startY,endX,endY):
|
|
|
|
dx = (endX - startX) # dx of original stroke
|
|
dy = (endY - startY) # dy of original stroke
|
|
len = sqrt( dx * dx + dy * dy ) # length of original stroke
|
|
opp = round(dy * (radius / len)) # offsets for on-curve points
|
|
adj = round(dx * (radius / len))
|
|
oppOff = round(opp * .55) # offsets for off-curve from on-curve
|
|
adjOff = round(adj * .55)
|
|
|
|
glyph.clearContours()
|
|
|
|
pen = glyph.getPen()
|
|
|
|
# print startX + opp, startY - adj
|
|
|
|
pen.moveTo((startX + opp, startY - adj))
|
|
pen.lineTo((endX + opp, endY - adj)) # first straight line
|
|
|
|
bcp1x = endX + opp + adjOff
|
|
bcp1y = endY - adj + oppOff
|
|
bcp2x = endX + adj + oppOff
|
|
bcp2y = endY + opp - adjOff
|
|
pen.curveTo((bcp1x, bcp1y), (bcp2x, bcp2y), (endX + adj, endY + opp))
|
|
|
|
bcp1x = endX + adj - oppOff
|
|
bcp1y = endY + opp + adjOff
|
|
bcp2x = endX - opp + adjOff
|
|
bcp2y = endY + adj + oppOff
|
|
pen.curveTo((bcp1x, bcp1y), (bcp2x, bcp2y), (endX - opp, endY + adj))
|
|
|
|
pen.lineTo((startX - opp, startY + adj)) # second straight line
|
|
|
|
bcp1x = startX - opp - adjOff
|
|
bcp1y = startY + adj - oppOff
|
|
bcp2x = startX - adj - oppOff
|
|
bcp2y = startY - opp + adjOff
|
|
pen.curveTo((bcp1x, bcp1y), (bcp2x, bcp2y), (startX - adj, startY - opp))
|
|
|
|
bcp1x = startX - adj + oppOff
|
|
bcp1y = startY - opp - adjOff
|
|
bcp2x = startX + opp - adjOff
|
|
bcp2y = startY - adj - oppOff
|
|
pen.curveTo((bcp1x, bcp1y), (bcp2x, bcp2y), (startX + opp, startY - adj))
|
|
# print startX + opp, startY - adj
|
|
|
|
pen.closePath()
|
|
|
|
|
|
def drawDot(glyph,dotX,dotY):
|
|
|
|
glyph.clearContours()
|
|
|
|
pen = glyph.getPen()
|
|
|
|
pen.moveTo((dotX, dotY - dotRadius))
|
|
pen.curveTo((dotX + dotBCP, dotY - dotRadius), (dotX + dotRadius, dotY - dotBCP), (dotX + dotRadius, dotY))
|
|
pen.curveTo((dotX + dotRadius, dotY + dotBCP), (dotX + dotBCP, dotY + dotRadius), (dotX, dotY + dotRadius))
|
|
pen.curveTo((dotX - dotBCP, dotY + dotRadius), (dotX - dotRadius, dotY + dotBCP), (dotX - dotRadius, dotY))
|
|
pen.curveTo((dotX - dotRadius, dotY - dotBCP), (dotX - dotBCP, dotY - dotRadius), (dotX, dotY - dotRadius))
|
|
pen.closePath()
|
|
|
|
|
|
def adjItalX(aiX,aiY):
|
|
newX = aiX + round(tan(radians(italicAngle)) * aiY)
|
|
return newX
|
|
|
|
|
|
def buildComp(f,g,pieces,ancLevelLeft,ancLevelMidLeft,ancLevelMidRight,ancLevelRight):
|
|
|
|
g.clear()
|
|
g.width = 0
|
|
|
|
for p in pieces:
|
|
g.appendComponent(p, (g.width, 0))
|
|
g.width += f[p].width
|
|
|
|
if ancLevelLeft > 0:
|
|
anc_nm = "_TL"
|
|
anc_x = adjItalX(0,anchorHeight[ancLevelLeft])
|
|
if g.name[0:7] == 'TnStaff':
|
|
anc_x = anc_x - anchorOffset
|
|
anc_y = anchorHeight[ancLevelLeft]
|
|
g.appendAnchor(anc_nm, (anc_x, anc_y))
|
|
|
|
if ancLevelMidLeft > 0:
|
|
anc_nm = "_TL"
|
|
anc_x = adjItalX(marginPointLeft + radius,anchorHeight[ancLevelMidLeft])
|
|
anc_y = anchorHeight[ancLevelMidLeft]
|
|
g.appendAnchor(anc_nm, (anc_x, anc_y))
|
|
|
|
if ancLevelMidRight > 0:
|
|
anc_nm = "TL"
|
|
anc_x = adjItalX(g.width - marginPointRight - radius,anchorHeight[ancLevelMidRight])
|
|
anc_y = anchorHeight[ancLevelMidRight]
|
|
g.appendAnchor(anc_nm, (anc_x, anc_y))
|
|
|
|
if ancLevelRight > 0:
|
|
anc_nm = "TL"
|
|
anc_x = adjItalX(g.width,anchorHeight[ancLevelRight])
|
|
if g.name[0:7] == 'TnStaff':
|
|
anc_x = anc_x + anchorOffset
|
|
anc_y = anchorHeight[ancLevelRight]
|
|
g.appendAnchor(anc_nm, (anc_x, anc_y))
|
|
|
|
|
|
# updating functions
|
|
|
|
def updateTLPieces(targetfont):
|
|
|
|
f = targetfont
|
|
|
|
# set spacer widths
|
|
f["TnLtrSpcFlatLeft"].width = marginFlatLeft + radius
|
|
f["TnLtrSpcPointLeft"].width = marginPointLeft + radius - 1 # -1 corrects final sidebearing
|
|
f["TnLtrSpcFlatRight"].width = marginFlatRight + radius
|
|
f["TnLtrSpcPointRight"].width = marginPointRight + radius - 1 # -1 corrects final sidebearing
|
|
f["TnLtrSpcDotLeft"].width = marginDotLeft + dotRadius
|
|
f["TnLtrSpcDotMiddle"].width = dotRadius + dotSpacing + radius
|
|
f["TnLtrSpcDotRight"].width = dotRadius + marginDotRight
|
|
|
|
# redraw bar
|
|
g = f["TnLtrBar"]
|
|
drawLine(g,adjItalX(0,strokeDepth),strokeDepth,adjItalX(0,strokeHeight),strokeHeight)
|
|
g.width = 0
|
|
|
|
# redraw contours
|
|
namePre = 'TnLtrSeg'
|
|
for i in range(1,6):
|
|
for j in range(1,6):
|
|
|
|
nameFull = namePre + str(i) + str(j)
|
|
|
|
if i == 5: # this deals with round off errors
|
|
startLevel = strokeHeight
|
|
else:
|
|
startLevel = fakeBottom + i * contourGap
|
|
if j == 5:
|
|
endLevel = strokeHeight
|
|
else:
|
|
endLevel = fakeBottom + j * contourGap
|
|
|
|
g = f[nameFull]
|
|
g.width = contourWidth
|
|
drawLine(g,adjItalX(1,startLevel),startLevel,adjItalX(contourWidth-1,endLevel),endLevel)
|
|
|
|
|
|
# redraw dots
|
|
namePre = 'TnLtrDot'
|
|
for i in range(1,6):
|
|
|
|
nameFull = namePre + str(i)
|
|
|
|
if i == 5: # this deals with round off errors
|
|
dotLevel = glyphHeight - dotRadius
|
|
else:
|
|
dotLevel = fakeBottomDot + i * contourGapDot
|
|
|
|
g = f[nameFull]
|
|
drawDot(g,adjItalX(0,dotLevel),dotLevel)
|
|
|
|
|
|
def rebuildTLComps(targetfont):
|
|
|
|
f = targetfont
|
|
|
|
# staff right
|
|
for i in range(1,6):
|
|
nameFull = 'TnStaffRt' + str(i)
|
|
buildComp(f,f[nameFull],['TnLtrBar','TnLtrSpcFlatRight'],i,0,0,0)
|
|
|
|
# staff right no outline
|
|
for i in range(1,6):
|
|
nameFull = 'TnStaffRt' + str(i) + 'no'
|
|
buildComp(f,f[nameFull],['TnLtrSpcFlatRight'],i,0,0,0)
|
|
|
|
# staff left
|
|
for i in range(1,6):
|
|
nameFull = 'TnStaffLft' + str(i)
|
|
buildComp(f,f[nameFull],['TnLtrSpcFlatLeft','TnLtrBar'],0,0,0,i)
|
|
|
|
# staff left no outline
|
|
for i in range(1,6):
|
|
nameFull = 'TnStaffLft' + str(i) + 'no'
|
|
buildComp(f,f[nameFull],['TnLtrSpcFlatLeft'],0,0,0,i)
|
|
|
|
# contours right
|
|
for i in range(1,6):
|
|
for j in range(1,6):
|
|
nameFull = 'TnContRt' + str(i) + str(j)
|
|
segment = 'TnLtrSeg' + str(i) + str(j)
|
|
buildComp(f,f[nameFull],['TnLtrSpcPointLeft',segment],0,i,0,j)
|
|
|
|
# contours left
|
|
for i in range(1,6):
|
|
for j in range(1,6):
|
|
nameFull = 'TnContLft' + str(i) + str(j)
|
|
segment = 'TnLtrSeg' + str(i) + str(j)
|
|
buildComp(f,f[nameFull],[segment,'TnLtrSpcPointRight'],i,0,j,0)
|
|
|
|
# basic tone letters
|
|
for i in range(1,6):
|
|
nameFull = 'TnLtr' + str(i)
|
|
segment = 'TnLtrSeg' + str(i) + str(i)
|
|
buildComp(f,f[nameFull],['TnLtrSpcPointLeft',segment,'TnLtrBar','TnLtrSpcFlatRight'],0,0,0,0)
|
|
|
|
# basic tone letters no outline
|
|
for i in range(1,6):
|
|
nameFull = 'TnLtr' + str(i) + 'no'
|
|
segment = 'TnLtrSeg' + str(i) + str(i)
|
|
buildComp(f,f[nameFull],['TnLtrSpcPointLeft',segment,'TnLtrSpcFlatRight'],0,i,0,0)
|
|
|
|
# left stem tone letters
|
|
for i in range(1,6):
|
|
nameFull = 'LftStemTnLtr' + str(i)
|
|
segment = 'TnLtrSeg' + str(i) + str(i)
|
|
buildComp(f,f[nameFull],['TnLtrSpcFlatLeft','TnLtrBar',segment,'TnLtrSpcPointRight'],0,0,0,0)
|
|
|
|
# left stem tone letters no outline
|
|
for i in range(1,6):
|
|
nameFull = 'LftStemTnLtr' + str(i) + 'no'
|
|
segment = 'TnLtrSeg' + str(i) + str(i)
|
|
buildComp(f,f[nameFull],['TnLtrSpcFlatLeft',segment,'TnLtrSpcPointRight'],0,0,i,0)
|
|
|
|
# dotted tone letters
|
|
for i in range(1,6):
|
|
nameFull = 'DotTnLtr' + str(i)
|
|
dot = 'TnLtrDot' + str(i)
|
|
buildComp(f,f[nameFull],['TnLtrSpcDotLeft',dot,'TnLtrSpcDotMiddle','TnLtrBar','TnLtrSpcFlatRight'],0,0,0,0)
|
|
|
|
# dotted left stem tone letters
|
|
for i in range(1,6):
|
|
nameFull = 'DotLftStemTnLtr' + str(i)
|
|
dot = 'TnLtrDot' + str(i)
|
|
buildComp(f,f[nameFull],['TnLtrSpcFlatLeft','TnLtrBar','TnLtrSpcDotMiddle',dot,'TnLtrSpcDotRight'],0,0,0,0)
|
|
|
|
|
|
def doit(args):
|
|
|
|
psffont = UFO.Ufont(args.ifont, params = args.paramsobj)
|
|
rffont = OpenFont(args.ifont)
|
|
outfont = args.ofont
|
|
|
|
getParameters(psffont)
|
|
|
|
updateTLPieces(rffont)
|
|
rebuildTLComps(rffont)
|
|
|
|
|
|
rffont.save(outfont)
|
|
|
|
return
|
|
|
|
def cmd() : execute(None,doit,argspec)
|
|
if __name__ == "__main__": cmd()
|