1
0
Fork 0

Adding upstream version 1.8.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-10 05:17:32 +01:00
parent c48d95b7fa
commit e40b3259c1
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
2403 changed files with 153656 additions and 0 deletions

99
examples/FFmapGdlNames.py Executable file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env python3
'''Write mapping of graphite names to new graphite names based on:
- two ttf files
- the gdl files produced by makeGdl run against those fonts
This could be different versions of makeGdl
- a csv mapping glyph names used in original ttf to those in the new font '''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2016 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
import datetime
suffix = "_mapGDLnames2"
argspec = [
('ifont1',{'help': 'First ttf font file'}, {'type': 'infont'}),
('ifont2',{'help': 'Second ttf font file'}, {'type': 'infont'}),
('gdl1',{'help': 'Original make_gdl file'}, {'type': 'infile'}),
('gdl2',{'help': 'Updated make_gdl file'}, {'type': 'infile'}),
('-m','--mapping',{'help': 'Mapping csv file'}, {'type': 'incsv', 'def': '_map.csv'}),
('-o','--output',{'help': 'Ouput csv file'}, {'type': 'outfile', 'def': suffix+'.csv'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': suffix+'.log'}),
('--nocomments',{'help': 'No comments in output files', 'action': 'store_true', 'default': False},{})]
def doit(args) :
logger = args.paramsobj.logger
# Check input fonts are ttf
fontfile1 = args.cmdlineargs[1]
fontfile2 = args.cmdlineargs[2]
if fontfile1[-3:] != "ttf" or fontfile2[-3:] != "ttf" :
logger.log("Input fonts needs to be ttf files", "S")
font1 = args.ifont1
font2 = args.ifont2
gdlfile1 = args.gdl1
gdlfile2 = args.gdl2
mapping = args.mapping
outfile = args.output
# Add initial comments to outfile
if not args.nocomments :
outfile.write("# " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S ") + args.cmdlineargs[0] + "\n")
outfile.write("# "+" ".join(args.cmdlineargs[1:])+"\n\n")
# Process gdl files
oldgrnames = {}
for line in gdlfile1 :
# Look for lines of format <grname> = glyphid(nnn)...
pos = line.find(" = glyphid(")
if pos == -1 : continue
grname = line[0:pos]
gid = line[pos+11:line.find(")")]
oldgrnames[int(gid)]=grname
newgrnames = {}
for line in gdlfile2 :
# Look for lines of format <grname> = glyphid(nnn)...
pos = line.find(" = glyphid(")
if pos == -1 : continue
grname = line[0:pos]
gid = line[pos+11:line.find(")")]
newgrnames[int(gid)]=grname
# Process mapping file
SILnames = {}
mapping.numfields = 2
for line in mapping : SILnames[line[1]] = line[0]
# Map SIL name to gids in font 2
SILtogid2={}
for glyph in font2.glyphs(): SILtogid2[glyph.glyphname] = glyph.originalgid
# Combine all the mappings via ttf1!
cnt1 = 0
cnt2 = 0
for glyph in font1.glyphs():
gid1 = glyph.originalgid
gname1 = glyph.glyphname
gname2 = SILnames[gname1]
gid2 = SILtogid2[gname2]
oldgrname = oldgrnames[gid1] if gid1 in oldgrnames else None
newgrname = newgrnames[gid2] if gid2 in newgrnames else None
if oldgrname is None or newgrname is None :
print type(gid1), gname1, oldgrname
print gid2, gname2, newgrname
cnt2 += 1
if cnt2 > 10 : break
else:
outfile.write(oldgrname + "," + newgrname+"\n")
cnt1 += 1
print cnt1,cnt2
outfile.close()
return
execute("FF",doit, argspec)

72
examples/FFmapGdlNames2.py Executable file
View file

@ -0,0 +1,72 @@
#!/usr/bin/env python3
'''Write mapping of graphite names to new graphite names based on:
- an original ttf font
- the gdl file produced by makeGdl when original font was produced
- a csv mapping glyph names used in original ttf to those in the new font
- pysilfont's gdl library - so assumes pysilfonts makeGdl will be used with new font'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2016 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
import silfont.gdl.psnames as ps
import datetime
suffix = "_mapGDLnames"
argspec = [
('ifont',{'help': 'Input ttf font file'}, {'type': 'infont'}),
('-g','--gdl',{'help': 'Input gdl file'}, {'type': 'infile', 'def': '.gdl'}),
('-m','--mapping',{'help': 'Mapping csv file'}, {'type': 'incsv', 'def': '_map.csv'}),
('-o','--output',{'help': 'Ouput csv file'}, {'type': 'outfile', 'def': suffix+'.csv'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': suffix+'.log'}),
('--nocomments',{'help': 'No comments in output files', 'action': 'store_true', 'default': False},{})]
def doit(args) :
logger = args.paramsobj.logger
# Check input font is a ttf
fontfile = args.cmdlineargs[1]
if fontfile[-3:] != "ttf" :
logger.log("Input font needs to be a ttf file", "S")
font = args.ifont
gdlfile = args.gdl
mapping = args.mapping
outfile = args.output
# Add initial comments to outfile
if not args.nocomments :
outfile.write("# " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S ") + args.cmdlineargs[0] + "\n")
outfile.write("# "+" ".join(args.cmdlineargs[1:])+"\n\n")
# Process gdl file
oldgrnames = {}
for line in args.gdl :
# Look for lines of format <grname> = glyphid(nnn)...
pos = line.find(" = glyphid(")
if pos == -1 : continue
grname = line[0:pos]
gid = line[pos+11:line.find(")")]
oldgrnames[int(gid)]=grname
# Create map from AGL name to new graphite name
newgrnames = {}
mapping.numfields = 2
for line in mapping :
AGLname = line[1]
SILname = line[0]
grname = ps.Name(SILname).GDL()
newgrnames[AGLname] = grname
# Find glyph names in ttf font
for glyph in font.glyphs():
gid = glyph.originalgid
gname = glyph.glyphname
oldgrname = oldgrnames[gid] if gid in oldgrnames else None
newgrname = newgrnames[gname] if gname in newgrnames else None
outfile.write(oldgrname + "," + newgrname+"\n")
outfile.close()
return
execute("FF",doit, argspec)

118
examples/FLWriteXml.py Executable file
View file

@ -0,0 +1,118 @@
#!/usr/bin/env python3
'''Outputs attachment point information and notes as XML file for TTFBuilder'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'M Hosken'
# user controls
# output entries for all glyphs even those with nothing interesting to say about them
all_glyphs = 1
# output the glyph id as part of the information
output_gid = 1
# output the glyph notes
output_note = 0
# output UID with "U+" prefix
output_uid_prefix = 0
# print progress indicator
print_progress = 0
# no user serviceable parts under here!
from xml.sax.saxutils import XMLGenerator
import os
def print_glyph(font, glyph, index):
if print_progress and index % 100 == 0:
print "%d: %s" % (index, glyph.name)
if (not all_glyphs and len(glyph.anchors) == 0 and len(glyph.components) == 0 and
not (glyph.note and output_note)):
return
attribs = {}
if output_gid:
attribs["GID"] = unicode(index)
if glyph.unicode:
if output_uid_prefix:
attribs["UID"] = unicode("U+%04X" % glyph.unicode)
else:
attribs["UID"] = unicode("%04X" % glyph.unicode)
if glyph.name:
attribs["PSName"] = unicode(glyph.name)
xg.startElement("glyph", attribs)
for anchor in (glyph.anchors):
xg.startElement("point", {"type":unicode(anchor.name), "mark":unicode(anchor.mark)})
xg.startElement("location", {"x":unicode(anchor.x), "y":unicode(anchor.y)})
xg.endElement("location")
xg.endElement("point")
for comp in (glyph.components):
g = font.glyphs[comp.index]
r = g.GetBoundingRect()
x0 = 0.5 * (r.ll.x * (1 + comp.scale.x) + r.ur.x * (1 - comp.scale.x)) + comp.delta.x
y0 = 0.5 * (r.ll.y * (1 + comp.scale.y) + r.ur.y * (1 - comp.scale.y)) + comp.delta.y
x1 = 0.5 * (r.ll.x * (1 - comp.scale.x) + r.ur.x * (1 + comp.scale.x)) + comp.delta.x
y1 = 0.5 * (r.ll.y * (1 - comp.scale.x) + r.ur.y * (1 + comp.scale.y)) + comp.delta.y
attribs = {"bbox":unicode("%d, %d, %d, %d" % (x0, y0, x1, y1))}
attribs["GID"] = unicode(comp.index)
if (g.unicode):
if output_uid_prefix:
attribs["UID"] = unicode("U+%04X" % g.unicode)
else:
attribs["UID"] = unicode("%04X" % g.unicode)
if (g.name):
attribs["PSName"] = unicode(g.name)
xg.startElement("compound", attribs)
xg.endElement("compound")
if glyph.mark:
xg.startElement("property", {"name":unicode("mark"), "value":unicode(glyph.mark)})
xg.endElement("property")
if glyph.customdata:
xg.startElement("customdata", {})
xg.characters(unicode(glyph.customdata.strip()))
xg.endElement("customdata")
if glyph.note and output_note:
xg.startElement("note", {})
xg.characters(glyph.note)
xg.endElement("note")
xg.endElement("glyph")
outname = fl.font.file_name.replace(".vfb", "_tmp.xml")
fh = open(outname, "w")
xg = XMLGenerator(fh, "utf-8")
xg.startDocument()
#fl.font.full_name is needed to get the name as it appears to Windows
#fl.font.font_name seems to be the PS name. This messes up GenTest.pl when it generates WPFeatures.wpx
xg.startElement("font", {'name':unicode(fl.font.full_name), "upem":unicode(fl.font.upm)})
for i in range(0, len(fl.font.glyphs)):
print_glyph(fl.font, fl.font.glyphs[i], i)
xg.endElement("font")
xg.endDocument()
fh.close()
#somehow this enables UNC naming (\\Gutenberg vs i:) to work when Saxon is called with popen
#without this, if outname is UNC-based, then drive letters and UNC volumes are invisible
# if outname is drive-letter-based, then drive letters and UNC volumes are already visible
if (outname[0:2] == r'\\'):
os.chdir("c:")
tidy = "tidy -i -xml -n -wrap 0 --char-encoding utf8 --indent-spaces 4 --quote-nbsp no --tab-size 4 -m %s"
saxon = "saxon %s %s" % ('"' + outname + '"', r'"C:\Roman Font\rfs_font\10 Misc Utils\glyph_norm.xsl"') #handle spaces in file name
f = os.popen(saxon, "rb")
g = open(outname.replace("_tmp.xml", ".xml"), "wb")
output = f.read()
g.write(output)
f.close()
g.close()
print "Done"

24
examples/FTMLnorm.py Normal file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env python3
'Normalize an FTML file'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2016 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
import silfont.ftml as ftml
from xml.etree import cElementTree as ET
argspec = [
('infile',{'help': 'Input ftml file'}, {'type': 'infile'}),
('outfile',{'help': 'Output ftml file', 'nargs': '?'}, {'type': 'outfile', 'def': '_new.xml'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': '_ftmltest.log'})
]
def doit(args) :
f = ftml.Fxml(args.infile)
f.save(args.outfile)
def cmd() : execute("",doit,argspec)
if __name__ == "__main__": cmd()execute("", doit, argspec)

49
examples/FTaddEmptyOT.py Normal file
View file

@ -0,0 +1,49 @@
#!/usr/bin/env python3
'Add empty Opentype tables to ttf font'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2014 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'Martin Hosken'
from silfont.core import execute
from fontTools import ttLib
from fontTools.ttLib.tables import otTables
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': '_conv.log'}),
('-s','--script',{'help': 'Script tag to generate [DFLT]', 'default': 'DFLT', }, {}),
('-t','--type',{'help': 'Table to create: gpos, gsub, [both]', 'default': 'both', }, {}) ]
def doit(args) :
font = args.ifont
args.type = args.type.upper()
for tag in ('GSUB', 'GPOS') :
if tag == args.type or args.type == 'BOTH' :
table = ttLib.getTableClass(tag)()
t = getattr(otTables, tag, None)()
t.Version = 1.0
t.ScriptList = otTables.ScriptList()
t.ScriptList.ScriptRecord = []
t.FeatureList = otTables.FeatureList()
t.FeatureList.FeatureRecord = []
t.LookupList = otTables.LookupList()
t.LookupList.Lookup = []
srec = otTables.ScriptRecord()
srec.ScriptTag = args.script
srec.Script = otTables.Script()
srec.Script.DefaultLangSys = None
srec.Script.LangSysRecord = []
t.ScriptList.ScriptRecord.append(srec)
t.ScriptList.ScriptCount = 1
t.FeatureList.FeatureCount = 0
t.LookupList.LookupCount = 0
table.table = t
font[tag] = table
return font
def cmd() : execute("FT",doit, argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,34 @@
#!/usr/bin/env python3
'Demo script for accessing fields in lib.plist'
__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__ = 'David Raymond'
from silfont.core import execute
argspec = [
('ifont', {'help': 'Input font file'}, {'type': 'infont'}),
('field', {'help': 'field to access'},{})]
def doit(args):
font = args.ifont
field = args.field
lib = font.lib
if field in lib:
val = lib.getval(field)
print
print val
print
print "Field " + field + " is type " + lib[field][1].tag + " in xml"
print "The retrieved value is " + str(type(val)) + " in Python"
else:
print "Field not in lib.plist"
return
def cmd(): execute("UFO", doit, argspec)
if __name__ == "__main__": cmd()

43
examples/chaindemo.py Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env python3
''' Demo of how to chain calls to multiple scripts together.
Running
python chaindemo.py infont outfont --featfile feat.csv --uidsfile uids.csv
will run execute() against psfnormalize, psfsetassocfeat and psfsetassocuids passing the font, parameters
and logger objects from one call to the next. So:
- the font is only opened once and written once
- there is a single log file produced
'''
__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__ = 'David Raymond'
from silfont.core import execute, chain
import silfont.scripts.psfnormalize as psfnormalize
import silfont.scripts.psfsetassocfeat as psfsetassocfeat
import silfont.scripts.psfsetassocuids as psfsetassocuids
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont'}),
('--featfile',{'help': 'Associate features csv'}, {'type': 'filename'}),
('--uidsfile', {'help': 'Associate uids csv'}, {'type': 'filename'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': '_chain.log'})]
def doit(args) :
argv = ['psfnormalize', 'dummy'] # 'dummy' replaces input font since font object is being passed. Other parameters could be added.
font = chain(argv, psfnormalize.doit, psfnormalize.argspec, args.ifont, args.paramsobj, args.logger, args.quiet)
argv = ['psfsetassocfeat', 'dummy', '-i', args.featfile]
font = chain(argv, psfsetassocfeat.doit, psfsetassocfeat.argspec, font, args.paramsobj, args.logger, args.quiet)
argv = ['psfsetassocuids', 'dummy', '-i', args.uidsfile]
font = chain(argv, psfsetassocuids.doit, psfsetassocuids.argspec, font, args.paramsobj, args.logger, args.quiet)
return font
def cmd() : execute("UFO",doit, argspec)
if __name__ == "__main__": cmd()

17
examples/fbonecheck.py Normal file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env python3
'''Example profile for use with psfrunfbchecks that will just run one or more specified checks'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2022 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.fbtests.ttfchecks import psfcheck_list, make_profile, check, PASS, FAIL
# Exclude all checks bar those listed
for check in psfcheck_list:
if check not in ["org.sil/check/whitespace_widths"]:
psfcheck_list[check] = {'exclude': True}
# Create the fontbakery profile
profile = make_profile(psfcheck_list, variable_font = False)

65
examples/fbttfchecks.py Normal file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
'''Example for making project-specific changes to the standard pysilfont set of Font Bakery ttf checks.
It will start with all the checks normally run by pysilfont's ttfchecks profile then modify as described below'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2020 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.fbtests.ttfchecks import psfcheck_list, make_profile, check, PASS, FAIL
#
# General settings
#
psfvariable_font = False # Set to True for variable fonts, so different checks will be run
#
# psfcheck_list is a dictionary of all standard Fontbakery checks with a dictionary for each check indicating
# pysilfont's standard processing of that check
#
# Specifically:
# - If the dictionary has "exclude" set to True, that check will be excluded from the profile
# - If change_status is set, the status values reported by psfrunfbchecks will be changed based on its values
# - If a change in status is temporary - eg just until something is fixed, use temp_change_status instead
#
# Projects can edit this dictionary to change behaviour from Pysilfont defaults. See examples below
# To reinstate the copyright check (which is normally excluded):
psfcheck_list["com.google.fonts/check/metadata/copyright"]["exclude"] = False
# To prevent the hinting_impact check from running:
psfcheck_list["com.google.fonts/check/hinting_impact"]["exclude"] = True
# To change a FAIL status for com.google.fonts/check/whitespace_glyphnames to WARN:
psfcheck_list["com.google.fonts/check/whitespace_glyphnames"]["temp_change_status"] = {
"FAIL": "WARN", "reason": "This font currently uses non-standard names"}
#
# Create the fontbakery profile
#
profile = make_profile(psfcheck_list, variable_font = psfvariable_font)
# Add any project-specific tests (This dummy test should normally be commented out!)
@profile.register_check
@check(
id = 'org.sil/dummy',
rationale = """
There is no reason for this test!
"""
)
def org_sil_dummy():
"""Dummy test that always fails"""
if True: yield FAIL, "Oops!"
'''
Run this using
$ psfrunfbchecks --profile fbttfchecks.py <ttf file(s) to check> ...
It can also be used with fontbakery directly if you want to use options that psfrunfbchecks does not support, however
status changes will not be actioned.
$ fontbakery check-profile fbttfchecks.py <ttf file(s) to check> ...
'''

65
examples/ffchangeglyphnames.py Executable file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
from __future__ import unicode_literals
'''Update glyph names in a font based on csv file
- Using FontForge rather than UFOlib so it can work with ttf (or sfd) files'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2016 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
''' This will need updating, since FontForge is no longer supported as a tool by execute() So:
- ifont and ofont will need to be changed to have type 'filename'
- ifont will then need to be opened using fontforge.open
- The font will need to be saved with font.save
- execute will need to be called with the tool set to None instead of "FF"
'''
argspec = [
('ifont',{'help': 'Input ttf font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont'}),
('-i','--input',{'help': 'Mapping csv file'}, {'type': 'incsv', 'def': 'psnames.csv'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': '_setPostNames.log'}),
('--reverse',{'help': 'Change names in reverse', 'action': 'store_true', 'default': False},{})]
def doit(args) :
logger = args.paramsobj.logger
font = args.ifont
# Process csv
csv = args.input
csv.numfields = 2
newnames={}
namescheck=[]
missingnames = False
for line in csv :
if args.reverse :
newnames[line[1]] = line[0]
namescheck.append(line[1])
else :
newnames[line[0]] = line[1]
namescheck.append(line[0])
for glyph in font.glyphs():
gname = glyph.glyphname
if gname in newnames :
namescheck.remove(gname)
glyph.glyphname = newnames[gname]
else:
missingnames = True
logger.log(gname + " in font but not csv file","W")
if missingnames : logger.log("Font glyph names missing from csv - see log for details","E")
for name in namescheck : # Any names left in namescheck were in csv but not ttf
logger.log(name + " in csv but not in font","W")
if namescheck != [] : logger.log("csv file names missing from font - see log for details","E")
return font
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

161
examples/ffcopyglyphs.py Normal file
View file

@ -0,0 +1,161 @@
#!/usr/bin/env python3
'''FontForge: Copy glyphs from one font to another, without using ffbuilder'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015-2019 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'Martin Hosken'
from silfont.core import execute
import psMat
import io
''' This will need updating, since FontForge is no longer supported as a tool by execute() So:
- ifont and ofont will need to be changed to have type 'filename'
- ifont will then need to be opened using fontforge.open
- The font will need to be saved with font.save
- execute will need to be called with the tool set to None instead of "FF"
'''
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont', 'def': 'new'}),
('-i','--input',{'help': 'Font to get glyphs from', 'required' : True}, {'type': 'infont'}),
('-r','--range',{'help': 'StartUnicode..EndUnicode no spaces, e.g. 20..7E', 'action' : 'append'}, {}),
('--rangefile',{'help': 'File with USVs e.g. 20 or a range e.g. 20..7E or both', 'action' : 'append'}, {}),
('-n','--name',{'help': 'Include glyph named name', 'action' : 'append'}, {}),
('--namefile',{'help': 'File with glyph names', 'action' : 'append'}, {}),
('-a','--anchors',{'help' : 'Copy across anchor points', 'action' : 'store_true'}, {}),
('-f','--force',{'help' : 'Overwrite existing glyphs in the font', 'action' : 'store_true'}, {}),
('-s','--scale',{'type' : float, 'help' : 'Scale glyphs by this factor'}, {})
]
def copyglyph(font, infont, g, u, args) :
extras = set()
if args.scale is None :
scale = psMat.identity()
else :
scale = psMat.scale(args.scale)
o = font.findEncodingSlot(u)
if o == -1 :
glyph = font.createChar(u, g.glyphname)
else :
glyph = font[o]
if len(g.references) == 0 :
font.selection.select(glyph)
pen = glyph.glyphPen()
g.draw(pen)
glyph.transform(scale)
else :
for r in g.references :
t = psMat.compose(r[1], scale)
newt = psMat.compose(psMat.identity(), psMat.translate(t[4], t[5]))
glyph.addReference(r[0], newt)
extras.add(r[0])
glyph.width = g.width * scale[0]
if args.anchors :
for a in g.anchorPoints :
try :
l = font.getSubtableOfAnchor(a[1])
except EnvironmentError :
font.addAnchorClass("", a[0]*scale[0], a[1]*scale[3])
glyph.anchorPoints = g.anchorPoints
return list(extras)
def doit(args) :
font = args.ifont
infont = args.input
font.encoding = "Original"
infont.encoding = "Original" # compact the font so findEncodingSlot will work
infont.layers["Fore"].is_quadratic = font.layers["Fore"].is_quadratic
# list of glyphs to copy
glist = list()
# glyphs specified on the command line
for n in args.name or [] :
glist.append(n)
# glyphs specified in a file
for filename in args.namefile or [] :
namefile = io.open(filename, 'r')
for line in namefile :
# ignore comments
line = line.partition('#')[0]
line = line.strip()
# ignore blank lines
if (line == ''):
continue
glist.append(line)
# copy glyphs by name
reportErrors = True
while len(glist) :
tglist = glist[:]
glist = []
for n in tglist:
if n in font and not args.force :
if reportErrors :
print("Glyph {} already present. Skipping".format(n))
continue
if n not in infont :
print("Can't find glyph {}".format(n))
continue
g = infont[n]
glist.extend(copyglyph(font, infont, g, -1, args))
reportErrors = False
# list of characters to copy
ulist = list()
# characters specified on the command line
for r in args.range or [] :
(rstart, rend) = [int(x, 16) for x in r.split('..')]
for u in range(rstart, rend + 1) :
ulist.append(u)
# characters specified in a file
for filename in args.rangefile or [] :
rangefile = io.open(filename, 'r')
for line in rangefile :
# ignore comments
line = line.partition('#')[0]
line = line.strip()
# ignore blank lines
if (line == ''):
continue
# obtain USVs
try:
(rstart, rend) = line.split('..')
except ValueError:
rstart = line
rend = line
rstart = int(rstart, 16)
rend = int(rend, 16)
for u in range(rstart, rend + 1):
ulist.append(u)
# copy the characters from the generated list
for u in ulist:
o = font.findEncodingSlot(u)
if o != -1 and not args.force :
print("Glyph for {:x} already present. Skipping".format(u))
continue
e = infont.findEncodingSlot(u)
if e == -1 :
print("Can't find glyph for {:04x}".format(u))
continue
g = infont[e]
copyglyph(font, infont, g, u, args)
return font
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

31
examples/ffremovealloverlaps.py Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env python3
from __future__ import unicode_literals
'FontForge: Remove overlap on all glyphs in font'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'Victor Gaultney'
from silfont.core import execute
''' This will need updating, since FontForge is no longer supported as a tool by execute() So:
- ifont and ofont will need to be changed to have type 'filename'
- ifont will then need to be opened using fontforge.open
- The font will need to be saved with font.save
- execute will need to be called with the tool set to None instead of "FF"
'''
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont', 'def': 'new'})]
def doit(args) :
font = args.ifont
for glyph in font:
font[glyph].removeOverlap()
return font
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python3
'''FontForge: Add cmap entries for all glyphs in the font'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2016 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'Martin Hosken'
from silfont.core import execute
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont', 'def': 'new'})
]
def nextpua(p) :
if p == 0 : return 0xE000
if p == 0xF8FF : return 0xF0000
return p + 1
def doit(args) :
p = nextpua(0)
font = args.ifont
for n in font :
g = font[n]
if g.unicode == -1 :
g.unicode = p
p = nextpua(p)
return font
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,62 @@
#!/usr/bin/env python3
'FontForge: Check for duplicate USVs in unicode or altuni fields'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('-o','--output',{'help': 'Output text file'}, {'type': 'outfile', 'def': 'DupUSV.txt'})]
def doit(args) :
font = args.ifont
outf = args.output
# Process unicode and altunicode for all glyphs
usvs={}
for glyph in font:
g = font[glyph]
if g.unicode != -1:
usv=UniStr(g.unicode)
AddUSV(usvs,usv,glyph)
# Check any alternate usvs
altuni=g.altuni
if altuni != None:
for au in altuni:
usv=UniStr(au[0]) # (may need to check variant flag)
AddUSV(usvs,usv,glyph + ' (alt)')
items = usvs.items()
items = filter(lambda x: len(x[1]) > 1, items)
items.sort()
for i in items:
usv = i[0]
print usv + ' has duplicates'
gl = i[1]
glyphs = gl[0]
for j in range(1,len(gl)):
glyphs = glyphs + ', ' + gl[j]
outf.write('%s: %s\n' % (usv,glyphs))
outf.close()
print "Done!"
def UniStr(u):
if u:
return "U+{0:04X}".format(u)
else:
return "No USV" #length same as above
def AddUSV(usvs,usv,glyph):
if not usvs.has_key(usv):
usvs[usv] = [glyph]
else:
usvs[usv].append(glyph)
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,51 @@
#!/usr/bin/env python3
'Set Glyph colours based on a csv file - format glyphname,colour'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont', 'def': 'new'}),
('-i','--input',{'help': 'Input csv file'}, {'type': 'infile', 'def': 'colourGlyphs.csv'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': 'colourGlyphs.log'})]
def doit(args) :
font=args.ifont
inpf = args.input
logf = args.log
# define colours
colours = {
'black' :0x000000,
'red' :0xFF0000,
'green' :0x00FF00,
'blue' :0x0000FF,
'cyan' :0x00FFFF,
'magenta':0xFF00FF,
'yellow' :0xFFFF00,
'white' :0xFFFFFF }
# Change colour of Glyphs
for line in inpf.readlines() :
glyphn, colour = line.strip().split(",") # will exception if not 2 elements
colour=colour.lower()
if glyphn[0] in '"\'' : glyphn = glyphn[1:-1] # slice off quote marks, if present
if glyphn not in font:
logf.write("Glyph %s not in font\n" % (glyphn))
print "Glyph %s not in font" % (glyphn)
continue
g = font[glyphn]
if colour in colours.keys():
g.color=colours[colour]
else:
logf.write("Glyph: %s - non-standard colour %s\n" % (glyphn,colour))
print "Glyph: %s - non-standard colour %s" % (glyphn,colour)
logf.close()
return font
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,44 @@
#!/usr/bin/env python3
'Compare two fonts based on specified criteria and report differences'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ifont2',{'help': 'Input font file 2'}, {'type': 'infont', 'def': 'new'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': 'compareFonts.log'}),
('-o','--options',{'help': 'Options', 'choices': ['c'], 'nargs': '*'}, {})
]
def doit(args) :
font1=args.ifont
font2=args.ifont2
logf = args.log
options = args.options
logf.write("Comparing fonts: \n %s (%s)\n %s (%s)\n" % (font1.path,font1.fontname,font2.path,font2.fontname))
if options != None : logf.write('with options: %s\n' % (options))
logf.write("\n")
compare(font1,font2,logf,options)
compare(font2,font1,logf,None) # Compare again the other way around, just looking for missing Glyphs
logf.close()
return
def compare(fonta,fontb,logf,options) :
for glyph in fonta :
if glyph in fontb :
if options != None : # Do extra checks based on options supplied
ga=fonta[glyph]
gb=fontb[glyph]
for opt in options :
if opt == "c" :
if len(ga.references) != len(gb.references) :
logf.write("Glyph %s: number of components is different - %s v %s\n" % (glyph,len(ga.references),len(gb.references)))
else :
logf.write("Glyph %s missing from %s\n" % (glyph,fonta.path))
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,48 @@
#!/usr/bin/env python3
'''FontForge: Double encode glyphs based on double encoding data in a file
Lines in file should look like: "LtnSmARetrHook",U+F236,U+1D8F'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont', 'def': 'new'}),
('-i','--input',{'help': 'Input csv text file'}, {'type': 'infile', 'def': 'DblEnc.txt'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': 'DblEnc.log'})]
def doit(args) :
font = args.ifont
inpf = args.input
logf = args.log
#Create dbl_encode list from the input file
dbl_encode = {}
for line in inpf.readlines() :
glyphn, pua_usv_str, std_usv_str = line.strip().split(",") # will exception if not 3 elements
if glyphn[0] in '"\'' : glyphn = glyphn[1:-1] # slice off quote marks, if present
pua_usv, std_usv = int(pua_usv_str[2:], 16), int(std_usv_str[2:], 16)
dbl_encode[glyphn] = [std_usv, pua_usv]
inpf.close()
for glyph in sorted(dbl_encode.keys()) :
if glyph not in font:
logf.write("Glyph %s not in font\n" % (glyph))
continue
g = font[glyph]
ousvs=[g.unicode]
oalt=g.altuni
if oalt != None:
for au in oalt:
ousvs.append(au[0]) # (may need to check variant flag)
dbl = dbl_encode[glyph]
g.unicode = dbl[0]
g.altuni = ((dbl[1],),)
logf.write("encoding for %s changed: %s -> %s\n" % (glyph, ousvs, dbl))
logf.close()
return font
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,74 @@
#!/usr/bin/env python3
'''Import Attachment Point database into a fontforge font'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'Martin Hosken'
from silfont.core import execute
argspec = [
('ifont', {'help': 'Input font file'}, {'type': 'infont'}),
('ofont', {'help': 'Output font file'}, {'type': 'outfont'}),
('-a','--ap', {'nargs' : 1, 'help': 'Input AP database (required)'}, {})
]
def assign(varlist, expr) :
"""passes a variable to be assigned as a list and returns the value"""
varlist[0] = expr
return expr
def getuidenc(e, f) :
if 'UID' in e.attrib :
u = int(e.get('UID'), 16)
return f.findEncodingSlot(u)
else :
return -1
def getgid(e, f) :
if 'GID' in e.attrib :
return int(e.get('GID'))
else :
return -1
def doit(args) :
from xml.etree.ElementTree import parse
f = args.ifont
g = None
etree = parse(args.ap)
u = []
for e in etree.getroot().iterfind("glyph") :
name = e.get('PSName')
if name in f :
g = f[name]
elif assign(u, getuidenc(e, f)) != -1 :
g = f[u[0]]
elif assign(u, getgid(e, f)) != -1 :
g = f[u[0]]
elif g is not None : # assume a rename so just take next glyph
g = f[g.encoding + 1]
else :
g = f[0]
g.name = name
g.anchorPoints = ()
for p in e.iterfind('point') :
pname = p.get('type')
l = p[0]
x = int(l.get('x'))
y = int(l.get('y'))
if pname.startswith('_') :
ptype = 'mark'
pname = pname[1:]
else :
ptype = 'base'
g.addAnchorPoint(pname, ptype, float(x), float(y))
comment = []
for p in e.iterfind('property') :
comment.append("{}: {}".format(e.get('name'), e.get('value')))
for p in e.iterfind('note') :
comment.append(e.text.strip())
g.comment = "\n".join(comment)
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,38 @@
#!/usr/bin/env python3
'FontForge: Report Glyph name, number of anchors - sorted by number of anchors'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('-o','--output',{'help': 'Output text file'}, {'type': 'outfile', 'def': 'APnum.txt'})]
def doit(args) :
font = args.ifont
outf = args.output
# Make a list of glyphs and number of anchor points
AP_lst = []
for glyph in font:
AP_lst.append( [glyph, len(font[glyph].anchorPoints)] )
# Sort by numb of APs then glyphname
AP_lst.sort(AP_cmp)
for AP in AP_lst:
outf.write("%s,%s\n" % (AP[0], AP[1]))
outf.close()
print "done"
def AP_cmp(a, b): # Comparison to sort first by number of attachment points) then by Glyph name
c = cmp(a[1], b[1])
if c != 0:
return c
else:
return cmp(a[0], b[0])
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,22 @@
#!/usr/bin/env python3
'FontForge: List all gyphs with encoding and name'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('-o','--output',{'help': 'Output text file'}, {'type': 'outfile', 'def': 'Gnames.txt'})]
def doit(args) :
outf = args.output
for glyph in args.ifont:
g = args.ifont[glyph]
outf.write('%s: %s, %s\n' % (glyph, g.encoding, g.glyphname))
outf.close()
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,61 @@
#!/usr/bin/env python3
'FontForge: List all the data in a glyph object in key, value pairs'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
import fontforge, types, sys
from silfont.core import execute
argspec = [
('font',{'help': 'Input font file'}, {'type': 'infont'}),
('-o','--output',{'help': 'Output text file'}, {'type': 'outfile', 'def': 'glyphinfo.txt'})]
def doit(args) :
font=args.font
outf = args.output
glyphn = raw_input("Glyph name or number: ")
while glyphn:
isglyph=True
if not(glyphn in font):
try:
glyphn=int(glyphn)
except ValueError:
isglyph=False
else:
if not(glyphn in font):
isglyph=False
if isglyph:
g=font[glyphn]
outf.write("\n%s\n\n" % glyphn)
# Write to file all normal key,value pairs - exclude __ and built in functions
for k in dir(g):
if k[0:2] == "__": continue
attrk=getattr(g,k)
if attrk is None: continue
tk=type(attrk)
if tk == types.BuiltinFunctionType: continue
if k == "ttinstrs": # ttinstr values are not printable characters
outf.write("%s,%s\n" % (k,"<has values>"))
else:
outf.write("%s,%s\n" % (k,attrk))
# Write out all normal keys where value is none
for k in dir(g):
attrk=getattr(g,k)
if attrk is None:
outf.write("%s,%s\n" % (k,attrk))
else:
print "Invalid glyph"
glyphn = raw_input("Glyph name or number: ")
print "done"
outf.close
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,33 @@
#!/usr/bin/env python3
'FontForge: Report Glyph name, Number of references (components)'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('-o','--output',{'help': 'Output text file'}, {'type': 'outfile', 'def': 'RefNum.txt'})]
def doit(args) :
font = args.ifont
outf = args.output
outf.write("# glyphs with number of components\n\n")
for glyph in font:
gname=font[glyph].glyphname
ref = font[glyph].references
if ref is None:
n=0
else:
n=len(ref)
outf.write("%s %i\n" % (gname,n))
outf.close()
print "Done!"
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,38 @@
#!/usr/bin/env python3
'Search and replace strings in Glyph names. Strings can be regular expressions'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
import re
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont', 'def': 'new'}),
('search',{'help': 'Expression to search for'}, {}),
('replace',{'help': 'Expression to replace with'}, {}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': 'searchNReplace.log'})]
def doit(args) :
font=args.ifont
search=args.search
replace=args.replace
logf = args.log
changes=False
for glyph in font :
newname = re.sub(search, replace, glyph)
if newname != glyph :
font[glyph].glyphname=newname
changes=True
logf.write('Glyph %s renamed to %s\n' % (glyph,newname))
logf.close()
if changes :
return font
else :
return
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,50 @@
#!/usr/bin/env python3
'''FontForge: Re-encode double-encoded glyphs based on double encoding data in a file
Lines in file should look like: "LtnSmARetrHook",U+F236,U+1D8F'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont', 'def': 'new'}),
('-i','--input',{'help': 'Input csv text file'}, {'type': 'infile', 'def': 'DblEnc.txt'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': 'unDblEnc.log'})]
def doit(args) :
font=args.ifont
inpf = args.input
logf = args.log
# Create dbl_encode list from the input file
dbl_encode = {}
for line in inpf.readlines():
glyphn, pua_usv_str, std_usv_str = line.strip().split(",") # will exception if not 3 elements
if glyphn[0] in '"\'' : glyphn = glyphn[1:-1] # slice off quote marks, if present
pua_usv, std_usv = int(pua_usv_str[2:], 16), int(std_usv_str[2:], 16)
dbl_encode[glyphn] = [std_usv, pua_usv]
inpf.close()
for glyph in sorted(dbl_encode.keys()):
logf.write (reincode(font,glyph,dbl_encode[glyph][0]))
logf.write (reincode(font,glyph+"Dep",dbl_encode[glyph][1]))
logf.close()
return font
def reincode(font,glyph,usv):
if glyph not in font:
return ("Glyph %s not in font\n" % (glyph))
g = font[glyph]
ousvs=[g.unicode]
oalt=g.altuni
if oalt != None:
for au in oalt:
ousvs.append(au[0]) # (may need to check variant flag)
g.unicode = usv
g.altuni = None
return ("encoding for %s changed: %s -> %s\n" % (glyph, ousvs, usv))
def cmd() : execute("FF",doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,38 @@
#!/usr/bin/env python3
'FontForge: Demo script to add menu items to FF tools menu'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2014 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
import sys, os, fontforge
sys.path.append(os.path.join(os.environ['HOME'], 'src/pysilfont/scripts'))
import samples.demoFunctions
from samples.demoFunctions import functionList, callFunctions
#from samples.demoCallFunctions import callFunctions
def toolMenuFunction(functionGroup,font) :
reload (samples.demoFunctions)
callFunctions(functionGroup,font)
funcList=functionList()
for functionGroup in funcList :
menuType = funcList[functionGroup][0]
fontforge.registerMenuItem(toolMenuFunction,None,functionGroup,menuType,None,functionGroup);
print functionGroup, " registered"
''' This script needs to be called from one of the folders that FontForge looks in for scripts to
run when it is started. With current versions of FontForge, one is Home/.config/fontforge/python.
You may need to turn on showing hidden files (ctrl-H in Nautilus) before you can see the .config
folder. Within there create a one-line python script, say call sampledemo.py containing a call
to this script, eg:
execfile("/home/david/src/pysilfont/scripts/samples/demoAddToMenu.py")
Due to the reload(samples.demoFunctions) line above, changes functions defined in demoFunctions.py
are dynamic, ie FontForge does not have to be restarted (as would be the case if the functions were
called directly from the tools menu. Functions can even be added dynamically to the function groups.
If new function groups are defined, FontForge does have to be restarted to add them to the tools menu.
'''

View file

@ -0,0 +1,29 @@
#!/usr/bin/env python3
'FontForge: Demo code to paste into the "Execute Script" dialog'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2013 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
import sys, os, fontforge
sys.path.append(os.path.join(os.environ['HOME'], 'src/pysilfont/scripts'))
import samples.demoFunctions # Loads demoFunctions.py module from src/pysilfont/scripts/samples
reload (samples.demoFunctions) # Reload the demo module each time you execute the script to pick up any recent edits
samples.demoFunctions.callFunctions("Colour Glyphs",fontforge.activeFont())
'''Demo usage:
Open the "Execute Script" dialog (from the FontForge File menu or press ctrl+.),
paste just the code section this (from "import..." to "samples...") into there then
run it (Alt+o) and see how it pops up a dialogue with a choice of 3 functions to run.
Edit demoFunctions.py and alter one of the functions.
Execute the script again and see that that the function's behaviour has changed.
Additional functions can be added to demoFunctions.py and, if also defined functionList()
become availably immdiately.
If you want to see the output from print statements, or use commands like input, (eg
for degugging purposes) then start FontForge from a terminal window rather than the
desktop launcher.
When starting from a terminal window, you can also specify the font to use,
eg $ fontforge /home/david/RFS/GenBasR.sfd'''

View file

@ -0,0 +1,90 @@
#!/usr/bin/env python3
'FontForge: Sample functions to call from other demo scripts'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2014 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
import fontforge
def colLtnAGlyphs(font) :
#print "Toggling colour of glyphs with LtnCapA in their name"
for glyph in font:
g = font[glyph]
if glyph.find('LtnCapA') >= 0:
if g.color != 0x00FF00:
g.color = 0x00FF00 # Green
else :
g.color = 0xFFFFFF # White
print "LtnCapA glyphs coloured"
def markOverlaps(font) :
print "Toggling colour of glyphs where contours overlap"
for glyph in font:
g = font[glyph]
if g.selfIntersects() :
if g.color != 0xFF0000:
g.color = 0xFF0000 # Red
else :
g.color = 0xFFFFFF # White
print "Glyphs coloured"
def markScaled(font) :
print "Toggling colour of glyphs with scaled components"
for glyph in font:
g = font[glyph]
for ref in g.references:
transform=ref[1]
if transform[0] != 1.0 or transform[3] != 1.0 :
if g.color != 0xFF0000:
g.color = 0xFF0000 # Red
else :
g.color = 0xFFFFFF # White
print "Glyphs coloured"
def clearColours(font) :
for glyph in font :
g = font[glyph]
g.color = 0xFFFFFF
def functionList() :
''' Returns a dictionary to be used by callFunctions() and demoAddToMenu.py
The dictionary is indexed by a group name which could be used as Tools menu
entry or to reference the group of functions. For each group there is a tuple
consisting of the Tools menu type (Font or Glyph) then one tuple per function.
For each function the tuple contains:
Function name
Label for the individual function in dialog box called from Tools menu
Actual function object'''
funcList = {
"Colour Glyphs":("Font",
("colLtnAGlyphs","Colour Latin A Glyphs",colLtnAGlyphs),
("markOverlaps","Mark Overlaps",markOverlaps),
("markScaled","Mark Scaled",markScaled),
("clearColours","Clear all colours",clearColours)),
"Group with single item":("Font",
("clearColours","Clear all colours",clearColours))}
return funcList
def callFunctions(functionGroup,font) :
funcList=functionList()[functionGroup]
i=0
for tuple in funcList :
if i == 0 :
pass # Font/Glyph parameter not relevant here
elif i == 1 :
functionDescs=[tuple[1]]
functions=[tuple[2]]
else :
functionDescs.append(tuple[1])
functions.append(tuple[2])
i=i+1
if i == 2 : # Only one function in the group, so just call the function
functions[0](font)
else :
functionNum=fontforge.ask(functionGroup,"Please choose the function to run",functionDescs)
functions[functionNum](font)

20
examples/gdl/__init__.py Normal file
View file

@ -0,0 +1,20 @@
# Copyright 2012, SIL International
# All rights reserved.
#
# This library is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; either version 2.1 of License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should also have received a copy of the GNU Lesser General Public
# License along with this library in the file named "LICENSE".
# If not, write to the Free Software Foundation, 51 Franklin Street,
# suite 500, Boston, MA 02110-1335, USA or visit their web page on the
# internet at https://www.fsf.org/licenses/lgpl.html.
__all__ = ['makegdl', 'psnames']

394
examples/gdl/font.py Normal file
View file

@ -0,0 +1,394 @@
#!/usr/bin/env python
'The main font object for GDL creation. Depends on fonttools'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2012 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
import os, re, traceback
from silfont.gdl.glyph import Glyph
from silfont.gdl.psnames import Name
from xml.etree.cElementTree import ElementTree, parse, Element
from fontTools.ttLib import TTFont
# A collection of glyphs that have a given attachment point defined
class PointClass(object) :
def __init__(self, name) :
self.name = name
self.glyphs = []
self.dias = []
def addBaseGlyph(self, g) :
self.glyphs.append(g)
def addDiaGlyph(self, g) :
self.dias.append(g)
g.isDia = True
def hasDias(self) :
if len(self.dias) and len(self.glyphs) :
return True
else :
return False
def classGlyphs(self, isDia = False) :
if isDia :
return self.dias
else :
return self.glyphs
def isNotInClass(self, g, isDia = False) :
if not g : return False
if not g.isDia : return False
if isDia :
return g not in self.dias
else :
return g not in self.dias and g not in self.glyphs
class FontClass(object) :
def __init__(self, elements = None, fname = None, lineno = None, generated = False, editable = False) :
self.elements = elements or []
self.fname = fname
self.lineno = lineno
self.generated = generated
self.editable = editable
def append(self, element) :
self.elements.append(element)
class Font(object) :
def __init__(self, fontfile) :
self.glyphs = []
self.psnames = {}
self.canons = {}
self.gdls = {}
self.anchors = {}
self.ligs = {}
self.subclasses = {}
self.points = {}
self.classes = {}
self.aliases = {}
self.rules = {}
self.posRules = {}
if fontfile :
self.font = TTFont(fontfile)
for i, n in enumerate(self.font.getGlyphOrder()) :
self.addGlyph(i, n)
else :
self.font = None
def __len__(self) :
return len(self.glyphs)
# [] syntax returns the indicated element of the glyphs array.
def __getitem__(self, y) :
try :
return self.glyphs[y]
except IndexError :
return None
def glyph(self, name) :
return self.psnames.get(name, None)
def alias(self, s) :
return self.aliases.get(s, s)
def emunits(self) :
return 0
def initGlyphs(self, nGlyphs) :
#print "Font::initGlyphs",nGlyphs
self.glyphs = [None] * nGlyphs
self.numRealGlyphs = nGlyphs # does not include pseudo-glyphs
self.psnames = {}
self.canons = {}
self.gdls = {}
self.classes = {}
def addGlyph(self, index = None, psName = None, gdlName = None, factory = Glyph) :
#print "Font::addGlyph",index,psName,gdlName
if psName in self.psnames :
return self.psnames[psName]
if index is not None and index < len(self.glyphs) and self.glyphs[index] :
g = self.glyphs[index]
return g
g = factory(psName, index) # create a new glyph of the given class
self.renameGlyph(g, psName, gdlName)
if index is None : # give it the next available index
index = len(self.glyphs)
self.glyphs.append(g)
elif index >= len(self.glyphs) :
self.glyphs.extend([None] * (len(self.glyphs) - index + 1))
self.glyphs[index] = g
return g
def renameGlyph(self, g, name, gdlName = None) :
if g.psname != name :
for n in g.parseNames() :
del self.psnames[n.psname]
del self.canons[n.canonical()]
if gdlName :
self.setGDL(g, gdlName)
else :
self.setGDL(g, g.GDLName())
for n in g.parseNames() :
if n is None : break
self.psnames[n.psname] = g
self.canons[n.canonical()] = (n, g)
def setGDL(self, glyph, name) :
if not glyph : return
n = glyph.GDLName()
if n != name and n in self.gdls : del self.gdls[n]
if name and name in self.gdls and self.gdls[name] is not glyph :
count = 1
index = -2
name = name + "_1"
while name in self.gdls :
if self.gdls[name] is glyph : break
count = count + 1
name = name[0:index] + "_" + str(count)
if count == 10 : index = -3
if count == 100 : index = -4
self.gdls[name] = glyph
glyph.setGDL(name)
def addClass(self, name, elements, fname = None, lineno = 0, generated = False, editable = False) :
if name :
self.classes[name] = FontClass(elements, fname, lineno, generated, editable)
def addGlyphClass(self, name, gid, editable = False) :
if name not in self.classes :
self.classes[name] = FontClass()
if gid not in self.classes[name].elements :
self.classes[name].append(gid)
def addRules(self, rules, index) :
self.rules[index] = rules
def addPosRules(self, rules, index) :
self.posRules[index] = rules
def classUpdated(self, name, value) :
c = []
if name in self.classes :
for gid in self.classes[name].elements :
g = self[gid]
if g : g.removeClass(name)
if value is None and name in classes :
del self.classes[name]
return
for n in value.split() :
g = self.gdls.get(n, None)
if g :
c.append(g.gid)
g.addClass(name)
if name in self.classes :
self.classes[name].elements = c
else :
self.classes[name] = FontClass(c)
# Return the list of classes that should be updated in the AP XML file.
# This does not include classes that are auto-generated or defined in the hand-crafted GDL code.
def filterAutoClasses(self, names, autoGdlFile) :
res = []
for n in names :
c = self.classes[n]
if not c.generated and (not c.fname or c.fname == autoGdlFile) : res.append(n)
return res
def loadAlias(self, fname) :
with open(fname) as f :
for l in f.readlines() :
l = l.strip()
l = re.sub(ur'#.*$', '', l).strip()
if not len(l) : continue
try :
k, v = re.split(ur'\s*[,;\s]\s*', l, 1)
except ValueError :
k = l
v = ''
self.aliases[k] = v
# TODO: move this method to GraideFont, or refactor
def loadAP(self, apFileName) :
if not os.path.exists(apFileName) : return False
etree = parse(apFileName)
self.initGlyphs(len(etree.getroot())) # guess each child is a glyph
i = 0
for e in etree.getroot().iterfind("glyph") :
g = self.addGlyph(i, e.get('PSName'))
g.readAP(e, self)
i += 1
return True
def saveAP(self, apFileName, autoGdlFile) :
root = Element('font')
root.set('upem', str(self.emunits()))
root.set('producer', 'graide 1.0')
root.text = "\n\n"
for g in self.glyphs :
if g : g.createAP(root, self, autoGdlFile)
ElementTree(root).write(apFileName, encoding="utf-8", xml_declaration=True)
def createClasses(self) :
self.subclasses = {}
for k, v in self.canons.items() :
if v[0].ext :
h = v[0].head()
o = self.canons.get(h.canonical(), None)
if o :
if v[0].ext not in self.subclasses : self.subclasses[v[0].ext] = {}
self.subclasses[v[0].ext][o[1].GDLName()] = v[1].GDLName()
# for g in self.glyphs :
# if not g : continue
# for c in g.classes :
# if c not in self.classes :
# self.classes[c] = []
# self.classes[c].append(g.gid)
def calculatePointClasses(self) :
self.points = {}
for g in self.glyphs :
if not g : continue
for apName in g.anchors.keys() :
genericName = apName[:-1] # without the M or S
if genericName not in self.points :
self.points[genericName] = PointClass(genericName)
if apName.endswith('S') :
self.points[genericName].addBaseGlyph(g)
else :
self.points[genericName].addDiaGlyph(g)
def calculateOTLookups(self) :
if self.font :
for t in ('GSUB', 'GPOS') :
if t in self.font :
self.font[t].table.LookupList.process(self)
def getPointClasses(self) :
if len(self.points) == 0 :
self.calculatePointClasses()
return self.points
def ligClasses(self) :
self.ligs = {}
for g in self.glyphs :
if not g or not g.name : continue
(h, t) = g.name.split_last()
if t :
o = self.canons.get(h.canonical(), None)
if o and o[0].ext == t.ext :
t.ext = None
t.cname = None
tn = t.canonical(noprefix = True)
if tn in self.ligs :
self.ligs[tn].append((g.GDLName(), o[0].GDL()))
else :
self.ligs[tn] = [(g.GDLName(), o[0].GDL())]
def outGDL(self, fh, args) :
munits = self.emunits()
fh.write('table(glyph) {MUnits = ' + str(munits) + '};\n')
nglyphs = 0
for g in self.glyphs :
if not g or not g.psname : continue
if g.psname == '.notdef' :
fh.write(g.GDLName() + ' = glyphid(0)')
else :
fh.write(g.GDLName() + ' = postscript("' + g.psname + '")')
outs = []
if len(g.anchors) :
for a in g.anchors.keys() :
v = g.anchors[a]
outs.append(a + "=point(" + str(int(v[0])) + "m, " + str(int(v[1])) + "m)")
for (p, v) in g.gdl_properties.items() :
outs.append("%s=%s" % (p, v))
if len(outs) : fh.write(" {" + "; ".join(outs) + "}")
fh.write(";\n")
nglyphs += 1
fh.write("\n")
fh.write("\n/* Point Classes */\n")
for p in sorted(self.points.values(), key=lambda x: x.name) :
if not p.hasDias() : continue
n = p.name + "Dia"
self.outclass(fh, "c" + n, p.classGlyphs(True))
self.outclass(fh, "cTakes" + n, p.classGlyphs(False))
self.outclass(fh, 'cn' + n, filter(lambda x : p.isNotInClass(x, True), self.glyphs))
self.outclass(fh, 'cnTakes' + n, filter(lambda x : p.isNotInClass(x, False), self.glyphs))
fh.write("\n/* Classes */\n")
for c in sorted(self.classes.keys()) : # c = class name, l = class object
if c not in self.subclasses and not self.classes[c].generated : # don't output the class to the AP file if it was autogenerated
self.outclass(fh, c, self.classes[c].elements)
for p in self.subclasses.keys() :
ins = []
outs = []
for k, v in self.subclasses[p].items() :
ins.append(k)
outs.append(v)
n = p.replace('.', '_')
self.outclass(fh, 'cno_' + n, ins)
self.outclass(fh, 'c' + n, outs)
fh.write("/* Ligature Classes */\n")
for k in sorted(self.ligs.keys()) :
self.outclass(fh, "clig" + k, map(lambda x: self.gdls[x[0]], self.ligs[k]))
self.outclass(fh, "cligno_" + k, map(lambda x: self.gdls[x[1]], self.ligs[k]))
fh.write("\nendtable;\n")
fh.write("/* Substitution Rules */\n")
for k, v in sorted(self.rules.items(), key=lambda x:map(int,x[0].split('_'))) :
fh.write('\n// lookup ' + k + '\n')
fh.write('// ' + "\n// ".join(v) + "\n")
fh.write("\n/* Positioning Rules */\n")
for k, v in sorted(self.posRules.items(), key=lambda x:map(int,x[0].split('_'))) :
fh.write('\n// lookup ' + k + '\n')
fh.write('// ' + "\n// ".join(v) + "\n")
fh.write("\n\n#define MAXGLYPH %d\n\n" % (nglyphs - 1))
if args.include :
fh.write("#include \"%s\"\n" % args.include)
def outPosRules(self, fh, num) :
fh.write("""
#ifndef opt2
#define opt(x) [x]?
#define opt2(x) [opt(x) x]?
#define opt3(x) [opt2(x) x]?
#define opt4(x) [opt3(x) x]?
#endif
#define posrule(x) c##x##Dia {attach{to=@1; at=x##S; with=x##M}} / cTakes##x##Dia opt4(cnTakes##x##Dia) _;
table(positioning);
pass(%d);
""" % num)
for p in self.points.values() :
if p.hasDias() :
fh.write("posrule(%s);\n" % p.name)
fh.write("endpass;\nendtable;\n")
def outclass(self, fh, name, glyphs) :
fh.write(name + " = (")
count = 1
sep = ""
for g in glyphs :
if not g : continue
if isinstance(g, basestring) :
fh.write(sep + g)
else :
if g.GDLName() is None :
print "Can't output " + str(g.gid) + " to class " + name
else :
fh.write(sep + g.GDLName())
if count % 8 == 0 :
sep = ',\n '
else :
sep = ', '
count += 1
fh.write(');\n\n')

174
examples/gdl/glyph.py Normal file
View file

@ -0,0 +1,174 @@
#!/usr/bin/env python
'Corresponds to a glyph, for analysis purposes, for GDL generation'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2012 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
import re, traceback
from silfont.gdl.psnames import Name
from xml.etree.cElementTree import SubElement
# Convert from Graphite AP name to the standard name, eg upperM -> _upper
def gr_ap(txt) :
if txt.endswith('M') :
return "_" + txt[:-1]
elif txt.endswith('S') :
return txt[:-1]
else :
return txt
# Convert from standard AP name to the Graphite name, eg _upper -> upperM
def ap_gr(txt) :
if txt.startswith('_') :
return txt[1:] + 'M'
else :
return txt + 'S'
class Glyph(object) :
isDia = False
def __init__(self, name, gid = 0) :
self.clear()
self.setName(name)
self.gdl = None
self.gid = gid
self.uid = "" # this is a string!
self.comment = ""
self.isDia = False
def clear(self) :
self.anchors = {}
self.classes = set()
self.gdl_properties = {}
self.properties = {}
def setName(self, name) :
self.psname = name
self.name = next(self.parseNames())
def setAnchor(self, name, x, y, t = None) :
send = True
if name in self.anchors :
if x is None and y is None :
del self.anchors[name]
return True
if x is None : x = self.anchors[name][0]
if y is None : y = self.anchors[name][1]
send = self.anchors[name] != (x, y)
self.anchors[name] = (x, y)
return send
# if not name.startswith("_") and t != 'basemark' :
# self.isBase = True
def parseNames(self) :
if self.psname :
for name in self.psname.split("/") :
res = Name(name)
yield res
else :
yield None
def GDLName(self) :
if self.gdl :
return self.gdl
elif self.name :
return self.name.GDL()
else :
return None
def setGDL(self, name) :
self.gdl = name
def readAP(self, elem, font) :
self.uid = elem.get('UID', None)
for p in elem.iterfind('property') :
n = p.get('name')
if n == 'GDLName' :
self.setGDL(p.get('value'))
elif n.startswith('GDL_') :
self.gdl_properties[n[4:]] = p.get('value')
else :
self.properties[n] = p.get('value')
for p in elem.iterfind('point') :
l = p.find('location')
self.setAnchor(ap_gr(p.get('type')), int(l.get('x', 0)), int(l.get('y', 0)))
p = elem.find('note')
if p is not None and p.text :
self.comment = p.text
if 'classes' in self.properties :
for c in self.properties['classes'].split() :
if c not in self.classes :
self.classes.add(c)
font.addGlyphClass(c, self, editable = True)
def createAP(self, elem, font, autoGdlFile) :
e = SubElement(elem, 'glyph')
if self.psname : e.set('PSName', self.psname)
if self.uid : e.set('UID', self.uid)
if self.gid is not None : e.set('GID', str(self.gid))
ce = None
if 'classes' in self.properties and self.properties['classes'].strip() :
tempClasses = self.properties['classes']
self.properties['classes'] = " ".join(font.filterAutoClasses(self.properties['classes'].split(), autoGdlFile))
for k in sorted(self.anchors.keys()) :
v = self.anchors[k]
p = SubElement(e, 'point')
p.set('type', gr_ap(k))
p.text = "\n "
l = SubElement(p, 'location')
l.set('x', str(v[0]))
l.set('y', str(v[1]))
l.tail = "\n "
if ce is not None : ce.tail = "\n "
ce = p
for k in sorted(self.gdl_properties.keys()) :
if k == "*skipPasses*" : continue # not set in GDL
v = self.gdl_properties[k]
if v :
p = SubElement(e, 'property')
p.set('name', 'GDL_' + k)
p.set('value', v)
if ce is not None : ce.tail = "\n "
ce = p
if self.gdl and (not self.name or self.gdl != self.name.GDL()) :
p = SubElement(e, 'property')
p.set('name', 'GDLName')
p.set('value', self.GDLName())
if ce is not None : ce.tail = "\n "
ce = p
for k in sorted(self.properties.keys()) :
v = self.properties[k]
if v :
p = SubElement(e, 'property')
p.set('name', k)
p.set('value', v)
if ce is not None : ce.tail = "\n "
ce = p
if self.comment :
p = SubElement(e, 'note')
p.text = self.comment
if ce is not None : ce.tail = "\n "
ce = p
if 'classes' in self.properties and self.properties['classes'].strip() :
self.properties['classes'] = tempClasses
if ce is not None :
ce.tail = "\n"
e.text = "\n "
e.tail = "\n"
return e
def isMakeGDLSpecialClass(name) :
# if re.match(r'^cn?(Takes)?.*?Dia$', name) : return True
# if name.startswith('clig') : return True
# if name.startswith('cno_') : return True
if re.match(r'^\*GC\d+\*$', name) : return True # auto-pseudo glyph with name = *GCXXXX*
return False

31
examples/gdl/makeGdl.py Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env python
'Analyse a font and generate GDL to help with the creation of graphite fonts'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
from gdl.font import Font
import gdl.ot
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument('infont')
parser.add_argument('outgdl')
parser.add_argument('-a','--ap')
parser.add_argument('-i','--include')
parser.add_argument('-y','--alias')
args = parser.parse_args()
f = Font(args.infont)
if args.alias : f.loadAlias(args.alias)
if args.ap : f.loadAP(args.ap)
f.createClasses()
f.calculateOTLookups()
f.calculatePointClasses()
f.ligClasses()
outf = open(args.outgdl, "w")
f.outGDL(outf, args)
outf.close()

448
examples/gdl/ot.py Normal file
View file

@ -0,0 +1,448 @@
#!/usr/bin/env python
'OpenType analysis for GDL conversion'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2012 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
import re, traceback, logging
from fontTools.ttLib.tables import otTables
def compress_strings(strings) :
'''If we replace one column in the string with different lists, can we reduce the number
of strings? Each string is a tuple of the string and a single value that will be put into
a class as well when list compression occurs'''
maxlen = max(map(lambda x: len(x[0]), strings))
scores = []
for r in range(maxlen) :
allchars = {}
count = 0
for s in strings :
if r >= len(s[0]) : continue
c = tuple(s[0][0:r] + (s[0][r+1:] if r < len(s[0]) - 1 else []))
if c in allchars :
allchars[c] += 1
else :
allchars[c] = 0
count += 1
scores.append((max(allchars.values()), len(allchars), count))
best = maxlen
bestr = 0
for r in range(maxlen) :
score = maxlen - (scores[r][2] - scores[r][1])
if score < best :
best = score
bestr = r
numstrings = len(strings)
i = 0
allchars = {}
while i < len(strings) :
s = strings[i][0]
if bestr >= len(s) :
i += 1
continue
c = tuple(s[0:bestr] + (s[bestr+1:] if bestr < len(s) - 1 else []))
if c in allchars :
allchars[c][1].append(s[bestr])
allchars[c][2].append(strings[i][1])
strings.pop(i)
else :
allchars[c] = [i, [s[bestr]], [strings[i][1]]]
i += 1
for v in allchars.values() :
if len(set(v[1])) != 1 : # if all values in the list identical, don't output list
strings[v[0]][0][bestr] = v[1]
if len(v[2]) > 1 : # don't need a list if length 1
strings[v[0]][1] = v[2]
return strings
def make_rule(left, right = None, before = None, after = None) :
res = " ".join(map(lambda x: x or "_", left))
if right :
res += " > " + " ".join(map(lambda x: x or "_", right))
if before or after :
res += " / "
if before : res += " ".join(map(lambda x: x or 'ANY', before))
res += " " + "_ " * len(left) + " "
if after : res += " ".join(map(lambda x: x or 'ANY', after))
res += ";"
return res
def add_class_classes(font, name, ctable) :
vals = {}
for k, v in ctable.classDefs.items() :
if v not in vals : vals[v] = []
vals[v].append(k)
numk = max(vals.keys())
res = [None] * (numk + 1)
for k, v in vals.items() :
if len(v) > 1 :
res[k] = font.alias(name+"{}".format(k))
font.addClass(res[k], map(font.glyph, v))
else :
res[k] = font.glyph(v[0]).GDLName()
return res
vrgdlmap = {
'XPlacement' : 'shift.x',
'YPlacement' : 'shift.y',
'XAdvance' : 'advance'
}
def valuerectogdl(vr) :
res = "{"
for k, v in vrgdlmap.items() :
if hasattr(vr, k) :
res += "{}={}; ".format(v, getattr(vr, k))
res = res[:-1] + "}"
if len(res) == 1 : return ""
return res
def _add_method(*clazzes):
"""Returns a decorator function that adds a new method to one or
more classes."""
def wrapper(method):
for c in clazzes:
assert c.__name__ != 'DefaultTable', \
'Oops, table class not found.'
assert not hasattr(c, method.__name__), \
"Oops, class '%s' has method '%s'." % (c.__name__,
method.__name__)
setattr(c, method.__name__, method)
return None
return wrapper
@_add_method(otTables.Lookup)
def process(self, font, index) :
for i, s in enumerate(self.SubTable) :
if hasattr(s, 'process') :
s.process(font, index + "_{}".format(i))
else :
logging.warning("No processing of {} {}_{}".format(str(s), index, i))
@_add_method(otTables.LookupList)
def process(self, font) :
for i, s in enumerate(self.Lookup) :
s.process(font, str(i))
@_add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
def process(self, font, index) :
x = self.ExtSubTable
if hasattr(x, 'process') :
x.process(font, index)
else :
logging.warning("No processing of {} {}".format(str(x), index))
@_add_method(otTables.SingleSubst)
def process(self, font, index) :
cname = "cot_s{}".format(index)
if not len(font.alias(cname)) : return
lists = zip(*self.mapping.items())
font.addClass(font.alias(cname+"l"), map(font.glyph, lists[0]))
font.addClass(font.alias(cname+"r"), map(font.glyph, lists[1]))
@_add_method(otTables.MultipleSubst)
def process(self, font, index) :
cname = "cot_m{}".format(index)
if not len(font.alias(cname)) : return
nums = len(self.Coverage.glyphs)
strings = []
for i in range(nums) :
strings.append([self.Sequence[i].Substitute, self.Coverage.glyphs[i]])
res = compress_strings(strings)
count = 0
rules = []
for r in res :
if hasattr(r[1], '__iter__') :
lname = font.alias(cname+"l{}".format(count))
font.addClass(lname, map(font.glyph, r[1]))
rule = lname
else :
rule = font.glyph(r[1]).GDLName()
rule += " _" * (len(r[0]) - 1) + " >"
for c in r[0] :
if hasattr(c, '__iter__') :
rname = font.alias(cname+"r{}".format(count))
font.addClass(rname, map(font.glyph, c))
rule += " " + rname
count += 1
else :
rule += " " + font.glyph(c).GDLName()
rule += ';'
rules.append(rule)
font.addRules(rules, index)
@_add_method(otTables.LigatureSubst)
def process(self, font, index) :
cname = "cot_l{}".format(index)
if not len(font.alias(cname)) : return
strings = []
for lg, ls in self.ligatures.items() :
for l in ls :
strings.append([[lg] + l.Component, l.LigGlyph])
res = compress_strings(strings)
count = 0
rules = []
for r in res :
rule = ""
besti = 0
for i, c in enumerate(r[0]) :
if hasattr(c, '__iter__') :
lname = font.alias(cname+"l{}".format(count))
font.addClass(lname, map(font.glyph, c))
rule += lname + " "
besti = i
else :
rule += font.glyph(c).GDLName() + " "
rule += "> " + "_ " * besti
if hasattr(r[1], '__iter__') :
rname = font.alias(cname+"r{}".format(count))
font.addClass(rname, map(font.glyph, r[1]))
rule += rname
count += 1
else :
rule += font.glyph(r[1]).GDLName()
rule += " _" * (len(r[0]) - 1 - besti) + ";"
rules.append(rule)
font.addRules(rules, index)
@_add_method(otTables.ChainContextSubst)
def process(self, font, index) :
def procsubst(rule, action) :
for s in rule.SubstLookupRecord :
action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
def procCover(cs, name) :
res = []
for i, c in enumerate(cs) :
if len(c.glyphs) > 1 :
n = font.alias(name+"{}".format(i))
font.addClass(n, map(font.glyph, c.glyphs))
res.append(n)
else :
res.append(font.glyph(c.glyphs[0]).GDLName())
return res
cname = "cot_c{}".format(index)
if not len(font.alias(cname)) : return
rules = []
if self.Format == 1 :
for i in range(len(self.ChainSubRuleSet)) :
for r in self.ChainSubRuleSet[i].ChainSubRule :
action = [self.Coverage.glyphs[i]] + r.Input
procsubst(r, action)
rules.append(make_rule(action, None, r.Backtrack, r.LookAhead))
elif self.Format == 2 :
ilist = add_class_classes(font, cname+"i", self.InputClassDef)
if self.BacktrackClassDef :
blist = add_class_classes(font, cname+"b", self.BacktrackClassDef)
if self.LookAheadClassDef :
alist = add_class_classes(font, cname+"a", self.LookAheadClassDef)
for i, s in enumerate(self.ChainSubClassSet) :
if s is None : continue
for r in s.ChainSubClassRule :
action = map(lambda x:ilist[x], [i]+r.Input)
procsubst(r, action)
rules.append(make_rule(action, None,
map(lambda x:blist[x], r.Backtrack or []),
map(lambda x:alist[x], r.LookAhead or [])))
elif self.Format == 3 :
backs = procCover(self.BacktrackCoverage, cname+"b")
aheads = procCover(self.LookAheadCoverage, cname+"a")
actions = procCover(self.InputCoverage, cname+"i")
procsubst(self, actions)
rules.append(make_rule(actions, None, backs, aheads))
font.addRules(rules, index)
@_add_method(otTables.SinglePos)
def process(self, font, index) :
cname = "cot_p{}".format(index)
if self.Format == 1 :
font.addClass(font.alias(cname), map(font.glyph, self.Coverage.glyphs))
rule = cname + " " + valuerectogdl(self.Value)
font.addPosRules([rule], index)
elif self.Format == 2 :
rules = []
for i, g in enumerage(map(font.glyph, self.Coverage.glyphs)) :
rule = font.glyph(g).GDLName()
rule += " " + valuerectogdl(self.Value[i])
rules.append(rule)
font.addPosRules(rules, index)
@_add_method(otTables.PairPos)
def process(self, font, index) :
pass
@_add_method(otTables.CursivePos)
def process(self, font, index) :
apname = "P{}".format(index)
if not len(font.alias(apname)) : return
if self.Format == 1 :
mark_names = self.Coverage.glyphs
for i, g in enumerate(map(font.glyph, mark_names)) :
rec = self.EntryExitRecord[i]
if rec.EntryAnchor is not None :
g.setAnchor(font.alias(apname+"_{}M".format(rec.EntryAnchor)),
rec.EntryAnchor.XCoordinate, rec.EntryAnchor.YCoordinate)
if rec.ExitAnchor is not None :
g.setAnchor(font.alias(apname+"_{}S".format(rec.ExitAnchor)),
rec.ExitAnchor.XCoordinate, rec.ExitAnchor.YCoordinate)
@_add_method(otTables.MarkBasePos)
def process(self, font, index) :
apname = "P{}".format(index)
if not len(font.alias(apname)) : return
if self.Format == 1 :
mark_names = self.MarkCoverage.glyphs
for i, g in enumerate(map(font.glyph, mark_names)) :
rec = self.MarkArray.MarkRecord[i]
g.setAnchor(font.alias(apname+"_{}M".format(rec.Class)),
rec.MarkAnchor.XCoordinate, rec.MarkAnchor.YCoordinate)
base_names = self.BaseCoverage.glyphs
for i, g in enumerate(map(font.glyph, base_names)) :
for j,b in enumerate(self.BaseArray.BaseRecord[i].BaseAnchor) :
if b : g.setAnchor(font.alias(apname+"_{}S".format(j)),
b.XCoordinate, b.YCoordinate)
@_add_method(otTables.MarkMarkPos)
def process(self, font, index) :
apname = "P{}".format(index)
if not len(font.alias(apname)) : return
if self.Format == 1 :
mark_names = self.Mark1Coverage.glyphs
for i, g in enumerate(map(font.glyph, mark_names)) :
rec = self.Mark1Array.MarkRecord[i]
g.setAnchor(font.alias(apname+"_{}M".format(rec.Class)),
rec.MarkAnchor.XCoordinate, rec.MarkAnchor.YCoordinate)
base_names = self.Mark2Coverage.glyphs
for i, g in enumerate(map(font.glyph, base_names)) :
for j,b in enumerate(self.Mark2Array.Mark2Record[i].Mark2Anchor) :
if b : g.setAnchor(font.alias(apname+"_{}S".format(j)),
b.XCoordinate, b.YCoordinate)
@_add_method(otTables.ContextSubst)
def process(self, font, index) :
def procsubst(rule, action) :
for s in rule.SubstLookupRecord :
action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
def procCover(cs, name) :
res = []
for i, c in enumerate(cs) :
if len(c.glyphs) > 1 :
n = font.alias(name+"{}".format(i))
font.addClass(n, map(font.glyph, c.glyphs))
res.append(n)
else :
res.append(font.glyph(c.glyphs[0]).GDLName())
return res
cname = "cot_cs{}".format(index)
if not len(font.alias(cname)) : return
rules = []
if self.Format == 1 :
for i in range(len(self.SubRuleSet)) :
for r in self.SubRuleSet[i].SubRule :
action = [self.Coverage.glyphs[i]] + r.Input
procsubst(r, action)
rules.append(make_rule(action, None, None, None))
elif self.Format == 2 :
ilist = add_class_classes(font, cname+"i", self.ClassDef)
for i, s in enumerate(self.SubClassSet) :
if s is None : continue
for r in s.SubClassRule :
action = map(lambda x:ilist[x], [i]+r.Class)
procsubst(r, action)
rules.append(make_rule(action, None, None, None))
elif self.Format == 3 :
actions = procCover(self.Coverage, cname+"i")
procsubst(self, actions)
rules.append(make_rule(actions, None, None, None))
font.addRules(rules, index)
@_add_method(otTables.ContextPos)
def process(self, font, index) :
def procsubst(rule, action) :
for s in rule.PosLookupRecord :
action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
def procCover(cs, name) :
res = []
for i, c in enumerate(cs) :
if len(c.glyphs) > 1 :
n = font.alias(name+"{}".format(i))
font.addClass(n, map(font.glyph, c.glyphs))
res.append(n)
else :
res.append(font.glyph(c.glyphs[0]).GDLName())
return res
cname = "cot_cp{}".format(index)
if not len(font.alias(cname)) : return
rules = []
if self.Format == 1 :
for i in range(len(self.PosRuleSet)) :
for r in self.PosRuleSet[i] :
action = [self.Coverage.glyphs[i]] + r.Input
procsubst(r, action)
rules.append(make_rule(action, None, None, None))
elif self.Format == 2 :
ilist = add_class_classes(font, cname+"i", self.ClassDef)
for i, s in enumerate(self.PosClassSet) :
if s is None : continue
for r in s.PosClassRule :
action = map(lambda x:ilist[x], [i]+r.Class)
procsubst(r, action)
rules.append(make_rule(action, None, None, None))
elif self.Format == 3 :
actions = procCover(self.Coverage, cname+"i")
procsubst(self, actions)
rules.append(make_rule(actions, None, None, None))
font.addPosRules(rules, index)
@_add_method(otTables.ChainContextPos)
def process(self, font, index) :
def procsubst(rule, action) :
for s in rule.PosLookupRecord :
action[s.SequenceIndex] += "/*{}*/".format(s.LookupListIndex)
def procCover(cs, name) :
res = []
for i, c in enumerate(cs) :
if len(c.glyphs) > 1 :
n = font.alias(name+"{}".format(i))
font.addClass(n, map(font.glyph, c.glyphs))
res.append(n)
else :
res.append(font.glyph(c.glyphs[0]).GDLName())
return res
cname = "cot_c{}".format(index)
if not len(font.alias(cname)) : return
rules = []
if self.Format == 1 :
for i in range(len(self.ChainPosRuleSet)) :
for r in self.ChainPosRuleSet[i].ChainPosRule :
action = [self.Coverage.glyphs[i]] + r.Input
procsubst(r, action)
rules.append(make_rule(action, None, r.Backtrack, r.LookAhead))
elif self.Format == 2 :
ilist = add_class_classes(font, cname+"i", self.InputClassDef)
if self.BacktrackClassDef :
blist = add_class_classes(font, cname+"b", self.BacktrackClassDef)
if self.LookAheadClassDef :
alist = add_class_classes(font, cname+"a", self.LookAheadClassDef)
for i, s in enumerate(self.ChainPosClassSet) :
if s is None : continue
for r in s.ChainPosClassRule :
action = map(lambda x:ilist[x], [i]+r.Input)
procsubst(r, action)
rules.append(make_rule(action, None,
map(lambda x:blist[x], r.Backtrack or []),
map(lambda x:alist[x], r.LookAhead or [])))
elif self.Format == 3 :
backs = procCover(self.BacktrackCoverage, cname+"b")
aheads = procCover(self.LookAheadCoverage, cname+"a")
actions = procCover(self.InputCoverage, cname+"i")
procsubst(self, actions)
rules.append(make_rule(actions, None, backs, aheads))
font.addPosRules(rules, index)

4506
examples/gdl/psnames.py Normal file

File diff suppressed because it is too large Load diff

9
examples/preflight Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
# Sample script for calling multiple routines on a project, typically prior to committing to a repository.
# Place this in root of a project, adjust the font path, then set it to be executable by typing:
# chmod +x preflight
psfnormalize -p checkfix=fix source/font-Regular.ufo
psfnormalize -p checkfix=fix source/font-Bold.ufo
psfsyncmasters source/font-RB.designspace

53
examples/psfaddGlyphDemo.py Executable file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env python3
'''Demo script for UFOlib to add a glyph to a UFO font'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
import silfont.ufo as ufo
from xml.etree import cElementTree as ET
suffix = '_addGlyph'
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'outfont'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': suffix+'log'})]
def doit(args) :
''' This will add the following glyph to the font
<?xml version="1.0" encoding="UTF-8"?>
<glyph name="Test" format="1">
<unicode hex="007D"/>
<outline>
<contour>
<point x="275" y="1582" type="line"/>
<point x="275" y="-493" type="line"/>
</contour>
</outline>
</glyph>
'''
font = args.ifont
# Create basic glyph
newglyph = ufo.Uglif(layer = font.deflayer, name = "Test")
newglyph.add("unicode", {"hex": "007D"})
# Add an outline
newglyph.add("outline")
# Create a contour and add to outline
element = ET.Element("contour")
ET.SubElement(element, "point", {"x": "275", "y": "1582", "type": "line"})
ET.SubElement(element, "point", {"x": "275", "y": "-493", "type": "line"})
contour =ufo.Ucontour(newglyph["outline"],element)
newglyph["outline"].appendobject(contour, "contour")
font.deflayer.addGlyph(newglyph)
return args.ifont
def cmd() : execute("UFO",doit,argspec)
if __name__ == "__main__": cmd()

641
examples/psfexpandstroke.py Executable file
View file

@ -0,0 +1,641 @@
#!/usr/bin/env python3
from __future__ import unicode_literals
'''Expands an unclosed UFO stroke font into monoline forms with a fixed width'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2017 SIL International (https://www.sil.org), based on outlinerRoboFontExtension Copyright (c) 2016 Frederik Berlaen'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'Victor Gaultney'
# Usage: psfexpandstroke ifont ofont expansion
# expansion is the number of units added to each side of the stroke
# To Do
# - Simplify to assume round caps and corners
# main input, output, and execution handled by pysilfont framework
from silfont.core import execute
from fontTools.pens.basePen import BasePen
from fontTools.misc.bezierTools import splitCubicAtT
from robofab.world import OpenFont
from robofab.pens.pointPen import AbstractPointPen
from robofab.pens.reverseContourPointPen import ReverseContourPointPen
from robofab.pens.adapterPens import PointToSegmentPen
from defcon import Glyph
from math import sqrt, cos, sin, acos, asin, degrees, radians, pi
suffix = '_expanded'
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'filename'}),
('ofont',{'help': 'Output font file','nargs': '?' }, {'type': 'filename', 'def': "_"+suffix}),
('thickness',{'help': 'Stroke thickness'}, {}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': suffix+'.log'})]
# The following functions are straight from outlinerRoboFontExtension
def roundFloat(f):
error = 1000000.
return round(f*error)/error
def checkSmooth(firstAngle, lastAngle):
if firstAngle is None or lastAngle is None:
return True
error = 4
firstAngle = degrees(firstAngle)
lastAngle = degrees(lastAngle)
if int(firstAngle) + error >= int(lastAngle) >= int(firstAngle) - error:
return True
return False
def checkInnerOuter(firstAngle, lastAngle):
if firstAngle is None or lastAngle is None:
return True
dirAngle = degrees(firstAngle) - degrees(lastAngle)
if dirAngle > 180:
dirAngle = 180 - dirAngle
elif dirAngle < -180:
dirAngle = -180 - dirAngle
if dirAngle > 0:
return True
if dirAngle <= 0:
return False
def interSect((seg1s, seg1e), (seg2s, seg2e)):
denom = (seg2e.y - seg2s.y)*(seg1e.x - seg1s.x) - (seg2e.x - seg2s.x)*(seg1e.y - seg1s.y)
if roundFloat(denom) == 0:
# print 'parallel: %s' % denom
return None
uanum = (seg2e.x - seg2s.x)*(seg1s.y - seg2s.y) - (seg2e.y - seg2s.y)*(seg1s.x - seg2s.x)
ubnum = (seg1e.x - seg1s.x)*(seg1s.y - seg2s.y) - (seg1e.y - seg1s.y)*(seg1s.x - seg2s.x)
ua = uanum / denom
# ub = ubnum / denom
x = seg1s.x + ua*(seg1e.x - seg1s.x)
y = seg1s.y + ua*(seg1e.y - seg1s.y)
return MathPoint(x, y)
def pointOnACurve((x1, y1), (cx1, cy1), (cx2, cy2), (x2, y2), value):
dx = x1
cx = (cx1 - dx) * 3.0
bx = (cx2 - cx1) * 3.0 - cx
ax = x2 - dx - cx - bx
dy = y1
cy = (cy1 - dy) * 3.0
by = (cy2 - cy1) * 3.0 - cy
ay = y2 - dy - cy - by
mx = ax*(value)**3 + bx*(value)**2 + cx*(value) + dx
my = ay*(value)**3 + by*(value)**2 + cy*(value) + dy
return MathPoint(mx, my)
class MathPoint(object):
def __init__(self, x, y=None):
if y is None:
x, y = x
self.x = x
self.y = y
def __repr__(self):
return "<MathPoint x:%s y:%s>" % (self.x, self.y)
def __getitem__(self, index):
if index == 0:
return self.x
if index == 1:
return self.y
raise IndexError
def __iter__(self):
for value in [self.x, self.y]:
yield value
def __add__(self, p): # p+ p
if not isinstance(p, self.__class__):
return self.__class__(self.x + p, self.y + p)
return self.__class__(self.x + p.x, self.y + p.y)
def __sub__(self, p): # p - p
if not isinstance(p, self.__class__):
return self.__class__(self.x - p, self.y - p)
return self.__class__(self.x - p.x, self.y - p.y)
def __mul__(self, p): # p * p
if not isinstance(p, self.__class__):
return self.__class__(self.x * p, self.y * p)
return self.__class__(self.x * p.x, self.y * p.y)
def __div__(self, p):
if not isinstance(p, self.__class__):
return self.__class__(self.x / p, self.y / p)
return self.__class__(self.x / p.x, self.y / p.y)
def __eq__(self, p): # if p == p
if not isinstance(p, self.__class__):
return False
return roundFloat(self.x) == roundFloat(p.x) and roundFloat(self.y) == roundFloat(p.y)
def __ne__(self, p): # if p != p
return not self.__eq__(p)
def copy(self):
return self.__class__(self.x, self.y)
def round(self):
self.x = round(self.x)
self.y = round(self.y)
def distance(self, p):
return sqrt((p.x - self.x)**2 + (p.y - self.y)**2)
def angle(self, other, add=90):
# returns the angle of a Line in radians
b = other.x - self.x
a = other.y - self.y
c = sqrt(a**2 + b**2)
if c == 0:
return None
if add is None:
return b/c
cosAngle = degrees(acos(b/c))
sinAngle = degrees(asin(a/c))
if sinAngle < 0:
cosAngle = 360 - cosAngle
return radians(cosAngle + add)
class CleanPointPen(AbstractPointPen):
def __init__(self, pointPen):
self.pointPen = pointPen
self.currentContour = None
def processContour(self):
pointPen = self.pointPen
contour = self.currentContour
index = 0
prevAngle = None
toRemove = []
for data in contour:
if data["segmentType"] in ["line", "move"]:
prevPoint = contour[index-1]
if prevPoint["segmentType"] in ["line", "move"]:
angle = MathPoint(data["point"]).angle(MathPoint(prevPoint["point"]))
if prevAngle is not None and angle is not None and roundFloat(prevAngle) == roundFloat(angle):
prevPoint["uniqueID"] = id(prevPoint)
toRemove.append(prevPoint)
prevAngle = angle
else:
prevAngle = None
else:
prevAngle = None
index += 1
for data in toRemove:
contour.remove(data)
pointPen.beginPath()
for data in contour:
pointPen.addPoint(data["point"], **data)
pointPen.endPath()
def beginPath(self):
assert self.currentContour is None
self.currentContour = []
self.onCurve = []
def endPath(self):
assert self.currentContour is not None
self.processContour()
self.currentContour = None
def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs):
data = dict(point=pt, segmentType=segmentType, smooth=smooth, name=name)
data.update(kwargs)
self.currentContour.append(data)
def addComponent(self, glyphName, transform):
assert self.currentContour is None
self.pointPen.addComponent(glyphName, transform)
# The following class has been been adjusted to work around how outline types use closePath() and endPath(),
# to remove unneeded bits, and hard-code some assumptions.
class OutlinePen(BasePen):
pointClass = MathPoint
magicCurve = 0.5522847498
def __init__(self, glyphSet, offset=10, contrast=0, contrastAngle=0, connection="round", cap="round", miterLimit=None, optimizeCurve=True):
BasePen.__init__(self, glyphSet)
self.offset = abs(offset)
self.contrast = abs(contrast)
self.contrastAngle = contrastAngle
self._inputmiterLimit = miterLimit
if miterLimit is None:
miterLimit = self.offset * 2
self.miterLimit = abs(miterLimit)
self.optimizeCurve = optimizeCurve
self.connectionCallback = getattr(self, "connection%s" % (connection.title()))
self.capCallback = getattr(self, "cap%s" % (cap.title()))
self.originalGlyph = Glyph()
self.originalPen = self.originalGlyph.getPen()
self.outerGlyph = Glyph()
self.outerPen = self.outerGlyph.getPen()
self.outerCurrentPoint = None
self.outerFirstPoint = None
self.outerPrevPoint = None
self.innerGlyph = Glyph()
self.innerPen = self.innerGlyph.getPen()
self.innerCurrentPoint = None
self.innerFirstPoint = None
self.innerPrevPoint = None
self.prevPoint = None
self.firstPoint = None
self.firstAngle = None
self.prevAngle = None
self.shouldHandleMove = True
self.components = []
self.drawSettings()
def _moveTo(self, (x, y)):
if self.offset == 0:
self.outerPen.moveTo((x, y))
self.innerPen.moveTo((x, y))
return
self.originalPen.moveTo((x, y))
p = self.pointClass(x, y)
self.prevPoint = p
self.firstPoint = p
self.shouldHandleMove = True
def _lineTo(self, (x, y)):
if self.offset == 0:
self.outerPen.lineTo((x, y))
self.innerPen.lineTo((x, y))
return
self.originalPen.lineTo((x, y))
currentPoint = self.pointClass(x, y)
if currentPoint == self.prevPoint:
return
self.currentAngle = self.prevPoint.angle(currentPoint)
thickness = self.getThickness(self.currentAngle)
self.innerCurrentPoint = self.prevPoint - self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness
self.outerCurrentPoint = self.prevPoint + self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness
if self.shouldHandleMove:
self.shouldHandleMove = False
self.innerPen.moveTo(self.innerCurrentPoint)
self.innerFirstPoint = self.innerCurrentPoint
self.outerPen.moveTo(self.outerCurrentPoint)
self.outerFirstPoint = self.outerCurrentPoint
self.firstAngle = self.currentAngle
else:
self.buildConnection()
self.innerCurrentPoint = currentPoint - self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness
self.innerPen.lineTo(self.innerCurrentPoint)
self.innerPrevPoint = self.innerCurrentPoint
self.outerCurrentPoint = currentPoint + self.pointClass(cos(self.currentAngle), sin(self.currentAngle)) * thickness
self.outerPen.lineTo(self.outerCurrentPoint)
self.outerPrevPoint = self.outerCurrentPoint
self.prevPoint = currentPoint
self.prevAngle = self.currentAngle
def _curveToOne(self, (x1, y1), (x2, y2), (x3, y3)):
if self.optimizeCurve:
curves = splitCubicAtT(self.prevPoint, (x1, y1), (x2, y2), (x3, y3), .5)
else:
curves = [(self.prevPoint, (x1, y1), (x2, y2), (x3, y3))]
for curve in curves:
p1, h1, h2, p2 = curve
self._processCurveToOne(h1, h2, p2)
def _processCurveToOne(self, (x1, y1), (x2, y2), (x3, y3)):
if self.offset == 0:
self.outerPen.curveTo((x1, y1), (x2, y2), (x3, y3))
self.innerPen.curveTo((x1, y1), (x2, y2), (x3, y3))
return
self.originalPen.curveTo((x1, y1), (x2, y2), (x3, y3))
p1 = self.pointClass(x1, y1)
p2 = self.pointClass(x2, y2)
p3 = self.pointClass(x3, y3)
if p1 == self.prevPoint:
p1 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.01)
if p2 == p3:
p2 = pointOnACurve(self.prevPoint, p1, p2, p3, 0.99)
a1 = self.prevPoint.angle(p1)
a2 = p2.angle(p3)
self.currentAngle = a1
tickness1 = self.getThickness(a1)
tickness2 = self.getThickness(a2)
a1bis = self.prevPoint.angle(p1, 0)
a2bis = p3.angle(p2, 0)
intersectPoint = interSect((self.prevPoint, self.prevPoint + self.pointClass(cos(a1), sin(a1)) * 100),
(p3, p3 + self.pointClass(cos(a2), sin(a2)) * 100))
self.innerCurrentPoint = self.prevPoint - self.pointClass(cos(a1), sin(a1)) * tickness1
self.outerCurrentPoint = self.prevPoint + self.pointClass(cos(a1), sin(a1)) * tickness1
if self.shouldHandleMove:
self.shouldHandleMove = False
self.innerPen.moveTo(self.innerCurrentPoint)
self.innerFirstPoint = self.innerPrevPoint = self.innerCurrentPoint
self.outerPen.moveTo(self.outerCurrentPoint)
self.outerFirstPoint = self.outerPrevPoint = self.outerCurrentPoint
self.firstAngle = a1
else:
self.buildConnection()
h1 = None
if intersectPoint is not None:
h1 = interSect((self.innerCurrentPoint, self.innerCurrentPoint + self.pointClass(cos(a1bis), sin(a1bis)) * tickness1), (intersectPoint, p1))
if h1 is None:
h1 = p1 - self.pointClass(cos(a1), sin(a1)) * tickness1
self.innerCurrentPoint = p3 - self.pointClass(cos(a2), sin(a2)) * tickness2
h2 = None
if intersectPoint is not None:
h2 = interSect((self.innerCurrentPoint, self.innerCurrentPoint + self.pointClass(cos(a2bis), sin(a2bis)) * tickness2), (intersectPoint, p2))
if h2 is None:
h2 = p2 - self.pointClass(cos(a1), sin(a1)) * tickness1
self.innerPen.curveTo(h1, h2, self.innerCurrentPoint)
self.innerPrevPoint = self.innerCurrentPoint
########
h1 = None
if intersectPoint is not None:
h1 = interSect((self.outerCurrentPoint, self.outerCurrentPoint + self.pointClass(cos(a1bis), sin(a1bis)) * tickness1), (intersectPoint, p1))
if h1 is None:
h1 = p1 + self.pointClass(cos(a1), sin(a1)) * tickness1
self.outerCurrentPoint = p3 + self.pointClass(cos(a2), sin(a2)) * tickness2
h2 = None
if intersectPoint is not None:
h2 = interSect((self.outerCurrentPoint, self.outerCurrentPoint + self.pointClass(cos(a2bis), sin(a2bis)) * tickness2), (intersectPoint, p2))
if h2 is None:
h2 = p2 + self.pointClass(cos(a1), sin(a1)) * tickness1
self.outerPen.curveTo(h1, h2, self.outerCurrentPoint)
self.outerPrevPoint = self.outerCurrentPoint
self.prevPoint = p3
self.currentAngle = a2
self.prevAngle = a2
def _closePath(self):
if self.shouldHandleMove:
return
self.originalPen.endPath()
self.innerPen.endPath()
self.outerPen.endPath()
innerContour = self.innerGlyph[-1]
outerContour = self.outerGlyph[-1]
innerContour.reverse()
innerContour[0].segmentType = "line"
outerContour[0].segmentType = "line"
self.buildCap(outerContour, innerContour)
for point in innerContour:
outerContour.addPoint((point.x, point.y), segmentType=point.segmentType, smooth=point.smooth)
self.innerGlyph.removeContour(innerContour)
def _endPath(self):
# The current way glyph outlines are processed means that _endPath() would not be called
# _closePath() is used instead
pass
def addComponent(self, glyphName, transform):
self.components.append((glyphName, transform))
# thickness
def getThickness(self, angle):
a2 = angle + pi * .5
f = abs(sin(a2 + radians(self.contrastAngle)))
f = f ** 5
return self.offset + self.contrast * f
# connections
def buildConnection(self, close=False):
if not checkSmooth(self.prevAngle, self.currentAngle):
if checkInnerOuter(self.prevAngle, self.currentAngle):
self.connectionCallback(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close)
self.connectionInnerCorner(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close)
else:
self.connectionCallback(self.innerPrevPoint, self.innerCurrentPoint, self.innerPen, close)
self.connectionInnerCorner(self.outerPrevPoint, self.outerCurrentPoint, self.outerPen, close)
def connectionRound(self, first, last, pen, close):
angle_1 = radians(degrees(self.prevAngle)+90)
angle_2 = radians(degrees(self.currentAngle)+90)
tempFirst = first - self.pointClass(cos(angle_1), sin(angle_1)) * self.miterLimit
tempLast = last + self.pointClass(cos(angle_2), sin(angle_2)) * self.miterLimit
newPoint = interSect((first, tempFirst), (last, tempLast))
if newPoint is None:
pen.lineTo(last)
return
distance1 = newPoint.distance(first)
distance2 = newPoint.distance(last)
if roundFloat(distance1) > self.miterLimit + self.contrast:
distance1 = self.miterLimit + tempFirst.distance(tempLast) * .7
if roundFloat(distance2) > self.miterLimit + self.contrast:
distance2 = self.miterLimit + tempFirst.distance(tempLast) * .7
distance1 *= self.magicCurve
distance2 *= self.magicCurve
bcp1 = first - self.pointClass(cos(angle_1), sin(angle_1)) * distance1
bcp2 = last + self.pointClass(cos(angle_2), sin(angle_2)) * distance2
pen.curveTo(bcp1, bcp2, last)
def connectionInnerCorner(self, first, last, pen, close):
if not close:
pen.lineTo(last)
# caps
def buildCap(self, firstContour, lastContour):
first = firstContour[-1]
last = lastContour[0]
first = self.pointClass(first.x, first.y)
last = self.pointClass(last.x, last.y)
self.capCallback(firstContour, lastContour, first, last, self.prevAngle)
first = lastContour[-1]
last = firstContour[0]
first = self.pointClass(first.x, first.y)
last = self.pointClass(last.x, last.y)
angle = radians(degrees(self.firstAngle)+180)
self.capCallback(lastContour, firstContour, first, last, angle)
def capRound(self, firstContour, lastContour, first, last, angle):
hookedAngle = radians(degrees(angle)+90)
p1 = first - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset
p2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset
oncurve = p1 + (p2-p1)*.5
roundness = .54
h1 = first - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness
h2 = oncurve + self.pointClass(cos(angle), sin(angle)) * self.offset * roundness
firstContour[-1].smooth = True
firstContour.addPoint((h1.x, h1.y))
firstContour.addPoint((h2.x, h2.y))
firstContour.addPoint((oncurve.x, oncurve.y), smooth=True, segmentType="curve")
h1 = oncurve - self.pointClass(cos(angle), sin(angle)) * self.offset * roundness
h2 = last - self.pointClass(cos(hookedAngle), sin(hookedAngle)) * self.offset * roundness
firstContour.addPoint((h1.x, h1.y))
firstContour.addPoint((h2.x, h2.y))
lastContour[0].segmentType = "curve"
lastContour[0].smooth = True
def drawSettings(self, drawOriginal=False, drawInner=False, drawOuter=True):
self.drawOriginal = drawOriginal
self.drawInner = drawInner
self.drawOuter = drawOuter
def drawPoints(self, pointPen):
if self.drawInner:
reversePen = ReverseContourPointPen(pointPen)
self.innerGlyph.drawPoints(CleanPointPen(reversePen))
if self.drawOuter:
self.outerGlyph.drawPoints(CleanPointPen(pointPen))
if self.drawOriginal:
if self.drawOuter:
pointPen = ReverseContourPointPen(pointPen)
self.originalGlyph.drawPoints(CleanPointPen(pointPen))
for glyphName, transform in self.components:
pointPen.addComponent(glyphName, transform)
def draw(self, pen):
pointPen = PointToSegmentPen(pen)
self.drawPoints(pointPen)
def getGlyph(self):
glyph = Glyph()
pointPen = glyph.getPointPen()
self.drawPoints(pointPen)
return glyph
# The following functions have been decoupled from the outlinerRoboFontExtension and
# effectively de-parameterized, with built-in assumptions
def calculate(glyph, strokewidth):
tickness = strokewidth
contrast = 0
contrastAngle = 0
keepBounds = False
optimizeCurve = True
miterLimit = None #assumed
corner = "round" #assumed - other options not supported
cap = "round" #assumed - other options not supported
drawOriginal = False
drawInner = True
drawOuter = True
pen = OutlinePen(glyph.getParent(),
tickness,
contrast,
contrastAngle,
connection=corner,
cap=cap,
miterLimit=miterLimit,
optimizeCurve=optimizeCurve)
glyph.draw(pen)
pen.drawSettings(drawOriginal=drawOriginal,
drawInner=drawInner,
drawOuter=drawOuter)
result = pen.getGlyph()
return result
def expandGlyph(glyph, strokewidth):
defconGlyph = glyph
outline = calculate(defconGlyph, strokewidth)
glyph.clearContours()
outline.drawPoints(glyph.getPointPen())
glyph.round()
def expandFont(targetfont, strokewidth):
font = targetfont
for glyph in font:
expandGlyph(glyph, strokewidth)
def doit(args):
infont = OpenFont(args.ifont)
outfont = args.ofont
# add try to catch bad input
strokewidth = int(args.thickness)
expandFont(infont, strokewidth)
infont.save(outfont)
return infont
def cmd() : execute(None,doit,argspec)
if __name__ == "__main__": cmd()

View file

@ -0,0 +1,30 @@
#!/usr/bin/env python3
'''Outputs an unsorted csv file containing the names of all the glyphs in the default layer
and their primary unicode values. Format name,usv'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2018 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'Victor Gaultney'
from silfont.core import execute
suffix = "_namesunicodes"
argspec = [
('ifont', {'help': 'Input font file'}, {'type': 'infont'}),
('-o','--output',{'help': 'Output csv file'}, {'type': 'outfile', 'def': suffix+'.csv'})]
def doit(args) :
font = args.ifont
outfile = args.output
for glyph in font:
unival = ""
if glyph.unicode:
unival = str.upper(hex(glyph.unicode))[2:7].zfill(4)
outfile.write(glyph.name + "," + unival + "\n")
print("Done")
def cmd() : execute("FP",doit,argspec)
if __name__ == "__main__": cmd()

189
examples/psfgenftml.py Normal file
View file

@ -0,0 +1,189 @@
#!/usr/bin/env python3
'''
Example script to generate ftml document from glyph_data.csv and UFO.
To try this with the Harmattan font project:
1) clone and build Harmattan:
clone https://github.com/silnrsi/font-harmattan
cd font-harmattan
smith configure
smith build ftml
2) run psfgenftml as follows:
python3 psfgenftml.py \
-t "AllChars" \
--ap "_?dia[AB]$" \
--xsl ../tools/lib/ftml.xsl \
--scale 200 \
-i source/glyph_data.csv \
-s "url(../references/Harmattan-Regular-v1.ttf)=ver 1" \
-s "url(../results/Harmattan-Regular.ttf)=Reg-GR" \
-s "url(../results/tests/ftml/fonts/Harmattan-Regular_ot_arab.ttf)=Reg-OT" \
source/Harmattan-Regular.ufo tests/AllChars-dev.ftml
3) launch resulting output file, tests/AllChars-dev.ftml, in a browser.
(see https://silnrsi.github.io/FDBP/en-US/Browsers%20as%20a%20font%20test%20platform.html)
NB: Using Firefox will allow simultaneous display of both Graphite and OpenType rendering
4) As above but substitute:
-t "Diac Test" for the -t parameter
tests/DiacTest-dev.ftml for the final parameter
and launch tests/DiacTest-dev.ftml in a browser.
'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2018,2021 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'Bob Hallissy'
import re
from silfont.core import execute
import silfont.ftml_builder as FB
argspec = [
('ifont', {'help': 'Input UFO'}, {'type': 'infont'}),
('output', {'help': 'Output file ftml in XML format', 'nargs': '?'}, {'type': 'outfile', 'def': '_out.ftml'}),
('-i','--input', {'help': 'Glyph info csv file'}, {'type': 'incsv', 'def': 'glyph_data.csv'}),
('-f','--fontcode', {'help': 'letter to filter for glyph_data'},{}),
('-l','--log', {'help': 'Set log file name'}, {'type': 'outfile', 'def': '_ftml.log'}),
('--langs', {'help':'List of bcp47 language tags', 'default': None}, {}),
('--rtl', {'help': 'enable right-to-left features', 'action': 'store_true'}, {}),
('--norendercheck', {'help': 'do not include the RenderingUnknown check', 'action': 'store_true'}, {}),
('-t', '--test', {'help': 'name of the test to generate', 'default': None}, {}),
('-s','--fontsrc', {'help': 'font source: "url()" or "local()" optionally followed by "=label"', 'action': 'append'}, {}),
('--scale', {'help': 'percentage to scale rendered text (default 100)'}, {}),
('--ap', {'help': 'regular expression describing APs to examine', 'default': '.'}, {}),
('-w', '--width', {'help': 'total width of all <string> column (default automatic)'}, {}),
('--xsl', {'help': 'XSL stylesheet to use'}, {}),
]
def doit(args):
logger = args.logger
# Read input csv
builder = FB.FTMLBuilder(logger, incsv=args.input, fontcode=args.fontcode, font=args.ifont, ap=args.ap,
rtlenable=True, langs=args.langs)
# Override default base (25CC) for displaying combining marks:
builder.diacBase = 0x0628 # beh
# Initialize FTML document:
# Default name for test: AllChars or something based on the csvdata file:
test = args.test or 'AllChars (NG)'
widths = None
if args.width:
try:
width, units = re.match(r'(\d+)(.*)$', args.width).groups()
if len(args.fontsrc):
width = int(round(int(width)/len(args.fontsrc)))
widths = {'string': f'{width}{units}'}
logger.log(f'width: {args.width} --> {widths["string"]}', 'I')
except:
logger.log(f'Unable to parse width argument "{args.width}"', 'W')
# split labels from fontsource parameter
fontsrc = []
labels = []
for sl in args.fontsrc:
try:
s, l = sl.split('=',1)
fontsrc.append(s)
labels.append(l)
except ValueError:
fontsrc.append(sl)
labels.append(None)
ftml = FB.FTML(test, logger, rendercheck=not args.norendercheck, fontscale=args.scale,
widths=widths, xslfn=args.xsl, fontsrc=fontsrc, fontlabel=labels, defaultrtl=args.rtl)
if test.lower().startswith("allchars"):
# all chars that should be in the font:
ftml.startTestGroup('Encoded characters')
for uid in sorted(builder.uids()):
if uid < 32: continue
c = builder.char(uid)
# iterate over all permutations of feature settings that might affect this character:
for featlist in builder.permuteFeatures(uids = (uid,)):
ftml.setFeatures(featlist)
builder.render((uid,), ftml)
# Don't close test -- collect consecutive encoded chars in a single row
ftml.clearFeatures()
for langID in sorted(c.langs):
ftml.setLang(langID)
builder.render((uid,), ftml)
ftml.clearLang()
# Add unencoded specials and ligatures -- i.e., things with a sequence of USVs in the glyph_data:
ftml.startTestGroup('Specials & ligatures from glyph_data')
for basename in sorted(builder.specials()):
special = builder.special(basename)
# iterate over all permutations of feature settings that might affect this special
for featlist in builder.permuteFeatures(uids = special.uids):
ftml.setFeatures(featlist)
builder.render(special.uids, ftml)
# close test so each special is on its own row:
ftml.closeTest()
ftml.clearFeatures()
if len(special.langs):
for langID in sorted(special.langs):
ftml.setLang(langID)
builder.render(special.uids, ftml)
ftml.closeTest()
ftml.clearLang()
# Add Lam-Alef data manually
ftml.startTestGroup('Lam-Alef')
# generate list of lam and alef characters that should be in the font:
lamlist = list(filter(lambda x: x in builder.uids(), (0x0644, 0x06B5, 0x06B6, 0x06B7, 0x06B8, 0x076A, 0x08A6)))
aleflist = list(filter(lambda x: x in builder.uids(), (0x0627, 0x0622, 0x0623, 0x0625, 0x0671, 0x0672, 0x0673, 0x0675, 0x0773, 0x0774)))
# iterate over all combinations:
for lam in lamlist:
for alef in aleflist:
for featlist in builder.permuteFeatures(uids = (lam, alef)):
ftml.setFeatures(featlist)
builder.render((lam,alef), ftml)
# close test so each combination is on its own row:
ftml.closeTest()
ftml.clearFeatures()
if test.lower().startswith("diac"):
# Diac attachment:
# Representative base and diac chars:
repDiac = list(filter(lambda x: x in builder.uids(), (0x064E, 0x0650, 0x065E, 0x0670, 0x0616, 0x06E3, 0x08F0, 0x08F2)))
repBase = list(filter(lambda x: x in builder.uids(), (0x0627, 0x0628, 0x062B, 0x0647, 0x064A, 0x77F, 0x08AC)))
ftml.startTestGroup('Representative diacritics on all bases that take diacritics')
for uid in sorted(builder.uids()):
# ignore some I don't care about:
if uid < 32 or uid in (0xAA, 0xBA): continue
c = builder.char(uid)
# Always process Lo, but others only if that take marks:
if c.general == 'Lo' or c.isBase:
for diac in repDiac:
for featlist in builder.permuteFeatures(uids = (uid,diac)):
ftml.setFeatures(featlist)
# Don't automatically separate connecting or mirrored forms into separate lines:
builder.render((uid,diac), ftml, addBreaks = False)
ftml.clearFeatures()
ftml.closeTest()
ftml.startTestGroup('All diacritics on representative bases')
for uid in sorted(builder.uids()):
# ignore non-ABS marks
if uid < 0x600 or uid in range(0xFE00, 0xFE10): continue
c = builder.char(uid)
if c.general == 'Mn':
for base in repBase:
for featlist in builder.permuteFeatures(uids = (uid,base)):
ftml.setFeatures(featlist)
builder.render((base,uid), ftml, keyUID = uid, addBreaks = False)
ftml.clearFeatures()
ftml.closeTest()
ftml.startTestGroup('Special cases')
builder.render((0x064A, 0x065E), ftml, comment="Yeh + Fatha should keep dots")
builder.render((0x064A, 0x0654), ftml, comment="Yeh + Hamza should loose dots")
ftml.closeTest()
# Write the output ftml file
ftml.writeFile(args.output)
def cmd() : execute("UFO",doit,argspec)
if __name__ == "__main__": cmd()

140
examples/psftidyfontlabufo.py Executable file
View file

@ -0,0 +1,140 @@
#!/usr/bin/env python3
__doc__ = '''Make changes to a backup UFO to match some changes made to another UFO by FontLab
When a UFO is first round-tripped through Fontlab 7, many changes are made including adding 'smooth="yes"' to many points
in glifs and removing it from others. Also if components are after contours in a glif, then they get moved to before them.
These changes make initial comparisons hard and can mask other changes.
This script takes the backup of the original font that Fontlab made and writes out a new version with contours changed
to match those in the round-tripped UFO so a diff can then be done to look for other differences.
A glif is only changed if there are no other changes to contours.
If also moves components to match.
'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2021 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute, splitfn
from xml.etree import ElementTree as ET
from silfont.ufo import Ufont
import os, glob
from difflib import ndiff
argspec = [
('ifont',{'help': 'post-fontlab ufo'}, {'type': 'infont'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': '_tidyfontlab.log'})]
def doit(args) :
flfont = args.ifont
logger = args.logger
params = args.paramsobj
fontname = args.ifont.ufodir
# Locate the oldest backup
(path, base, ext) = splitfn(fontname)
backuppath = os.path.join(path, base + ".*-*" + ext) # Backup has date/time added in format .yymmdd-hhmm
backups = glob.glob(backuppath)
if len(backups) == 0:
logger.log("No backups found matching %s so aborting..." % backuppath, "P")
return
backupname = sorted(backups)[0] # Choose the oldest backup - date/time format sorts alphabetically
logger.log(f"Opening backup font {backupname}", "P")
bfont = Ufont(backupname, params=params)
outufoname = os.path.join(path, base + ".tidied.ufo")
fllayers = {} # Dictionary of flfont layers by layer name
for layer in flfont.layers: fllayers[layer.layername] = layer
for layer in bfont.layers:
if layer.layername not in fllayers:
logger.log(f"layer {layer.layername} missing", "E")
continue
fllayer = fllayers[layer.layername]
glifchangecount = 0
smoothchangecount = 0
duplicatenodecount = 0
compchangecount = 0
for gname in layer:
glif = layer[gname]
glifchange = False
flglif = fllayer[gname]
if "outline" in glif and "outline" in flglif:
changestomake = []
otherchange = False
outline = glif["outline"]
floutline = flglif["outline"]
contours = outline.contours
if len(contours) != len(floutline.contours): break # Different number so can't all be identical!
flcontours = iter(floutline.contours)
for contour in contours:
flc = next(flcontours)
points = contour["point"]
flpoints = flc["point"]
duplicatenode = False
smoothchanges = True
if len(points) != len(flpoints): # Contours must be different!
if len(flpoints) - len(points) == 1: # Look for duplicate node issue
(different, plus, minus) = sdiff(str(ET.tostring(points[0]).strip()), str(ET.tostring(flpoints[0]).strip()))
if ET.tostring(points[0]).strip() == ET.tostring(flpoints[-1]).strip(): # With duplicate node issue first point is appended to the end
if plus == "lin" and minus == "curv": # On first point curve changed to line.
duplicatenode = True # Also still need check all the remaining points are the same
break # but next check does that
otherchange = True # Duplicate node issue above is only case where contour count can be different
break
firstpoint = True
for point in points:
flp = flpoints.pop(0)
if firstpoint and duplicatenode: # Ignore the first point since that will be different
firstpoint = False
continue
firstpoint = False
(different, plus, minus) = sdiff(str(ET.tostring(point).strip()), str(ET.tostring(flp).strip()))
if different: # points are different
if plus.strip() + minus.strip() == 'smooth="yes"':
smoothchanges = True # Only difference is addition or removal of smooth="yes"
else: # Other change to glif,so can't safely make changes
otherchange = True
if (smoothchanges or duplicatenode) and not otherchange: # Only changes to contours in glif are known issues that should be reset
flcontours = iter(floutline.contours)
for contour in list(contours):
flcontour = next(flcontours)
outline.replaceobject(contour, flcontour, "contour")
if smoothchanges:
logger.log(f'Smooth changes made to {gname}', "I")
smoothchangecount += 1
if duplicatenode:
logger.log(f'Duplicate node changes made to {gname}', "I")
duplicatenodecount += 1
glifchange = True
# Now need to move components to the front...
components = outline.components
if len(components) > 0 and len(contours) > 0 and list(outline)[0] == "contour":
oldcontours = list(contours) # Easiest way to 'move' components is to delete contours then append back at the end
for contour in oldcontours: outline.removeobject(contour, "contour")
for contour in oldcontours: outline.appendobject(contour, "contour")
logger.log(f'Component position changes made to {gname}', "I")
compchangecount += 1
glifchange = True
if glifchange: glifchangecount += 1
logger.log(f'{layer.layername}: {glifchangecount} glifs changed', 'P')
logger.log(f'{layer.layername}: {smoothchangecount} changes due to smooth, {duplicatenodecount} due to duplicate nodes and {compchangecount} due to components position', "P")
bfont.write(outufoname)
return
def sdiff(before, after): # Returns strings with the differences between the supplited strings
if before == after: return(False,"","") # First returned value is True if the strings are different
diff = ndiff(before, after)
plus = "" # Plus will have the extra characters that are only in after
minus = "" # Minus will have the characters missing from after
for d in diff:
if d[0] == "+": plus += d[2]
if d[0] == "-": minus += d[2]
return(True, plus, minus)
def cmd() : execute("UFO",doit, argspec)
if __name__ == "__main__": cmd()

327
examples/psftoneletters.py Normal file
View file

@ -0,0 +1,327 @@
#!/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()

54
examples/xmlDemo.py Executable file
View file

@ -0,0 +1,54 @@
#!/usr/bin/env python3
'Demo script for use of ETWriter'
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
import silfont.etutil as etutil
from xml.etree import cElementTree as ET
argspec = [('outfile1',{'help': 'output file 1','default': './xmlDemo.xml','nargs': '?'}, {'type': 'outfile'}),
('outfile2',{'help': 'output file 2','nargs': '?'}, {'type': 'outfile', 'def':'_2.xml'}),
('outfile3',{'help': 'output file 3','nargs': '?'}, {'type': 'outfile', 'def':'_3.xml'})]
def doit(args) :
ofile1 = args.outfile1
ofile2 = args.outfile2
ofile3 = args.outfile3
xmlstring = "<item>\n<subitem hello='world'>\n<subsub name='moon'>\n<value>lunar</value>\n</subsub>\n</subitem>"
xmlstring += "<subitem hello='jupiter'>\n<subsub name='moon'>\n<value>IO</value>\n</subsub>\n</subitem>\n</item>"
# Using etutil's xmlitem class
xmlobj = etutil.xmlitem()
xmlobj.etree = ET.fromstring(xmlstring)
etwobj = etutil.ETWriter(xmlobj.etree)
xmlobj.outxmlstr = etwobj.serialize_xml()
ofile1.write(xmlobj.outxmlstr)
# Just using ETWriter
etwobj = etutil.ETWriter( ET.fromstring(xmlstring) )
xmlstr = etwobj.serialize_xml()
ofile2.write(xmlstr)
# Changing parameters
etwobj = etutil.ETWriter( ET.fromstring(xmlstring) )
etwobj.indentIncr = " "
etwobj.indentFirst = ""
xmlstr = etwobj.serialize_xml()
ofile3.write(xmlstr)
# Close files and exit
ofile1.close()
ofile2.close()
ofile3.close()
return
def cmd() : execute("",doit,argspec)
if __name__ == "__main__": cmd()