Adding upstream version 1.8.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
c48d95b7fa
commit
e40b3259c1
2403 changed files with 153656 additions and 0 deletions
99
examples/FFmapGdlNames.py
Executable file
99
examples/FFmapGdlNames.py
Executable 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
72
examples/FFmapGdlNames2.py
Executable 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
118
examples/FLWriteXml.py
Executable 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
24
examples/FTMLnorm.py
Normal 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
49
examples/FTaddEmptyOT.py
Normal 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()
|
34
examples/accesslibplist.py
Normal file
34
examples/accesslibplist.py
Normal 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
43
examples/chaindemo.py
Normal 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
17
examples/fbonecheck.py
Normal 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
65
examples/fbttfchecks.py
Normal 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
65
examples/ffchangeglyphnames.py
Executable 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
161
examples/ffcopyglyphs.py
Normal 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
31
examples/ffremovealloverlaps.py
Executable 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()
|
31
examples/fontforge-old/FFaddPUA.py
Executable file
31
examples/fontforge-old/FFaddPUA.py
Executable 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()
|
62
examples/fontforge-old/FFcheckDupUSV.py
Executable file
62
examples/fontforge-old/FFcheckDupUSV.py
Executable 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()
|
51
examples/fontforge-old/FFcolourGlyphs.py
Executable file
51
examples/fontforge-old/FFcolourGlyphs.py
Executable 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()
|
44
examples/fontforge-old/FFcompareFonts.py
Executable file
44
examples/fontforge-old/FFcompareFonts.py
Executable 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()
|
48
examples/fontforge-old/FFdblEncode.py
Executable file
48
examples/fontforge-old/FFdblEncode.py
Executable 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()
|
74
examples/fontforge-old/FFfromAP.py
Executable file
74
examples/fontforge-old/FFfromAP.py
Executable 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()
|
38
examples/fontforge-old/FFlistAPNum.py
Executable file
38
examples/fontforge-old/FFlistAPNum.py
Executable 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()
|
22
examples/fontforge-old/FFlistGlyphNames.py
Executable file
22
examples/fontforge-old/FFlistGlyphNames.py
Executable 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()
|
61
examples/fontforge-old/FFlistGlyphinfo.py
Executable file
61
examples/fontforge-old/FFlistGlyphinfo.py
Executable 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()
|
33
examples/fontforge-old/FFlistRefNum.py
Executable file
33
examples/fontforge-old/FFlistRefNum.py
Executable 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()
|
38
examples/fontforge-old/FFnameSearchNReplace.py
Executable file
38
examples/fontforge-old/FFnameSearchNReplace.py
Executable 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()
|
50
examples/fontforge-old/FFundblEncode.py
Executable file
50
examples/fontforge-old/FFundblEncode.py
Executable 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()
|
38
examples/fontforge-old/demoAddToMenu.py
Executable file
38
examples/fontforge-old/demoAddToMenu.py
Executable 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.
|
||||
'''
|
29
examples/fontforge-old/demoExecuteScript.py
Executable file
29
examples/fontforge-old/demoExecuteScript.py
Executable 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'''
|
90
examples/fontforge-old/demoFunctions.py
Executable file
90
examples/fontforge-old/demoFunctions.py
Executable 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
20
examples/gdl/__init__.py
Normal 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
394
examples/gdl/font.py
Normal 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
174
examples/gdl/glyph.py
Normal 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
31
examples/gdl/makeGdl.py
Executable 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
448
examples/gdl/ot.py
Normal 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
4506
examples/gdl/psnames.py
Normal file
File diff suppressed because it is too large
Load diff
9
examples/preflight
Executable file
9
examples/preflight
Executable 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
53
examples/psfaddGlyphDemo.py
Executable 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
641
examples/psfexpandstroke.py
Executable 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()
|
30
examples/psfexportnamesunicodesfp.py
Normal file
30
examples/psfexportnamesunicodesfp.py
Normal 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
189
examples/psfgenftml.py
Normal 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
140
examples/psftidyfontlabufo.py
Executable 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
327
examples/psftoneletters.py
Normal 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
54
examples/xmlDemo.py
Executable 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()
|
Loading…
Add table
Add a link
Reference in a new issue